From c6e302961a5667832262debf142cebbd230e65bf Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 24 Sep 2024 12:24:16 -0600 Subject: [PATCH 001/373] Rename sheet.code_runs to sheet.data_tables --- quadratic-core/src/controller/dependencies.rs | 2 +- .../execute_operation/execute_code.rs | 2 +- .../execute_operation/execute_col_rows.rs | 2 +- .../execution/receive_multiplayer.rs | 2 +- .../src/controller/execution/run_code/mod.rs | 15 ++++++------ .../execution/run_code/run_javascript.rs | 2 +- .../execution/run_code/run_python.rs | 2 +- .../src/controller/execution/spills.rs | 16 ++++++------- .../src/controller/operations/code_cell.rs | 4 ++-- quadratic-core/src/grid/file/mod.rs | 8 +++---- .../src/grid/file/serialize/sheets.rs | 4 ++-- quadratic-core/src/grid/sheet.rs | 12 +++++----- quadratic-core/src/grid/sheet/bounds.rs | 2 +- quadratic-core/src/grid/sheet/cell_array.rs | 2 +- quadratic-core/src/grid/sheet/code.rs | 18 +++++++------- .../src/grid/sheet/col_row/column.rs | 18 +++++++------- quadratic-core/src/grid/sheet/col_row/row.rs | 24 +++++++++---------- quadratic-core/src/grid/sheet/rendering.rs | 10 ++++---- quadratic-core/src/grid/sheet/search.rs | 2 +- quadratic-core/src/grid/sheet/selection.rs | 8 +++---- quadratic-core/src/test_util.rs | 2 +- 21 files changed, 79 insertions(+), 78 deletions(-) diff --git a/quadratic-core/src/controller/dependencies.rs b/quadratic-core/src/controller/dependencies.rs index 41aaa8b4f3..68e5f9843c 100644 --- a/quadratic-core/src/controller/dependencies.rs +++ b/quadratic-core/src/controller/dependencies.rs @@ -12,7 +12,7 @@ impl GridController { let mut dependent_cells = HashSet::new(); self.grid.sheets().iter().for_each(|sheet| { - sheet.code_runs.iter().for_each(|(pos, code_run)| { + sheet.data_tables.iter().for_each(|(pos, code_run)| { code_run.cells_accessed.iter().for_each(|cell_accessed| { if sheet_rect.intersects(*cell_accessed) { dependent_cells.insert(pos.to_sheet_pos(sheet.id)); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs index b595a5dd34..f6fb759be6 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs @@ -51,7 +51,7 @@ impl GridController { }; let rect: Rect = (*sheet_rect).into(); let code_runs_to_delete: Vec = sheet - .code_runs + .data_tables .iter() .filter_map(|(pos, _)| { // only delete code runs that are within the sheet_rect diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs b/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs index ce3f8206f6..dafe611b4f 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs @@ -19,7 +19,7 @@ impl GridController { delta: i64, ) { self.grid.sheets().iter().for_each(|sheet| { - sheet.code_runs.iter().for_each(|(pos, code_run)| { + sheet.data_tables.iter().for_each(|(pos, code_run)| { if let Some(column) = column { if code_run.cells_accessed.iter().any(|sheet_rect| { // if the cells accessed is beyond the column that was deleted diff --git a/quadratic-core/src/controller/execution/receive_multiplayer.rs b/quadratic-core/src/controller/execution/receive_multiplayer.rs index 9bac10bfd4..bb1d2fc0ff 100644 --- a/quadratic-core/src/controller/execution/receive_multiplayer.rs +++ b/quadratic-core/src/controller/execution/receive_multiplayer.rs @@ -1138,7 +1138,7 @@ mod tests { ); let find_index = |sheet: &Sheet, x: i64, y: i64| { sheet - .code_runs + .data_tables .iter() .position(|(code_pos, _)| *code_pos == Pos { x, y }) .unwrap() diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 16944c71c4..1d509dca73 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -33,24 +33,25 @@ impl GridController { // index for SetCodeRun is either set by execute_set_code_run or calculated let index = index.unwrap_or( sheet - .code_runs + .data_tables .iter() .position(|(p, _)| p == &pos) - .unwrap_or(sheet.code_runs.len()), + .unwrap_or(sheet.data_tables.len()), ); let old_code_run = if let Some(new_code_run) = &new_code_run { - let (old_index, old_code_run) = sheet.code_runs.insert_full(pos, new_code_run.clone()); + let (old_index, old_code_run) = + sheet.data_tables.insert_full(pos, new_code_run.clone()); // keep the orderings of the code runs consistent, particularly when undoing/redoing - let index = if index > sheet.code_runs.len() - 1 { - sheet.code_runs.len() - 1 + let index = if index > sheet.data_tables.len() - 1 { + sheet.data_tables.len() - 1 } else { index }; - sheet.code_runs.move_index(old_index, index); + sheet.data_tables.move_index(old_index, index); old_code_run } else { - sheet.code_runs.shift_remove(&pos) + sheet.data_tables.shift_remove(&pos) }; if old_code_run == new_code_run { diff --git a/quadratic-core/src/controller/execution/run_code/run_javascript.rs b/quadratic-core/src/controller/execution/run_code/run_javascript.rs index 31880e7efc..82154a4420 100644 --- a/quadratic-core/src/controller/execution/run_code/run_javascript.rs +++ b/quadratic-core/src/controller/execution/run_code/run_javascript.rs @@ -79,7 +79,7 @@ mod tests { } _ => panic!("expected code cell"), } - let code_run = sheet.code_runs.get(&pos).unwrap(); + let code_run = sheet.data_tables.get(&pos).unwrap(); assert_eq!(code_run.output_size(), ArraySize::_1X1); assert_eq!( code_run.cell_value_at(0, 0), diff --git a/quadratic-core/src/controller/execution/run_code/run_python.rs b/quadratic-core/src/controller/execution/run_code/run_python.rs index f90002bdea..8d65fbedeb 100644 --- a/quadratic-core/src/controller/execution/run_code/run_python.rs +++ b/quadratic-core/src/controller/execution/run_code/run_python.rs @@ -79,7 +79,7 @@ mod tests { } _ => panic!("expected code cell"), } - let code_run = sheet.code_runs.get(&pos).unwrap(); + let code_run = sheet.data_tables.get(&pos).unwrap(); assert_eq!(code_run.output_size(), ArraySize::_1X1); assert_eq!( code_run.cell_value_at(0, 0), diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index 650b9e82a4..3b0c1c83a9 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -18,7 +18,7 @@ impl GridController { ) { // change the spill for the first code_cell and then iterate the later code_cells. if let Some(sheet) = self.grid.try_sheet_mut(sheet_id) { - if let Some((pos, run)) = sheet.code_runs.get_index_mut(index) { + if let Some((pos, run)) = sheet.data_tables.get_index_mut(index) { let sheet_pos = pos.to_sheet_pos(sheet.id); transaction.reverse_operations.push(Operation::SetCodeRun { sheet_pos, @@ -61,7 +61,7 @@ impl GridController { /// Checks if a code_cell has a spill error by comparing its output to both CellValues in that range, and earlier code_runs output. fn check_spill(&self, sheet_id: SheetId, index: usize) -> Option { if let Some(sheet) = self.grid.try_sheet(sheet_id) { - if let Some((pos, code_run)) = sheet.code_runs.get_index(index) { + if let Some((pos, code_run)) = sheet.data_tables.get_index(index) { // output sizes of 1x1 cannot spill if matches!(code_run.output_size(), ArraySize::_1X1) { return None; @@ -96,7 +96,7 @@ impl GridController { send_client: bool, ) { if let Some(sheet) = self.grid.try_sheet(sheet_id) { - for index in 0..sheet.code_runs.len() { + for index in 0..sheet.data_tables.len() { if let Some(spill_error) = self.check_spill(sheet_id, index) { self.change_spill(transaction, sheet_id, index, spill_error, send_client); } @@ -173,12 +173,12 @@ mod tests { sheet.set_cell_value(Pos { x: 1, y: 1 }, CellValue::Number(3.into())); let sheet = gc.grid.try_sheet(sheet_id).unwrap(); - assert!(!sheet.code_runs[0].spill_error); + assert!(!sheet.data_tables[0].spill_error); gc.check_all_spills(&mut transaction, sheet_id, false); let sheet = gc.grid.try_sheet(sheet_id).unwrap(); - assert!(sheet.code_runs[0].spill_error); + assert!(sheet.data_tables[0].spill_error); } #[test] @@ -213,11 +213,11 @@ mod tests { sheet.set_cell_value(Pos { x: 1, y: 1 }, CellValue::Number(3.into())); let sheet = gc.sheet(sheet_id); - assert!(!sheet.code_runs[0].spill_error); + assert!(!sheet.data_tables[0].spill_error); gc.check_all_spills(&mut transaction, sheet_id, false); let sheet = gc.sheet(sheet_id); - assert!(sheet.code_runs[0].spill_error); + assert!(sheet.data_tables[0].spill_error); expect_js_call_count("jsUpdateCodeCell", 0, true); // remove the cell causing the spill error @@ -227,7 +227,7 @@ mod tests { gc.check_all_spills(&mut transaction, sheet_id, true); expect_js_call_count("jsUpdateCodeCell", 1, true); let sheet = gc.sheet(sheet_id); - assert!(!sheet.code_runs[0].spill_error); + assert!(!sheet.data_tables[0].spill_error); } #[test] diff --git a/quadratic-core/src/controller/operations/code_cell.rs b/quadratic-core/src/controller/operations/code_cell.rs index 55772e1162..d2d0911804 100644 --- a/quadratic-core/src/controller/operations/code_cell.rs +++ b/quadratic-core/src/controller/operations/code_cell.rs @@ -89,7 +89,7 @@ impl GridController { return vec![]; }; let mut code_cell_positions = sheet - .code_runs + .data_tables .iter() .map(|(pos, code_run)| (pos.to_sheet_pos(sheet_id), code_run)) .collect::>(); @@ -112,7 +112,7 @@ impl GridController { .iter() .flat_map(|sheet| { sheet - .code_runs + .data_tables .iter() .map(|(pos, code_run)| (pos.to_sheet_pos(sheet.id), code_run)) }) diff --git a/quadratic-core/src/grid/file/mod.rs b/quadratic-core/src/grid/file/mod.rs index afa908935d..fb5450a699 100644 --- a/quadratic-core/src/grid/file/mod.rs +++ b/quadratic-core/src/grid/file/mod.rs @@ -219,7 +219,7 @@ mod tests { fn process_a_v1_3_single_formula_file() { let imported = import(V1_3_SINGLE_FORMULAS_CODE_CELL_FILE.to_vec()).unwrap(); assert!(imported.sheets[0] - .code_runs + .data_tables .get(&Pos { x: 0, y: 2 }) .is_some()); let cell_value = imported.sheets[0].cell_value(Pos { x: 0, y: 2 }).unwrap(); @@ -371,7 +371,7 @@ mod tests { ); assert_eq!( sheet - .code_runs + .data_tables .get(&Pos { x: 0, y: 3 }) .unwrap() .output_size(), @@ -384,9 +384,9 @@ mod tests { code: "// fix by putting a let statement in front of x \nx = 5; ".to_string(), }) ); - assert_eq!(sheet.code_runs.len(), 10); + assert_eq!(sheet.data_tables.len(), 10); assert_eq!( - sheet.code_runs.get(&Pos { x: 2, y: 6 }).unwrap().std_err, + sheet.data_tables.get(&Pos { x: 2, y: 6 }).unwrap().std_err, Some("x is not defined".into()) ); } diff --git a/quadratic-core/src/grid/file/serialize/sheets.rs b/quadratic-core/src/grid/file/serialize/sheets.rs index 276da152a2..ce6ca75db5 100644 --- a/quadratic-core/src/grid/file/serialize/sheets.rs +++ b/quadratic-core/src/grid/file/serialize/sheets.rs @@ -28,7 +28,7 @@ pub fn import_sheet(sheet: current::SheetSchema) -> Result { offsets: SheetOffsets::import(sheet.offsets), columns: import_column_builder(sheet.columns)?, - code_runs: import_code_cell_builder(sheet.code_runs)?, + data_tables: import_code_cell_builder(sheet.code_runs)?, data_bounds: GridBounds::Empty, format_bounds: GridBounds::Empty, @@ -60,7 +60,7 @@ pub(crate) fn export_sheet(sheet: Sheet) -> current::SheetSchema { validations: export_validations(sheet.validations), rows_resize: export_rows_size(sheet.rows_resize), borders: export_borders(sheet.borders), - code_runs: export_rows_code_runs(sheet.code_runs), + code_runs: export_rows_code_runs(sheet.data_tables), columns: export_column_builder(sheet.columns), } } diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 9c593ee1a2..2b6b0b61ab 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -52,7 +52,7 @@ pub struct Sheet { pub columns: BTreeMap, #[serde(with = "crate::util::indexmap_serde")] - pub code_runs: IndexMap, + pub data_tables: IndexMap, // todo: we need to redo this struct to track the timestamp for all formats // applied to column and rows to properly use the latest column or row @@ -104,7 +104,7 @@ impl Sheet { columns: BTreeMap::new(), - code_runs: IndexMap::new(), + data_tables: IndexMap::new(), formats_columns: BTreeMap::new(), formats_rows: BTreeMap::new(), @@ -194,7 +194,7 @@ impl Sheet { } // remove code_cells where the rect overlaps the anchor cell - self.code_runs.retain(|pos, _| !rect.contains(*pos)); + self.data_tables.retain(|pos, _| !rect.contains(*pos)); old_cell_values_array } @@ -214,7 +214,7 @@ impl Sheet { if let Some(cell_value) = cell_value { match cell_value { CellValue::Code(_) => self - .code_runs + .data_tables .get(&pos) .and_then(|run| run.cell_value_at(0, 0)), CellValue::Blank => self.get_code_cell_value(pos), @@ -254,7 +254,7 @@ impl Sheet { if let Some(cell_value) = cell_value { match cell_value { - CellValue::Blank | CellValue::Code(_) => match self.code_runs.get(&pos) { + CellValue::Blank | CellValue::Code(_) => match self.data_tables.get(&pos) { Some(run) => run.get_cell_for_formula(0, 0), None => CellValue::Blank, }, @@ -358,7 +358,7 @@ impl Sheet { /// Deletes all data and formatting in the sheet, effectively recreating it. pub fn clear(&mut self) { self.columns.clear(); - self.code_runs.clear(); + self.data_tables.clear(); self.recalculate_bounds(); } diff --git a/quadratic-core/src/grid/sheet/bounds.rs b/quadratic-core/src/grid/sheet/bounds.rs index bf6934d2af..a9fdf9e592 100644 --- a/quadratic-core/src/grid/sheet/bounds.rs +++ b/quadratic-core/src/grid/sheet/bounds.rs @@ -36,7 +36,7 @@ impl Sheet { self.calculate_bounds(); - self.code_runs.iter().for_each(|(pos, code_cell_value)| { + self.data_tables.iter().for_each(|(pos, code_cell_value)| { let output_rect = code_cell_value.output_rect(*pos, false); self.data_bounds.add(output_rect.min); self.data_bounds.add(output_rect.max); diff --git a/quadratic-core/src/grid/sheet/cell_array.rs b/quadratic-core/src/grid/sheet/cell_array.rs index df6a704fa8..d991730c68 100644 --- a/quadratic-core/src/grid/sheet/cell_array.rs +++ b/quadratic-core/src/grid/sheet/cell_array.rs @@ -130,7 +130,7 @@ impl Sheet { } // then check code runs - for (pos, code_run) in &self.code_runs { + for (pos, code_run) in &self.data_tables { // once we reach the code_pos, no later code runs can be the cause of the spill error if pos == &code_pos { break; diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 18e2440a5e..5e0d1e008e 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -16,22 +16,22 @@ impl Sheet { /// Returns the old value if it was set. pub fn set_code_run(&mut self, pos: Pos, code_run: Option) -> Option { if let Some(code_run) = code_run { - self.code_runs.insert(pos, code_run) + self.data_tables.insert(pos, code_run) } else { - self.code_runs.shift_remove(&pos) + self.data_tables.shift_remove(&pos) } } /// Returns a CodeCell at a Pos pub fn code_run(&self, pos: Pos) -> Option<&CodeRun> { - self.code_runs.get(&pos) + self.data_tables.get(&pos) } /// Gets column bounds for code_runs that output to the columns pub fn code_columns_bounds(&self, column_start: i64, column_end: i64) -> Option> { let mut min: Option = None; let mut max: Option = None; - for (pos, code_run) in &self.code_runs { + for (pos, code_run) in &self.data_tables { let output_rect = code_run.output_rect(*pos, false); if output_rect.min.x <= column_end && output_rect.max.x >= column_start { min = min @@ -53,7 +53,7 @@ impl Sheet { pub fn code_rows_bounds(&self, row_start: i64, row_end: i64) -> Option> { let mut min: Option = None; let mut max: Option = None; - for (pos, code_run) in &self.code_runs { + for (pos, code_run) in &self.data_tables { let output_rect = code_run.output_rect(*pos, false); if output_rect.min.y <= row_end && output_rect.max.y >= row_start { min = min @@ -75,7 +75,7 @@ impl Sheet { /// /// Note: spill error will return a CellValue::Blank to ensure calculations can continue. pub fn get_code_cell_value(&self, pos: Pos) -> Option { - self.code_runs.iter().find_map(|(code_cell_pos, code_run)| { + self.data_tables.iter().find_map(|(code_cell_pos, code_run)| { if code_run.output_rect(*code_cell_pos, false).contains(pos) { code_run.cell_value_at( (pos.x - code_cell_pos.x) as u32, @@ -88,7 +88,7 @@ impl Sheet { } pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { - self.code_runs + self.data_tables .iter() .filter_map(move |(pos, code_cell_value)| { let output_rect = code_cell_value.output_rect(*pos, false); @@ -107,7 +107,7 @@ impl Sheet { /// Returns whether a rect overlaps the output of a code cell. /// It will only check code_cells until it finds the code_run at code_pos (since later code_runs do not cause spills in earlier ones) pub fn has_code_cell_in_rect(&self, rect: &Rect, code_pos: Pos) -> bool { - for (pos, code_run) in &self.code_runs { + for (pos, code_run) in &self.data_tables { if pos == &code_pos { // once we reach the code_cell, we can stop checking return false; @@ -126,7 +126,7 @@ impl Sheet { let code_cell = if let Some(cell_value) = self.cell_value(pos) { Some(cell_value) } else { - self.code_runs.iter().find_map(|(code_cell_pos, code_run)| { + self.data_tables.iter().find_map(|(code_cell_pos, code_run)| { if code_run.output_rect(*code_cell_pos, false).contains(pos) { if let Some(code_value) = self.cell_value(*code_cell_pos) { code_pos = *code_cell_pos; diff --git a/quadratic-core/src/grid/sheet/col_row/column.rs b/quadratic-core/src/grid/sheet/col_row/column.rs index 71db982f3a..1cc2da4d85 100644 --- a/quadratic-core/src/grid/sheet/col_row/column.rs +++ b/quadratic-core/src/grid/sheet/col_row/column.rs @@ -73,7 +73,7 @@ impl Sheet { fn code_runs_for_column(&self, column: i64) -> Vec { let mut reverse_operations = Vec::new(); - self.code_runs + self.data_tables .iter() .enumerate() .for_each(|(index, (pos, code_run))| { @@ -135,7 +135,7 @@ impl Sheet { } // remove the column's code runs from the sheet - self.code_runs.retain(|pos, code_run| { + self.data_tables.retain(|pos, code_run| { if pos.x == column { transaction.add_code_cell(self.id, *pos); @@ -182,13 +182,13 @@ impl Sheet { // update the indices of all code_runs impacted by the deletion let mut code_runs_to_move = Vec::new(); - for (pos, _) in self.code_runs.iter() { + for (pos, _) in self.data_tables.iter() { if pos.x > column { code_runs_to_move.push(*pos); } } for old_pos in code_runs_to_move { - if let Some(code_run) = self.code_runs.shift_remove(&old_pos) { + if let Some(code_run) = self.data_tables.shift_remove(&old_pos) { let new_pos = Pos { x: old_pos.x - 1, y: old_pos.y, @@ -203,7 +203,7 @@ impl Sheet { transaction.add_image_cell(self.id, new_pos); } - self.code_runs.insert(new_pos, code_run); + self.data_tables.insert(new_pos, code_run); // signal client to update the code runs transaction.add_code_cell(self.id, old_pos); @@ -272,7 +272,7 @@ impl Sheet { // update the indices of all code_runs impacted by the insertion let mut code_runs_to_move = Vec::new(); - for (pos, _) in self.code_runs.iter() { + for (pos, _) in self.data_tables.iter() { if pos.x >= column { code_runs_to_move.push(*pos); } @@ -283,7 +283,7 @@ impl Sheet { x: old_pos.x + 1, y: old_pos.y, }; - if let Some(code_run) = self.code_runs.shift_remove(&old_pos) { + if let Some(code_run) = self.data_tables.shift_remove(&old_pos) { // signal html and image cells to update if code_run.is_html() { transaction.add_html_cell(self.id, old_pos); @@ -293,7 +293,7 @@ impl Sheet { transaction.add_image_cell(self.id, new_pos); } - self.code_runs.insert(new_pos, code_run); + self.data_tables.insert(new_pos, code_run); // signal the client to updates to the code cells (to draw the code arrays) transaction.add_code_cell(self.id, old_pos); @@ -433,7 +433,7 @@ mod tests { ..Default::default() } ); - assert!(sheet.code_runs.get(&Pos { x: 0, y: 2 }).is_some()); + assert!(sheet.data_tables.get(&Pos { x: 0, y: 2 }).is_some()); } #[test] diff --git a/quadratic-core/src/grid/sheet/col_row/row.rs b/quadratic-core/src/grid/sheet/col_row/row.rs index 30bc2f0c7c..b7db5f4274 100644 --- a/quadratic-core/src/grid/sheet/col_row/row.rs +++ b/quadratic-core/src/grid/sheet/col_row/row.rs @@ -71,7 +71,7 @@ impl Sheet { fn code_runs_for_row(&self, row: i64) -> Vec { let mut reverse_operations = Vec::new(); - self.code_runs + self.data_tables .iter() .enumerate() .for_each(|(index, (pos, code_run))| { @@ -204,7 +204,7 @@ impl Sheet { } // remove the column's code runs from the sheet - self.code_runs.retain(|pos, code_run| { + self.data_tables.retain(|pos, code_run| { if pos.y == row { transaction.add_code_cell(self.id, *pos); @@ -238,14 +238,14 @@ impl Sheet { // update the indices of all code_runs impacted by the deletion let mut code_runs_to_move = Vec::new(); - for (pos, _) in self.code_runs.iter() { + for (pos, _) in self.data_tables.iter() { if pos.y > row { code_runs_to_move.push(*pos); } } code_runs_to_move.sort_unstable(); for old_pos in code_runs_to_move { - if let Some(code_run) = self.code_runs.shift_remove(&old_pos) { + if let Some(code_run) = self.data_tables.shift_remove(&old_pos) { let new_pos = Pos { x: old_pos.x, y: old_pos.y - 1, @@ -260,7 +260,7 @@ impl Sheet { transaction.add_image_cell(self.id, new_pos); } - self.code_runs.insert(new_pos, code_run); + self.data_tables.insert(new_pos, code_run); // signal client to update the code runs transaction.add_code_cell(self.id, old_pos); @@ -382,7 +382,7 @@ impl Sheet { // update the indices of all code_runs impacted by the insertion let mut code_runs_to_move = Vec::new(); - for (pos, _) in self.code_runs.iter() { + for (pos, _) in self.data_tables.iter() { if pos.y >= row { code_runs_to_move.push(*pos); } @@ -394,7 +394,7 @@ impl Sheet { x: old_pos.x, y: old_pos.y + 1, }; - if let Some(code_run) = self.code_runs.shift_remove(&old_pos) { + if let Some(code_run) = self.data_tables.shift_remove(&old_pos) { // signal html and image cells to update if code_run.is_html() { transaction.add_html_cell(self.id, old_pos); @@ -404,7 +404,7 @@ impl Sheet { transaction.add_image_cell(self.id, new_pos); } - self.code_runs.insert(new_pos, code_run); + self.data_tables.insert(new_pos, code_run); // signal the client to updates to the code cells (to draw the code arrays) transaction.add_code_cell(self.id, old_pos); @@ -554,8 +554,8 @@ mod test { ..Default::default() } ); - assert!(sheet.code_runs.get(&Pos { x: 1, y: 2 }).is_some()); - assert!(sheet.code_runs.get(&Pos { x: 1, y: 3 }).is_some()); + assert!(sheet.data_tables.get(&Pos { x: 1, y: 2 }).is_some()); + assert!(sheet.data_tables.get(&Pos { x: 1, y: 3 }).is_some()); } #[test] @@ -624,8 +624,8 @@ mod test { ); assert_eq!(sheet.borders.get(5, 1).top, None); - assert!(sheet.code_runs.get(&Pos { x: 4, y: 1 }).is_none()); - assert!(sheet.code_runs.get(&Pos { x: 4, y: 2 }).is_some()); + assert!(sheet.data_tables.get(&Pos { x: 4, y: 1 }).is_none()); + assert!(sheet.data_tables.get(&Pos { x: 4, y: 2 }).is_some()); assert_eq!( sheet.display_value(Pos { x: 4, y: 2 }), diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 8f6894f523..8e05d97f4d 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -286,7 +286,7 @@ impl Sheet { } pub fn get_single_html_output(&self, pos: Pos) -> Option { - let run = self.code_runs.get(&pos)?; + let run = self.data_tables.get(&pos)?; if !run.is_html() { return None; } @@ -307,7 +307,7 @@ impl Sheet { } pub fn get_html_output(&self) -> Vec { - self.code_runs + self.data_tables .iter() .filter_map(|(pos, run)| { let output = run.cell_value_at(0, 0)?; @@ -376,7 +376,7 @@ impl Sheet { } pub fn get_render_code_cell(&self, pos: Pos) -> Option { - let run = self.code_runs.get(&pos)?; + let run = self.data_tables.get(&pos)?; let code = self.cell_value(pos)?; let output_size = run.output_size(); let (state, w, h, spill_error) = if run.spill_error { @@ -416,7 +416,7 @@ impl Sheet { /// Returns data for all rendering code cells pub fn get_all_render_code_cells(&self) -> Vec { - self.code_runs + self.data_tables .iter() .filter_map(|(pos, run)| { if let Some(code) = self.cell_value(*pos) { @@ -473,7 +473,7 @@ impl Sheet { return; } - self.code_runs.iter().for_each(|(pos, run)| { + self.data_tables.iter().for_each(|(pos, run)| { if let Some(CellValue::Image(image)) = run.cell_value_at(0, 0) { let (w, h) = if let Some(render_size) = self.render_size(*pos) { (Some(render_size.w), Some(render_size.h)) diff --git a/quadratic-core/src/grid/sheet/search.rs b/quadratic-core/src/grid/sheet/search.rs index ec8fafbe88..7c90251766 100644 --- a/quadratic-core/src/grid/sheet/search.rs +++ b/quadratic-core/src/grid/sheet/search.rs @@ -114,7 +114,7 @@ impl Sheet { whole_cell: bool, ) -> Vec { let mut results = vec![]; - self.code_runs + self.data_tables .iter() .for_each(|(pos, code_run)| match &code_run.result { CodeRunResult::Ok(value) => match value { diff --git a/quadratic-core/src/grid/sheet/selection.rs b/quadratic-core/src/grid/sheet/selection.rs index 375c75ec75..5c5704ad71 100644 --- a/quadratic-core/src/grid/sheet/selection.rs +++ b/quadratic-core/src/grid/sheet/selection.rs @@ -69,7 +69,7 @@ impl Sheet { })); } if !skip_code_runs { - for (pos, code_run) in self.code_runs.iter() { + for (pos, code_run) in self.data_tables.iter() { match code_run.result { CodeRunResult::Ok(ref value) => match value { Value::Single(v) => { @@ -141,7 +141,7 @@ impl Sheet { } } if !skip_code_runs { - for (pos, code_run) in self.code_runs.iter() { + for (pos, code_run) in self.data_tables.iter() { let rect = code_run.output_rect(*pos, false); if columns .iter() @@ -183,7 +183,7 @@ impl Sheet { } } if !skip_code_runs { - for (pos, code_run) in self.code_runs.iter() { + for (pos, code_run) in self.data_tables.iter() { let rect = code_run.output_rect(*pos, false); for y in rect.min.y..=rect.max.y { if rows.contains(&y) { @@ -231,7 +231,7 @@ impl Sheet { } } if !skip_code_runs { - for (pos, code_run) in self.code_runs.iter() { + for (pos, code_run) in self.data_tables.iter() { let rect = code_run.output_rect(*pos, false); for x in rect.min.x..=rect.max.x { for y in rect.min.y..=rect.max.y { diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index cf90cbf764..1f1938484a 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -264,7 +264,7 @@ pub fn print_table_sheet(sheet: &Sheet, rect: Rect) { /// Prints the order of the code_runs to the console. pub fn print_code_run_order(sheet: &Sheet) { dbgjs!(sheet - .code_runs + .data_tables .iter() .map(|(pos, _)| pos) .collect::>()); From b8113fa9549c573837f9454ee07bd852db63d080 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 24 Sep 2024 12:35:10 -0600 Subject: [PATCH 002/373] More Renaming of sheet.code_runs to sheet.data_tables --- quadratic-core/src/controller/dependencies.rs | 4 +- .../execute_operation/execute_code.rs | 8 +- .../execute_operation/execute_col_rows.rs | 8 +- .../execute_operation/execute_formats.rs | 4 +- .../execute_operation/execute_values.rs | 2 +- .../execution/run_code/get_cells.rs | 2 +- .../src/controller/execution/run_code/mod.rs | 8 +- .../execution/run_code/run_formula.rs | 8 +- .../src/controller/execution/spills.rs | 22 ++--- quadratic-core/src/controller/send_render.rs | 2 +- .../src/controller/user_actions/import.rs | 2 +- quadratic-core/src/grid/code_run.rs | 2 +- quadratic-core/src/grid/sheet.rs | 10 +-- quadratic-core/src/grid/sheet/cell_array.rs | 2 +- quadratic-core/src/grid/sheet/code.rs | 84 ++++++++++--------- quadratic-core/src/grid/sheet/rendering.rs | 6 +- quadratic-core/src/grid/sheet/search.rs | 14 ++-- quadratic-core/src/grid/sheet/sheet_test.rs | 6 +- quadratic-core/src/test_util.rs | 4 +- 19 files changed, 101 insertions(+), 97 deletions(-) diff --git a/quadratic-core/src/controller/dependencies.rs b/quadratic-core/src/controller/dependencies.rs index 68e5f9843c..d15f00a17e 100644 --- a/quadratic-core/src/controller/dependencies.rs +++ b/quadratic-core/src/controller/dependencies.rs @@ -7,7 +7,7 @@ use crate::{SheetPos, SheetRect}; use super::GridController; impl GridController { - /// Searches all code_runs in all sheets for cells that are dependent on the given sheet_rect. + /// Searches all data_tables in all sheets for cells that are dependent on the given sheet_rect. pub fn get_dependent_code_cells(&self, sheet_rect: &SheetRect) -> Option> { let mut dependent_cells = HashSet::new(); @@ -67,7 +67,7 @@ mod test { sheet_id, }; cells_accessed.insert(sheet_rect); - sheet.set_code_run( + sheet.set_data_table( Pos { x: 0, y: 2 }, Some(CodeRun { formatted_code_string: None, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs index f6fb759be6..3fc1ec750a 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs @@ -39,7 +39,7 @@ impl GridController { } // delete any code runs within the sheet_rect. - pub(super) fn check_deleted_code_runs( + pub(super) fn check_deleted_data_tables( &mut self, transaction: &mut PendingTransaction, sheet_rect: &SheetRect, @@ -50,7 +50,7 @@ impl GridController { return; }; let rect: Rect = (*sheet_rect).into(); - let code_runs_to_delete: Vec = sheet + let data_tables_to_delete: Vec = sheet .data_tables .iter() .filter_map(|(pos, _)| { @@ -71,7 +71,7 @@ impl GridController { } }) .collect(); - code_runs_to_delete.iter().for_each(|pos| { + data_tables_to_delete.iter().for_each(|pos| { self.finalize_code_run(transaction, pos.to_sheet_pos(sheet_id), None, None); }); } @@ -200,7 +200,7 @@ mod tests { Some(CellValue::Blank) ); - let code_cell = sheet.code_run(Pos { x: 1, y: 0 }); + let code_cell = sheet.data_table(Pos { x: 1, y: 0 }); assert!(code_cell.unwrap().spill_error); } diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs b/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs index dafe611b4f..2b564d6d2b 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs @@ -146,7 +146,7 @@ impl GridController { if let GridBounds::NonEmpty(bounds) = sheet.bounds(true) { let mut sheet_rect = bounds.to_sheet_rect(sheet_id); sheet_rect.min.x = column; - self.check_deleted_code_runs(transaction, &sheet_rect); + self.check_deleted_data_tables(transaction, &sheet_rect); self.add_compute_operations(transaction, &sheet_rect, None); self.check_all_spills(transaction, sheet_rect.sheet_id, true); } @@ -183,7 +183,7 @@ impl GridController { if let GridBounds::NonEmpty(bounds) = sheet.bounds(true) { let mut sheet_rect = bounds.to_sheet_rect(sheet_id); sheet_rect.min.y = row; - self.check_deleted_code_runs(transaction, &sheet_rect); + self.check_deleted_data_tables(transaction, &sheet_rect); self.add_compute_operations(transaction, &sheet_rect, None); self.check_all_spills(transaction, sheet_rect.sheet_id, true); } @@ -220,7 +220,7 @@ impl GridController { if let GridBounds::NonEmpty(bounds) = sheet.bounds(true) { let mut sheet_rect = bounds.to_sheet_rect(sheet_id); sheet_rect.min.x = column + 1; - self.check_deleted_code_runs(transaction, &sheet_rect); + self.check_deleted_data_tables(transaction, &sheet_rect); self.add_compute_operations(transaction, &sheet_rect, None); self.check_all_spills(transaction, sheet_rect.sheet_id, true); } @@ -257,7 +257,7 @@ impl GridController { if let GridBounds::NonEmpty(bounds) = sheet.bounds(true) { let mut sheet_rect = bounds.to_sheet_rect(sheet_id); sheet_rect.min.y = row + 1; - self.check_deleted_code_runs(transaction, &sheet_rect); + self.check_deleted_data_tables(transaction, &sheet_rect); self.add_compute_operations(transaction, &sheet_rect, None); self.check_all_spills(transaction, sheet_rect.sheet_id, true); } diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs index d86fdf8176..0c10cb96d2 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs @@ -67,7 +67,7 @@ impl GridController { // RenderSize is always sent as a 1,1 rect. TODO: we need to refactor formats to make it less generic. if let Some(sheet) = self.grid.try_sheet(sheet_rect.sheet_id) { if let Some(code_run) = - sheet.code_run((sheet_rect.min.x, sheet_rect.min.y).into()) + sheet.data_table((sheet_rect.min.x, sheet_rect.min.y).into()) { if code_run.is_html() { self.send_html_output_rect(&sheet_rect); @@ -197,7 +197,7 @@ mod test { code: "code".to_string(), }), ); - sheet.set_code_run( + sheet.set_data_table( Pos { x: 0, y: 0 }, Some(CodeRun { formatted_code_string: None, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_values.rs b/quadratic-core/src/controller/execution/execute_operation/execute_values.rs index 8600bab848..7dcd62cd7d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_values.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_values.rs @@ -50,7 +50,7 @@ impl GridController { .push(Operation::SetCellValues { sheet_pos, values }); if transaction.is_user() { - self.check_deleted_code_runs(transaction, &sheet_rect); + self.check_deleted_data_tables(transaction, &sheet_rect); self.add_compute_operations(transaction, &sheet_rect, None); self.check_all_spills(transaction, sheet_rect.sheet_id, true); } diff --git a/quadratic-core/src/controller/execution/run_code/get_cells.rs b/quadratic-core/src/controller/execution/run_code/get_cells.rs index 12693a3888..597a3e69ef 100644 --- a/quadratic-core/src/controller/execution/run_code/get_cells.rs +++ b/quadratic-core/src/controller/execution/run_code/get_cells.rs @@ -180,7 +180,7 @@ mod test { assert!(result.is_err()); let sheet = gc.sheet(sheet_id); let error = sheet - .code_run(Pos { x: 0, y: 0 }) + .data_table(Pos { x: 0, y: 0 }) .unwrap() .clone() .std_err diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 1d509dca73..b7681289f2 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -198,7 +198,7 @@ impl GridController { let result = CodeRunResult::Err(error.clone()); - let new_code_run = match sheet.code_run(pos) { + let new_code_run = match sheet.data_table(pos) { Some(old_code_run) => { CodeRun { formatted_code_string: old_code_run.formatted_code_string.clone(), @@ -375,7 +375,7 @@ mod test { assert_eq!(transaction.forward_operations.len(), 1); assert_eq!(transaction.reverse_operations.len(), 1); let sheet = gc.try_sheet(sheet_id).unwrap(); - assert_eq!(sheet.code_run(sheet_pos.into()), Some(&new_code_run)); + assert_eq!(sheet.data_table(sheet_pos.into()), Some(&new_code_run)); // todo: need a way to test the js functions as that replaced these // let summary = transaction.send_transaction(true); @@ -404,7 +404,7 @@ mod test { assert_eq!(transaction.forward_operations.len(), 1); assert_eq!(transaction.reverse_operations.len(), 1); let sheet = gc.try_sheet(sheet_id).unwrap(); - assert_eq!(sheet.code_run(sheet_pos.into()), Some(&new_code_run)); + assert_eq!(sheet.data_table(sheet_pos.into()), Some(&new_code_run)); // todo: need a way to test the js functions as that replaced these // let summary = transaction.send_transaction(true); @@ -418,7 +418,7 @@ mod test { assert_eq!(transaction.forward_operations.len(), 1); assert_eq!(transaction.reverse_operations.len(), 1); let sheet = gc.try_sheet(sheet_id).unwrap(); - assert_eq!(sheet.code_run(sheet_pos.into()), None); + assert_eq!(sheet.data_table(sheet_pos.into()), None); // todo: need a way to test the js functions as that replaced these // let summary = transaction.send_transaction(true); diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index e775b0b4c6..8f374b90b0 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -371,7 +371,7 @@ mod test { ); assert!( gc.sheet(sheet_id) - .code_run(Pos { x: 1, y: 0 }) + .data_table(Pos { x: 1, y: 0 }) .unwrap() .spill_error ); @@ -392,7 +392,7 @@ mod test { gc.redo(None); assert!( gc.sheet(sheet_id) - .code_run(Pos { x: 1, y: 0 }) + .data_table(Pos { x: 1, y: 0 }) .unwrap() .spill_error ); @@ -431,7 +431,7 @@ mod test { code: "this shouldn't work".into(), })) ); - let result = sheet.code_run(pos).unwrap(); + let result = sheet.data_table(pos).unwrap(); assert!(!result.spill_error); assert!(result.std_err.is_some()); @@ -449,7 +449,7 @@ mod test { code: "{0,1/0;2/0,0}".into(), })) ); - let result = sheet.code_run(pos).unwrap(); + let result = sheet.data_table(pos).unwrap(); assert!(!result.spill_error); assert!(result.std_err.is_some()); } diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index 3b0c1c83a9..c3263e25fa 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -58,16 +58,16 @@ impl GridController { } } - /// Checks if a code_cell has a spill error by comparing its output to both CellValues in that range, and earlier code_runs output. + /// Checks if a code_cell has a spill error by comparing its output to both CellValues in that range, and earlier data_tables output. fn check_spill(&self, sheet_id: SheetId, index: usize) -> Option { if let Some(sheet) = self.grid.try_sheet(sheet_id) { - if let Some((pos, code_run)) = sheet.data_tables.get_index(index) { + if let Some((pos, data_table)) = sheet.data_tables.get_index(index) { // output sizes of 1x1 cannot spill - if matches!(code_run.output_size(), ArraySize::_1X1) { + if matches!(data_table.output_size(), ArraySize::_1X1) { return None; } - let output: Rect = code_run + let output: Rect = data_table .output_sheet_rect(pos.to_sheet_pos(sheet_id), true) .into(); @@ -76,10 +76,10 @@ impl GridController { || sheet.has_code_cell_in_rect(&output, *pos) { // if spill error has not been set, then set it and start the more expensive checks for all later code_cells. - if !code_run.spill_error { + if !data_table.spill_error { return Some(true); } - } else if code_run.spill_error { + } else if data_table.spill_error { // release the code_cell's spill error, then start the more expensive checks for all later code_cells. return Some(false); } @@ -88,7 +88,7 @@ impl GridController { None } - /// Checks all code_runs for changes in spill_errors. + /// Checks all data_tables for changes in spill_errors. pub fn check_all_spills( &mut self, transaction: &mut PendingTransaction, @@ -266,7 +266,7 @@ mod tests { gc.check_all_spills(transaction, sheet_id, false); let sheet = gc.sheet(sheet_id); - let code_run = sheet.code_run(Pos { x: 0, y: 0 }).unwrap(); + let code_run = sheet.data_table(Pos { x: 0, y: 0 }).unwrap(); assert!(code_run.spill_error); // should be a spill caused by 0,1 @@ -285,7 +285,7 @@ mod tests { ); let sheet = gc.try_sheet(sheet_id).unwrap(); - let code_run = sheet.code_run(Pos { x: 0, y: 0 }); + let code_run = sheet.data_table(Pos { x: 0, y: 0 }); assert!(code_run.is_some()); assert!(!code_run.unwrap().spill_error); @@ -423,7 +423,7 @@ mod tests { #[test] #[parallel] - fn test_check_deleted_code_runs() { + fn test_check_deleted_data_tables() { let mut gc = GridController::default(); let sheet_id = gc.sheet_ids()[0]; let code_run = CodeRun { @@ -440,6 +440,6 @@ mod tests { }; let pos = Pos { x: 0, y: 0 }; let sheet = gc.sheet_mut(sheet_id); - sheet.set_code_run(pos, Some(code_run.clone())); + sheet.set_data_table(pos, Some(code_run.clone())); } } diff --git a/quadratic-core/src/controller/send_render.rs b/quadratic-core/src/controller/send_render.rs index 7571f6228b..b51a805e46 100644 --- a/quadratic-core/src/controller/send_render.rs +++ b/quadratic-core/src/controller/send_render.rs @@ -218,7 +218,7 @@ impl GridController { pub fn send_image(&self, sheet_pos: SheetPos) { if cfg!(target_family = "wasm") || cfg!(test) { if let Some(sheet) = self.try_sheet(sheet_pos.sheet_id) { - let image = sheet.code_run(sheet_pos.into()).and_then(|code_run| { + let image = sheet.data_table(sheet_pos.into()).and_then(|code_run| { code_run .cell_value_at(0, 0) .and_then(|cell_value| match cell_value { diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index 8011cac1b5..e198dbf06a 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -326,7 +326,7 @@ mod tests { // all code cells should have valid function names, // valid functions may not be implemented yet - let code_run = sheet.code_run(pos).unwrap(); + let code_run = sheet.data_table(pos).unwrap(); if let CodeRunResult::Err(error) = &code_run.result { if error.msg == RunErrorMsg::BadFunctionName { panic!("expected valid function name") diff --git a/quadratic-core/src/grid/code_run.rs b/quadratic-core/src/grid/code_run.rs index 80ab9321d6..e6b81c69cb 100644 --- a/quadratic-core/src/grid/code_run.rs +++ b/quadratic-core/src/grid/code_run.rs @@ -1,6 +1,6 @@ //! CodeRun is the output of a CellValue::Code type //! -//! This lives in sheet.code_runs. CodeRun is optional within sheet.code_runs for +//! This lives in sheet.data_tables. CodeRun is optional within sheet.data_tables for //! any given CellValue::Code type (ie, if it doesn't exist then a run hasn't been //! performed yet). diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 2b6b0b61ab..1ac98aac6f 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -203,14 +203,14 @@ impl Sheet { self.columns.iter() } - /// Returns the cell_value at a Pos using both column.values and code_runs (i.e., what would be returned if code asked + /// Returns the cell_value at a Pos using both column.values and data_tables (i.e., what would be returned if code asked /// for it). pub fn display_value(&self, pos: Pos) -> Option { let cell_value = self .get_column(pos.x) .and_then(|column| column.values.get(&pos.y)); - // if CellValue::Code, then we need to get the value from code_runs + // if CellValue::Code, then we need to get the value from data_tables if let Some(cell_value) = cell_value { match cell_value { CellValue::Code(_) => self @@ -221,7 +221,7 @@ impl Sheet { _ => Some(cell_value.clone()), } } else { - // if there is no CellValue at Pos, then we still need to check code_runs + // if there is no CellValue at Pos, then we still need to check data_tables self.get_code_cell_value(pos) } } @@ -234,7 +234,7 @@ impl Sheet { }) } - /// Returns the cell_value at the Pos in column.values. This does not check or return results within code_runs. + /// Returns the cell_value at the Pos in column.values. This does not check or return results within data_tables. pub fn cell_value(&self, pos: Pos) -> Option { let column = self.get_column(pos.x)?; column.values.get(&pos.y).cloned() @@ -246,7 +246,7 @@ impl Sheet { } /// Returns the cell value at a position using both `column.values` and - /// `code_runs`, for use when a formula references a cell. + /// `data_tables`, for use when a formula references a cell. pub fn get_cell_for_formula(&self, pos: Pos) -> CellValue { let cell_value = self .get_column(pos.x) diff --git a/quadratic-core/src/grid/sheet/cell_array.rs b/quadratic-core/src/grid/sheet/cell_array.rs index d991730c68..0a88aa302c 100644 --- a/quadratic-core/src/grid/sheet/cell_array.rs +++ b/quadratic-core/src/grid/sheet/cell_array.rs @@ -283,7 +283,7 @@ mod tests { None, ); let sheet = gc.sheet(sheet_id); - let run = sheet.code_run(Pos { x: 0, y: 0 }).unwrap(); + let run = sheet.data_table(Pos { x: 0, y: 0 }).unwrap(); assert!(run.spill_error); let reasons = sheet.find_spill_error_reasons( &run.output_rect(Pos { x: 0, y: 0 }, true), diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 5e0d1e008e..318159df6b 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -14,7 +14,7 @@ impl Sheet { /// Sets or deletes a code run. /// /// Returns the old value if it was set. - pub fn set_code_run(&mut self, pos: Pos, code_run: Option) -> Option { + pub fn set_data_table(&mut self, pos: Pos, code_run: Option) -> Option { if let Some(code_run) = code_run { self.data_tables.insert(pos, code_run) } else { @@ -22,12 +22,12 @@ impl Sheet { } } - /// Returns a CodeCell at a Pos - pub fn code_run(&self, pos: Pos) -> Option<&CodeRun> { + /// Returns a DatatTable at a Pos + pub fn data_table(&self, pos: Pos) -> Option<&CodeRun> { self.data_tables.get(&pos) } - /// Gets column bounds for code_runs that output to the columns + /// Gets column bounds for data_tables that output to the columns pub fn code_columns_bounds(&self, column_start: i64, column_end: i64) -> Option> { let mut min: Option = None; let mut max: Option = None; @@ -49,7 +49,7 @@ impl Sheet { } } - /// Gets the row bounds for code_runs that output to the rows + /// Gets the row bounds for data_tables that output to the rows pub fn code_rows_bounds(&self, row_start: i64, row_end: i64) -> Option> { let mut min: Option = None; let mut max: Option = None; @@ -75,16 +75,18 @@ impl Sheet { /// /// Note: spill error will return a CellValue::Blank to ensure calculations can continue. pub fn get_code_cell_value(&self, pos: Pos) -> Option { - self.data_tables.iter().find_map(|(code_cell_pos, code_run)| { - if code_run.output_rect(*code_cell_pos, false).contains(pos) { - code_run.cell_value_at( - (pos.x - code_cell_pos.x) as u32, - (pos.y - code_cell_pos.y) as u32, - ) - } else { - None - } - }) + self.data_tables + .iter() + .find_map(|(code_cell_pos, code_run)| { + if code_run.output_rect(*code_cell_pos, false).contains(pos) { + code_run.cell_value_at( + (pos.x - code_cell_pos.x) as u32, + (pos.y - code_cell_pos.y) as u32, + ) + } else { + None + } + }) } pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { @@ -105,7 +107,7 @@ impl Sheet { } /// Returns whether a rect overlaps the output of a code cell. - /// It will only check code_cells until it finds the code_run at code_pos (since later code_runs do not cause spills in earlier ones) + /// It will only check code_cells until it finds the code_run at code_pos (since later data_tables do not cause spills in earlier ones) pub fn has_code_cell_in_rect(&self, rect: &Rect, code_pos: Pos) -> bool { for (pos, code_run) in &self.data_tables { if pos == &code_pos { @@ -126,18 +128,20 @@ impl Sheet { let code_cell = if let Some(cell_value) = self.cell_value(pos) { Some(cell_value) } else { - self.data_tables.iter().find_map(|(code_cell_pos, code_run)| { - if code_run.output_rect(*code_cell_pos, false).contains(pos) { - if let Some(code_value) = self.cell_value(*code_cell_pos) { - code_pos = *code_cell_pos; - Some(code_value) + self.data_tables + .iter() + .find_map(|(code_cell_pos, code_run)| { + if code_run.output_rect(*code_cell_pos, false).contains(pos) { + if let Some(code_value) = self.cell_value(*code_cell_pos) { + code_pos = *code_cell_pos; + Some(code_value) + } else { + None + } } else { None } - } else { - None - } - }) + }) }; let code_cell = code_cell?; @@ -150,7 +154,7 @@ impl Sheet { code_cell.code = replaced; } - if let Some(code_run) = self.code_run(code_pos) { + if let Some(code_run) = self.data_table(code_pos) { let evaluation_result = serde_json::to_string(&code_run.result).unwrap_or("".into()); let spill_error = if code_run.spill_error { @@ -259,11 +263,11 @@ mod test { output_type: None, spill_error: false, }; - let old = sheet.set_code_run(Pos { x: 0, y: 0 }, Some(code_run.clone())); + let old = sheet.set_data_table(Pos { x: 0, y: 0 }, Some(code_run.clone())); assert_eq!(old, None); - assert_eq!(sheet.code_run(Pos { x: 0, y: 0 }), Some(&code_run)); - assert_eq!(sheet.code_run(Pos { x: 0, y: 0 }), Some(&code_run)); - assert_eq!(sheet.code_run(Pos { x: 1, y: 0 }), None); + assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&code_run)); + assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&code_run)); + assert_eq!(sheet.data_table(Pos { x: 1, y: 0 }), None); } #[test] @@ -284,13 +288,13 @@ mod test { spill_error: false, last_modified: Utc::now(), }; - sheet.set_code_run(Pos { x: 0, y: 0 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 0, y: 0 }, Some(code_run.clone())); assert_eq!( sheet.get_code_cell_value(Pos { x: 0, y: 0 }), Some(CellValue::Number(BigDecimal::from(2))) ); - assert_eq!(sheet.code_run(Pos { x: 0, y: 0 }), Some(&code_run)); - assert_eq!(sheet.code_run(Pos { x: 1, y: 1 }), None); + assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&code_run)); + assert_eq!(sheet.data_table(Pos { x: 1, y: 1 }), None); } #[test] @@ -318,7 +322,7 @@ mod test { spill_error: false, last_modified: Utc::now(), }; - sheet.set_code_run(Pos { x: 0, y: 0 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 0, y: 0 }, Some(code_run.clone())); assert_eq!( sheet.edit_code_value(Pos { x: 0, y: 0 }), Some(JsCodeCell { @@ -409,9 +413,9 @@ mod test { spill_error: false, last_modified: Utc::now(), }; - sheet.set_code_run(Pos { x: 0, y: 0 }, Some(code_run.clone())); - sheet.set_code_run(Pos { x: 1, y: 1 }, Some(code_run.clone())); - sheet.set_code_run(Pos { x: 2, y: 3 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 0, y: 0 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 1, y: 1 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 2, y: 3 }, Some(code_run.clone())); assert_eq!(sheet.code_columns_bounds(0, 0), Some(0..3)); assert_eq!(sheet.code_columns_bounds(1, 1), Some(1..4)); @@ -440,9 +444,9 @@ mod test { spill_error: false, last_modified: Utc::now(), }; - sheet.set_code_run(Pos { x: 0, y: 0 }, Some(code_run.clone())); - sheet.set_code_run(Pos { x: 1, y: 1 }, Some(code_run.clone())); - sheet.set_code_run(Pos { x: 3, y: 2 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 0, y: 0 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 1, y: 1 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 3, y: 2 }, Some(code_run.clone())); assert_eq!(sheet.code_rows_bounds(0, 0), Some(0..3)); assert_eq!(sheet.code_rows_bounds(1, 1), Some(1..4)); diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 8e05d97f4d..34abe77e07 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -645,7 +645,7 @@ mod tests { code: "1 + 1".to_string(), }), ); - sheet.set_code_run( + sheet.set_data_table( Pos { x: 2, y: 3 }, Some(CodeRun { formatted_code_string: None, @@ -1043,7 +1043,7 @@ mod tests { line_number: None, output_type: None, }; - sheet.set_code_run(pos, Some(run)); + sheet.set_data_table(pos, Some(run)); sheet.set_cell_value(pos, code); let rendering = sheet.get_render_code_cell(pos); assert_eq!( @@ -1088,7 +1088,7 @@ mod tests { line_number: None, output_type: None, }; - sheet.set_code_run(pos, Some(run)); + sheet.set_data_table(pos, Some(run)); sheet.set_cell_value(pos, code); sheet.send_all_images(); expect_js_call( diff --git a/quadratic-core/src/grid/sheet/search.rs b/quadratic-core/src/grid/sheet/search.rs index 7c90251766..bbc15b3f67 100644 --- a/quadratic-core/src/grid/sheet/search.rs +++ b/quadratic-core/src/grid/sheet/search.rs @@ -107,7 +107,7 @@ impl Sheet { .collect::>() } - fn search_code_runs( + fn search_data_tables( &self, query: &String, case_sensitive: bool, @@ -116,7 +116,7 @@ impl Sheet { let mut results = vec![]; self.data_tables .iter() - .for_each(|(pos, code_run)| match &code_run.result { + .for_each(|(pos, data_table)| match &data_table.result { CodeRunResult::Ok(value) => match value { Value::Single(v) => { if self.compare_cell_value( @@ -125,7 +125,7 @@ impl Sheet { *pos, case_sensitive, whole_cell, - false, // code_runs can never have code within them (although that would be cool if they did ;) + false, // data_tables can never have code within them (although that would be cool if they did ;) ) { results.push(pos.to_sheet_pos(self.id)); } @@ -143,7 +143,7 @@ impl Sheet { }, case_sensitive, whole_cell, - false, // code_runs can never have code within them (although that would be cool if they did ;) + false, // data_tables can never have code within them (although that would be cool if they did ;) ) { results.push(SheetPos { x: pos.x + x as i64, @@ -175,7 +175,7 @@ impl Sheet { let whole_cell = options.whole_cell.unwrap_or(false); let search_code = options.search_code.unwrap_or(false); let mut results = self.search_cell_values(&query, case_sensitive, whole_cell, search_code); - results.extend(self.search_code_runs(&query, case_sensitive, whole_cell)); + results.extend(self.search_data_tables(&query, case_sensitive, whole_cell)); results.sort_by(|a, b| { let order = a.x.cmp(&b.x); if order == std::cmp::Ordering::Equal { @@ -473,7 +473,7 @@ mod test { output_type: None, last_modified: Utc::now(), }; - sheet.set_code_run(Pos { x: 1, y: 2 }, Some(code_run)); + sheet.set_data_table(Pos { x: 1, y: 2 }, Some(code_run)); let results = sheet.search( &"hello".into(), @@ -515,7 +515,7 @@ mod test { output_type: None, last_modified: Utc::now(), }; - sheet.set_code_run(Pos { x: 1, y: 2 }, Some(code_run)); + sheet.set_data_table(Pos { x: 1, y: 2 }, Some(code_run)); let results = sheet.search( &"abc".into(), diff --git a/quadratic-core/src/grid/sheet/sheet_test.rs b/quadratic-core/src/grid/sheet/sheet_test.rs index eb094cb30a..00c50f84a7 100644 --- a/quadratic-core/src/grid/sheet/sheet_test.rs +++ b/quadratic-core/src/grid/sheet/sheet_test.rs @@ -60,7 +60,7 @@ impl Sheet { }), ); - self.set_code_run( + self.set_data_table( crate::Pos { x, y }, Some(crate::grid::CodeRun { std_out: None, @@ -127,7 +127,7 @@ impl Sheet { code: "code".to_string(), }), ); - self.set_code_run( + self.set_data_table( Pos { x, y }, Some(CodeRun { std_out: None, @@ -175,7 +175,7 @@ impl Sheet { } } - self.set_code_run( + self.set_data_table( Pos { x, y }, Some(CodeRun { std_out: None, diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index 1f1938484a..f66f612573 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -261,8 +261,8 @@ pub fn print_table_sheet(sheet: &Sheet, rect: Rect) { println!("\nsheet: {}\n{}", sheet.id, table); } -/// Prints the order of the code_runs to the console. -pub fn print_code_run_order(sheet: &Sheet) { +/// Prints the order of the data_tables to the console. +pub fn print_data_table_order(sheet: &Sheet) { dbgjs!(sheet .data_tables .iter() From d4c5123982330ca00c5f55ade6debed79ff402da Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 25 Sep 2024 16:47:57 -0600 Subject: [PATCH 003/373] Convert code_runs to data_tables --- .../pending_transaction.rs | 24 +- quadratic-core/src/controller/dependencies.rs | 28 +- .../execution/control_transaction.rs | 32 +- .../execute_operation/execute_col_rows.rs | 2 +- .../execute_operation/execute_formats.rs | 22 +- .../execution/run_code/get_cells.rs | 2 + .../src/controller/execution/run_code/mod.rs | 117 ++++--- .../execution/run_code/run_formula.rs | 66 ++-- .../src/controller/execution/spills.rs | 14 +- .../src/controller/operations/code_cell.rs | 17 +- .../src/controller/operations/operation.rs | 4 +- .../src/controller/user_actions/import.rs | 6 +- .../src/formulas/functions/mathematics.rs | 4 +- quadratic-core/src/grid/code_run.rs | 255 ++------------ quadratic-core/src/grid/data_table.rs | 251 ++++++++++++++ quadratic-core/src/grid/file/mod.rs | 8 +- .../src/grid/file/serialize/code_cell.rs | 121 ------- .../src/grid/file/serialize/data_table.rs | 327 ++++++++++++++++++ quadratic-core/src/grid/file/serialize/mod.rs | 2 +- .../src/grid/file/serialize/sheets.rs | 6 +- quadratic-core/src/grid/file/v1_4/schema.rs | 9 + quadratic-core/src/grid/file/v1_6/file.rs | 142 +++++++- quadratic-core/src/grid/file/v1_6/schema.rs | 2 + quadratic-core/src/grid/file/v1_7/schema.rs | 50 ++- quadratic-core/src/grid/mod.rs | 2 + quadratic-core/src/grid/sheet.rs | 13 +- quadratic-core/src/grid/sheet/code.rs | 118 ++++--- quadratic-core/src/grid/sheet/rendering.rs | 142 ++++---- quadratic-core/src/grid/sheet/search.rs | 103 +++--- quadratic-core/src/grid/sheet/selection.rs | 60 ++-- quadratic-core/src/grid/sheet/sheet_test.rs | 126 +++---- quadratic-core/src/values/array_size.rs | 26 ++ 32 files changed, 1353 insertions(+), 748 deletions(-) create mode 100644 quadratic-core/src/grid/data_table.rs delete mode 100644 quadratic-core/src/grid/file/serialize/code_cell.rs create mode 100644 quadratic-core/src/grid/file/serialize/data_table.rs diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index 5d97ba49bb..c9e6fa93ee 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -12,7 +12,7 @@ use crate::{ controller::{ execution::TransactionType, operations::operation::Operation, transaction::Transaction, }, - grid::{sheet::validations::validation::Validation, CodeCellLanguage, CodeRun, SheetId}, + grid::{sheet::validations::validation::Validation, CodeCellLanguage, DataTable, SheetId}, selection::Selection, viewport::ViewportBuffer, Pos, SheetPos, SheetRect, @@ -204,7 +204,7 @@ impl PendingTransaction { } /// Adds a code cell, html cell and image cell to the transaction from a CodeRun - pub fn add_from_code_run(&mut self, sheet_id: SheetId, pos: Pos, code_run: &Option) { + pub fn add_from_code_run(&mut self, sheet_id: SheetId, pos: Pos, code_run: &Option) { if let Some(code_run) = &code_run { self.add_code_cell(sheet_id, pos); if code_run.is_html() { @@ -263,7 +263,7 @@ impl PendingTransaction { mod tests { use crate::{ controller::operations::operation::Operation, - grid::{CodeRunResult, SheetId}, + grid::{CodeRun, DataTableKind, SheetId}, CellValue, Value, }; @@ -352,14 +352,19 @@ mod tests { std_err: None, formatted_code_string: None, cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Single(CellValue::Html("html".to_string()))), + error: None, return_type: None, line_number: None, output_type: None, + }; + + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Html("html".to_string())), spill_error: false, last_modified: Utc::now(), }; - transaction.add_from_code_run(sheet_id, pos, &Some(code_run)); + transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); assert_eq!(transaction.html_cells.len(), 1); assert_eq!(transaction.image_cells.len(), 0); @@ -369,14 +374,19 @@ mod tests { std_err: None, formatted_code_string: None, cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Single(CellValue::Image("image".to_string()))), + error: None, return_type: None, line_number: None, output_type: None, + }; + + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Image("image".to_string())), spill_error: false, last_modified: Utc::now(), }; - transaction.add_from_code_run(sheet_id, pos, &Some(code_run)); + transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); assert_eq!(transaction.html_cells.len(), 1); assert_eq!(transaction.image_cells.len(), 1); diff --git a/quadratic-core/src/controller/dependencies.rs b/quadratic-core/src/controller/dependencies.rs index d15f00a17e..9a57921a9d 100644 --- a/quadratic-core/src/controller/dependencies.rs +++ b/quadratic-core/src/controller/dependencies.rs @@ -12,7 +12,7 @@ impl GridController { let mut dependent_cells = HashSet::new(); self.grid.sheets().iter().for_each(|sheet| { - sheet.data_tables.iter().for_each(|(pos, code_run)| { + sheet.iter_code_runs().for_each(|(pos, code_run)| { code_run.cells_accessed.iter().for_each(|cell_accessed| { if sheet_rect.intersects(*cell_accessed) { dependent_cells.insert(pos.to_sheet_pos(sheet.id)); @@ -37,7 +37,7 @@ mod test { use crate::{ controller::GridController, - grid::{CodeCellLanguage, CodeRun, CodeRunResult}, + grid::{CodeCellLanguage, CodeRun, DataTable, DataTableKind}, CellValue, Pos, SheetPos, SheetRect, Value, }; use serial_test::parallel; @@ -67,19 +67,23 @@ mod test { sheet_id, }; cells_accessed.insert(sheet_rect); + let code_run = CodeRun { + formatted_code_string: None, + std_err: None, + std_out: None, + error: None, + return_type: Some("text".into()), + line_number: None, + output_type: None, + cells_accessed: cells_accessed.clone(), + }; sheet.set_data_table( Pos { x: 0, y: 2 }, - Some(CodeRun { - formatted_code_string: None, - last_modified: Utc::now(), - std_err: None, - std_out: None, + Some(DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Text("test".to_string())), spill_error: false, - result: CodeRunResult::Ok(Value::Single(CellValue::Text("test".to_string()))), - return_type: Some("text".into()), - line_number: None, - output_type: None, - cells_accessed: cells_accessed.clone(), + last_modified: Utc::now(), }), ); let sheet_pos_02 = SheetPos { diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index 565a00a7e2..848c52f2f7 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -9,7 +9,7 @@ use crate::controller::transaction::Transaction; use crate::controller::transaction_types::JsCodeResult; use crate::error_core::Result; use crate::grid::js_types::JsHtmlOutput; -use crate::grid::{CodeRun, CodeRunResult}; +use crate::grid::{CodeRun, DataTable, DataTableKind}; use crate::parquet::parquet_to_vec; use crate::renderer_constants::{CELL_SHEET_HEIGHT, CELL_SHEET_WIDTH}; use crate::{Pos, RunError, RunErrorMsg, Value}; @@ -250,27 +250,33 @@ impl GridController { return_type = format!("{return_type}\n{extra}"); } - let result = if let Some(error_msg) = &std_err { - let msg = RunErrorMsg::PythonError(error_msg.clone().into()); - CodeRunResult::Err(RunError { span: None, msg }) - } else { - CodeRunResult::Ok(Value::Array(array.into())) - }; - + let error = std_err.to_owned().map(|msg| RunError { + span: None, + msg: RunErrorMsg::PythonError(msg.into()), + }); let code_run = CodeRun { formatted_code_string: None, - result, - return_type: Some(return_type.clone()), + error, + return_type: Some(return_type.to_owned()), line_number: Some(1), output_type: Some(return_type), std_out, - std_err, + std_err: std_err.to_owned(), + cells_accessed: transaction.cells_accessed.to_owned(), + }; + let value = if std_err.is_some() { + Value::default() // TODO(ddimaria): this will be an empty vec + } else { + Value::Array(array.into()) + }; + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value, spill_error: false, last_modified: Utc::now(), - cells_accessed: transaction.cells_accessed.clone(), }; - self.finalize_code_run(&mut transaction, current_sheet_pos, Some(code_run), None); + self.finalize_code_run(&mut transaction, current_sheet_pos, Some(data_table), None); transaction.waiting_for_async = None; self.start_transaction(&mut transaction); self.finalize_transaction(transaction); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs b/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs index 2b564d6d2b..11a2c780e9 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs @@ -19,7 +19,7 @@ impl GridController { delta: i64, ) { self.grid.sheets().iter().for_each(|sheet| { - sheet.data_tables.iter().for_each(|(pos, code_run)| { + sheet.iter_code_runs().for_each(|(pos, code_run)| { if let Some(column) = column { if code_run.cells_accessed.iter().any(|sheet_rect| { // if the cells accessed is beyond the column that was deleted diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs index 0c10cb96d2..9f74d02860 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs @@ -197,18 +197,22 @@ mod test { code: "code".to_string(), }), ); + let code_run = CodeRun { + formatted_code_string: None, + output_type: None, + std_err: None, + std_out: None, + error: None, + cells_accessed: HashSet::new(), + return_type: None, + line_number: None, + }; sheet.set_data_table( Pos { x: 0, y: 0 }, - Some(CodeRun { - formatted_code_string: None, + Some(DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Image("image".to_string())), spill_error: false, - output_type: None, - std_err: None, - std_out: None, - result: CodeRunResult::Ok(Value::Single(CellValue::Image("image".to_string()))), - cells_accessed: HashSet::new(), - return_type: None, - line_number: None, last_modified: Utc::now(), }), ); diff --git a/quadratic-core/src/controller/execution/run_code/get_cells.rs b/quadratic-core/src/controller/execution/run_code/get_cells.rs index 597a3e69ef..0d3926d6e2 100644 --- a/quadratic-core/src/controller/execution/run_code/get_cells.rs +++ b/quadratic-core/src/controller/execution/run_code/get_cells.rs @@ -182,6 +182,8 @@ mod test { let error = sheet .data_table(Pos { x: 0, y: 0 }) .unwrap() + .code_run() + .unwrap() .clone() .std_err .unwrap(); diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index b7681289f2..bb6a99c649 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -5,7 +5,7 @@ use crate::controller::operations::operation::Operation; use crate::controller::transaction_types::JsCodeResult; use crate::controller::GridController; use crate::error_core::{CoreError, Result}; -use crate::grid::{CodeCellLanguage, CodeRun, CodeRunResult}; +use crate::grid::{CodeCellLanguage, CodeRun, DataTable, DataTableKind}; use crate::{Array, CellValue, Pos, RunError, RunErrorMsg, SheetPos, SheetRect, Span, Value}; pub mod get_cells; @@ -20,7 +20,7 @@ impl GridController { &mut self, transaction: &mut PendingTransaction, sheet_pos: SheetPos, - new_code_run: Option, + new_data_table: Option, index: Option, ) { let sheet_id = sheet_pos.sheet_id; @@ -39,9 +39,9 @@ impl GridController { .unwrap_or(sheet.data_tables.len()), ); - let old_code_run = if let Some(new_code_run) = &new_code_run { - let (old_index, old_code_run) = - sheet.data_tables.insert_full(pos, new_code_run.clone()); + let old_data_table = if let Some(new_data_table) = &new_data_table { + let (old_index, old_data_table) = + sheet.data_tables.insert_full(pos, new_data_table.clone()); // keep the orderings of the code runs consistent, particularly when undoing/redoing let index = if index > sheet.data_tables.len() - 1 { sheet.data_tables.len() - 1 @@ -49,16 +49,16 @@ impl GridController { index }; sheet.data_tables.move_index(old_index, index); - old_code_run + old_data_table } else { sheet.data_tables.shift_remove(&pos) }; - if old_code_run == new_code_run { + if old_data_table == new_data_table { return; } - let sheet_rect = match (&old_code_run, &new_code_run) { + let sheet_rect = match (&old_data_table, &new_data_table) { (None, None) => sheet_pos.into(), (None, Some(code_cell_value)) => code_cell_value.output_sheet_rect(sheet_pos, false), (Some(old_code_cell_value), None) => { @@ -79,8 +79,8 @@ impl GridController { }; if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() { - transaction.add_from_code_run(sheet_id, pos, &old_code_run); - transaction.add_from_code_run(sheet_id, pos, &new_code_run); + transaction.add_from_code_run(sheet_id, pos, &old_data_table); + transaction.add_from_code_run(sheet_id, pos, &new_data_table); self.send_updated_bounds_rect(&sheet_rect, false); @@ -100,13 +100,13 @@ impl GridController { if transaction.is_user_undo_redo() { transaction.forward_operations.push(Operation::SetCodeRun { sheet_pos, - code_run: new_code_run, + code_run: new_data_table, index, }); transaction.reverse_operations.push(Operation::SetCodeRun { sheet_pos, - code_run: old_code_run, + code_run: old_data_table, index, }); @@ -139,7 +139,7 @@ impl GridController { } Some(waiting_for_async) => match waiting_for_async { CodeCellLanguage::Python | CodeCellLanguage::Javascript => { - let new_code_run = self.js_code_result_to_code_cell_value( + let new_data_table = self.js_code_result_to_code_cell_value( transaction, result, current_sheet_pos, @@ -149,7 +149,7 @@ impl GridController { self.finalize_code_run( transaction, current_sheet_pos, - Some(new_code_run), + Some(new_data_table), None, ); } @@ -196,20 +196,21 @@ impl GridController { return Ok(()); }; - let result = CodeRunResult::Err(error.clone()); + let code_run = sheet + .data_table(pos) + .map(|data_table| data_table.code_run()) + .flatten(); - let new_code_run = match sheet.data_table(pos) { + let new_code_run = match code_run { Some(old_code_run) => { CodeRun { formatted_code_string: old_code_run.formatted_code_string.clone(), - result, + error: Some(error.to_owned()), return_type: None, line_number: old_code_run.line_number, output_type: old_code_run.output_type.clone(), std_out: None, std_err: Some(error.msg.to_string()), - spill_error: false, - last_modified: Utc::now(), // keep the old cells_accessed to better rerun after an error cells_accessed: old_code_run.cells_accessed.clone(), @@ -217,7 +218,7 @@ impl GridController { } None => CodeRun { formatted_code_string: None, - result, + error: Some(error.to_owned()), return_type: None, line_number: error .span @@ -225,13 +226,17 @@ impl GridController { output_type: None, std_out: None, std_err: Some(error.msg.to_string()), - spill_error: false, - last_modified: Utc::now(), cells_accessed: transaction.cells_accessed.clone(), }, }; + let new_data_table = DataTable { + kind: DataTableKind::CodeRun(new_code_run), + value: Value::Single(CellValue::Blank), + spill_error: false, + last_modified: Utc::now(), + }; transaction.waiting_for_async = None; - self.finalize_code_run(transaction, sheet_pos, Some(new_code_run), None); + self.finalize_code_run(transaction, sheet_pos, Some(new_data_table), None); Ok(()) } @@ -242,13 +247,13 @@ impl GridController { transaction: &mut PendingTransaction, js_code_result: JsCodeResult, start: SheetPos, - ) -> CodeRun { + ) -> DataTable { let Some(sheet) = self.try_sheet_mut(start.sheet_id) else { // todo: this is probably not the best place to handle this // sheet may have been deleted before the async operation completed - return CodeRun { + let code_run = CodeRun { formatted_code_string: None, - result: CodeRunResult::Err(RunError { + error: Some(RunError { span: None, msg: RunErrorMsg::PythonError( "Sheet was deleted before the async operation completed".into(), @@ -259,12 +264,16 @@ impl GridController { output_type: js_code_result.output_display_type, std_out: None, std_err: None, + cells_accessed: transaction.cells_accessed.clone(), + }; + return DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Blank), // TODO(ddimaria): this will eventually be an empty vec spill_error: false, last_modified: Utc::now(), - cells_accessed: transaction.cells_accessed.clone(), }; }; - let result = if js_code_result.success { + let value = if js_code_result.success { let result = if let Some(array_output) = js_code_result.output_array { let (array, ops) = Array::from_string_list(start.into(), sheet, array_output); transaction.reverse_operations.extend(ops); @@ -285,8 +294,12 @@ impl GridController { } else { Value::Single(CellValue::Blank) }; - CodeRunResult::Ok(result) + result } else { + Value::Single(CellValue::Blank) // TODO(ddimaria): this will eventually be an empty vec + }; + + let error = { let error_msg = js_code_result .std_err .clone() @@ -296,30 +309,34 @@ impl GridController { start: line_number, end: line_number, }); - CodeRunResult::Err(RunError { span, msg }) + RunError { span, msg } }; - let return_type = match result { - CodeRunResult::Ok(Value::Single(ref cell_value)) => Some(cell_value.type_name().into()), - CodeRunResult::Ok(Value::Array(_)) => Some("array".into()), - CodeRunResult::Ok(Value::Tuple(_)) => Some("tuple".into()), - CodeRunResult::Err(_) => None, + let return_type = match value { + Value::Single(ref cell_value) => Some(cell_value.type_name().into()), + Value::Array(_) => Some("array".into()), + Value::Tuple(_) => Some("tuple".into()), }; let code_run = CodeRun { formatted_code_string: None, - result, + error: Some(error), return_type, line_number: js_code_result.line_number, output_type: js_code_result.output_display_type, std_out: js_code_result.std_out, std_err: js_code_result.std_err, + cells_accessed: transaction.cells_accessed.clone(), + }; + + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value, spill_error: false, last_modified: Utc::now(), - cells_accessed: transaction.cells_accessed.clone(), }; transaction.cells_accessed.clear(); - code_run + data_table } } @@ -363,19 +380,23 @@ mod test { formatted_code_string: None, std_err: None, std_out: None, - result: CodeRunResult::Ok(Value::Single(CellValue::Text("delete me".to_string()))), + error: None, return_type: Some("text".into()), line_number: None, output_type: None, - last_modified: Utc::now(), cells_accessed: HashSet::new(), + }; + let new_data_table = DataTable { + kind: DataTableKind::CodeRun(new_code_run), + value: Value::Single(CellValue::Text("delete me".to_string())), spill_error: false, + last_modified: Utc::now(), }; - gc.finalize_code_run(transaction, sheet_pos, Some(new_code_run.clone()), None); + gc.finalize_code_run(transaction, sheet_pos, Some(new_data_table.clone()), None); assert_eq!(transaction.forward_operations.len(), 1); assert_eq!(transaction.reverse_operations.len(), 1); let sheet = gc.try_sheet(sheet_id).unwrap(); - assert_eq!(sheet.data_table(sheet_pos.into()), Some(&new_code_run)); + assert_eq!(sheet.data_table(sheet_pos.into()), Some(&new_data_table)); // todo: need a way to test the js functions as that replaced these // let summary = transaction.send_transaction(true); @@ -392,19 +413,23 @@ mod test { formatted_code_string: None, std_err: None, std_out: None, - result: CodeRunResult::Ok(Value::Single(CellValue::Text("replace me".to_string()))), + error: None, return_type: Some("text".into()), line_number: None, output_type: None, - last_modified: Utc::now(), cells_accessed: HashSet::new(), + }; + let new_data_table = DataTable { + kind: DataTableKind::CodeRun(new_code_run), + value: Value::Single(CellValue::Text("replace me".to_string())), spill_error: false, + last_modified: Utc::now(), }; - gc.finalize_code_run(transaction, sheet_pos, Some(new_code_run.clone()), None); + gc.finalize_code_run(transaction, sheet_pos, Some(new_data_table.clone()), None); assert_eq!(transaction.forward_operations.len(), 1); assert_eq!(transaction.reverse_operations.len(), 1); let sheet = gc.try_sheet(sheet_id).unwrap(); - assert_eq!(sheet.data_table(sheet_pos.into()), Some(&new_code_run)); + assert_eq!(sheet.data_table(sheet_pos.into()), Some(&new_data_table)); // todo: need a way to test the js functions as that replaced these // let summary = transaction.send_transaction(true); diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index 8f374b90b0..9e04242986 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -4,7 +4,7 @@ use itertools::Itertools; use crate::{ controller::{active_transactions::pending_transaction::PendingTransaction, GridController}, formulas::{parse_formula, Ctx}, - grid::{CodeRun, CodeRunResult}, + grid::{CodeRun, DataTable, DataTableKind}, SheetPos, }; @@ -27,15 +27,19 @@ impl GridController { std_err: (!errors.is_empty()) .then(|| errors.into_iter().map(|e| e.to_string()).join("\n")), formatted_code_string: None, - spill_error: false, - last_modified: Utc::now(), cells_accessed: transaction.cells_accessed.clone(), - result: CodeRunResult::Ok(output.inner), + error: None, return_type: None, line_number: None, output_type: None, }; - self.finalize_code_run(transaction, sheet_pos, Some(new_code_run), None); + let new_data_table = DataTable { + kind: DataTableKind::CodeRun(new_code_run), + value: output.inner, + spill_error: false, + last_modified: Utc::now(), + }; + self.finalize_code_run(transaction, sheet_pos, Some(new_data_table), None); } Err(error) => { let _ = self.code_cell_sheet_error(transaction, &error); @@ -62,7 +66,7 @@ mod test { transaction_types::JsCodeResult, GridController, }, - grid::{CodeCellLanguage, CodeRun, CodeRunResult}, + grid::{CodeCellLanguage, CodeRun, DataTable, DataTableKind}, Array, ArraySize, CellValue, CodeCellValue, Pos, SheetPos, Value, }; @@ -246,19 +250,23 @@ mod test { // need the result to ensure last_modified is the same let result = gc.js_code_result_to_code_cell_value(&mut transaction, result, sheet_pos); + let code_run = CodeRun { + std_out: None, + std_err: None, + formatted_code_string: None, + cells_accessed: HashSet::new(), + return_type: None, + line_number: None, + output_type: None, + error: None, + }; assert_eq!( result, - CodeRun { - std_out: None, - std_err: None, - formatted_code_string: None, - last_modified: result.last_modified, - result: CodeRunResult::Ok(Value::Single(CellValue::Number(12.into()))), - return_type: Some("number".into()), - line_number: None, - output_type: None, - cells_accessed: HashSet::new(), + DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Number(12.into())), spill_error: false, + last_modified: result.last_modified, }, ); } @@ -311,17 +319,21 @@ mod test { let _ = array.set(1, 1, CellValue::Text("Hello".into())); let result = gc.js_code_result_to_code_cell_value(&mut transaction, result, sheet_pos); + let code_run = CodeRun { + std_out: None, + std_err: None, + formatted_code_string: None, + error: None, + return_type: Some("array".into()), + line_number: None, + output_type: None, + cells_accessed: HashSet::new(), + }; assert_eq!( result, - CodeRun { - std_out: None, - std_err: None, - formatted_code_string: None, - result: CodeRunResult::Ok(Value::Array(array)), - return_type: Some("array".into()), - line_number: None, - output_type: None, - cells_accessed: HashSet::new(), + DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(array), spill_error: false, last_modified: result.last_modified, } @@ -433,7 +445,7 @@ mod test { ); let result = sheet.data_table(pos).unwrap(); assert!(!result.spill_error); - assert!(result.std_err.is_some()); + assert!(result.code_run().unwrap().std_err.is_some()); gc.set_code_cell( sheet_pos, @@ -451,6 +463,6 @@ mod test { ); let result = sheet.data_table(pos).unwrap(); assert!(!result.spill_error); - assert!(result.std_err.is_some()); + assert!(result.code_run().unwrap().std_err.is_some()); } } diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index c3263e25fa..093015f684 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -115,7 +115,7 @@ mod tests { use crate::controller::active_transactions::pending_transaction::PendingTransaction; use crate::controller::GridController; use crate::grid::js_types::{JsNumber, JsRenderCell, JsRenderCellSpecial}; - use crate::grid::{CellAlign, CodeCellLanguage, CodeRun, CodeRunResult}; + use crate::grid::{CellAlign, CodeCellLanguage, CodeRun, DataTable, DataTableKind}; use crate::wasm_bindings::js::{clear_js_calls, expect_js_call_count}; use crate::{Array, CellValue, Pos, Rect, SheetPos, Value}; @@ -429,17 +429,21 @@ mod tests { let code_run = CodeRun { std_err: None, std_out: None, - result: CodeRunResult::Ok(Value::Array(Array::from(vec![vec!["1"]]))), + error: None, return_type: Some("number".into()), line_number: None, output_type: None, - spill_error: false, - last_modified: Utc::now(), cells_accessed: HashSet::new(), formatted_code_string: None, }; + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(Array::from(vec![vec!["1"]])), + spill_error: false, + last_modified: Utc::now(), + }; let pos = Pos { x: 0, y: 0 }; let sheet = gc.sheet_mut(sheet_id); - sheet.set_data_table(pos, Some(code_run.clone())); + sheet.set_data_table(pos, Some(data_table.clone())); } } diff --git a/quadratic-core/src/controller/operations/code_cell.rs b/quadratic-core/src/controller/operations/code_cell.rs index d2d0911804..a53f2abfcf 100644 --- a/quadratic-core/src/controller/operations/code_cell.rs +++ b/quadratic-core/src/controller/operations/code_cell.rs @@ -3,7 +3,7 @@ use crate::{ cell_values::CellValues, controller::GridController, formulas::replace_a1_notation, - grid::{CodeCellLanguage, CodeRun, SheetId}, + grid::{CodeCellLanguage, DataTable, SheetId}, CellValue, CodeCellValue, SheetPos, }; @@ -30,15 +30,20 @@ impl GridController { } // Returns whether a code_cell is dependent on another code_cell. - fn is_dependent_on(&self, current: &CodeRun, other_pos: SheetPos) -> bool { + fn is_dependent_on(&self, current: &DataTable, other_pos: SheetPos) -> bool { current - .cells_accessed - .iter() - .any(|sheet_rect| sheet_rect.contains(other_pos)) + .code_run() + .map(|code_run| { + code_run + .cells_accessed + .iter() + .any(|sheet_rect| sheet_rect.contains(other_pos)) + }) + .unwrap_or(false) } /// Orders code cells to ensure earlier computes do not depend on later computes. - fn order_code_cells(&self, code_cell_positions: &mut Vec<(SheetPos, &CodeRun)>) { + fn order_code_cells(&self, code_cell_positions: &mut Vec<(SheetPos, &DataTable)>) { // Change the ordering of code_cell_positions to ensure earlier operations do not depend on later operations. // // Algorithm: iterate through all code cells and check if they are dependent on later code cells. If they are, diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 4e310631f3..b664913271 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -7,7 +7,7 @@ use crate::{ grid::{ file::sheet_schema::SheetSchema, formats::Formats, formatting::CellFmtArray, js_types::JsRowHeight, sheet::borders::BorderStyleCellUpdates, - sheet::validations::validation::Validation, CodeRun, Sheet, SheetBorders, SheetId, + sheet::validations::validation::Validation, DataTable, Sheet, SheetBorders, SheetId, }, selection::Selection, SheetPos, SheetRect, @@ -26,7 +26,7 @@ pub enum Operation { }, SetCodeRun { sheet_pos: SheetPos, - code_run: Option, + code_run: Option, index: usize, }, ComputeCode { diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index e198dbf06a..b3fd50b32e 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -78,7 +78,7 @@ mod tests { use std::str::FromStr; use crate::{ - grid::{CodeCellLanguage, CodeRunResult}, + grid::CodeCellLanguage, test_util::{assert_cell_value_row, print_table}, wasm_bindings::js::clear_js_calls, CellValue, CodeCellValue, Rect, RunError, RunErrorMsg, Span, @@ -326,8 +326,8 @@ mod tests { // all code cells should have valid function names, // valid functions may not be implemented yet - let code_run = sheet.data_table(pos).unwrap(); - if let CodeRunResult::Err(error) = &code_run.result { + let code_run = sheet.data_table(pos).unwrap().code_run().unwrap(); + if let Some(error) = &code_run.error { if error.msg == RunErrorMsg::BadFunctionName { panic!("expected valid function name") } diff --git a/quadratic-core/src/formulas/functions/mathematics.rs b/quadratic-core/src/formulas/functions/mathematics.rs index 44c4b82e5c..9a8b7c40c7 100644 --- a/quadratic-core/src/formulas/functions/mathematics.rs +++ b/quadratic-core/src/formulas/functions/mathematics.rs @@ -441,8 +441,8 @@ mod tests { // Test mismatched range assert_eq!( RunErrorMsg::ExactArraySizeMismatch { - expected: ArraySize::try_from((1, 12)).unwrap(), - got: ArraySize::try_from((1, 11)).unwrap(), + expected: ArraySize::try_from((1 as i64, 12 as i64)).unwrap(), + got: ArraySize::try_from((1 as i64, 11 as i64)).unwrap(), }, eval_to_err(&g, "SUMIFS(0..10, 0..11, \"<=5\")").msg, ); diff --git a/quadratic-core/src/grid/code_run.rs b/quadratic-core/src/grid/code_run.rs index e6b81c69cb..e0b5c71d45 100644 --- a/quadratic-core/src/grid/code_run.rs +++ b/quadratic-core/src/grid/code_run.rs @@ -4,8 +4,7 @@ //! any given CellValue::Code type (ie, if it doesn't exist then a run hasn't been //! performed yet). -use crate::{ArraySize, CellValue, Pos, Rect, RunError, RunErrorMsg, SheetPos, SheetRect, Value}; -use chrono::{DateTime, Utc}; +use crate::{RunError, SheetRect}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use strum_macros::Display; @@ -15,113 +14,19 @@ use wasm_bindgen::{convert::IntoWasmAbi, JsValue}; pub struct CodeRun { #[serde(skip_serializing_if = "Option::is_none")] pub formatted_code_string: Option, - pub std_out: Option, pub std_err: Option, pub cells_accessed: HashSet, - pub result: CodeRunResult, + pub error: Option, pub return_type: Option, - pub spill_error: bool, pub line_number: Option, pub output_type: Option, - pub last_modified: DateTime, } impl CodeRun { - /// Returns the output value of a code run at the relative location (ie, (0,0) is the top of the code run result). - /// A spill or error returns [`CellValue::Blank`]. Note: this assumes a [`CellValue::Code`] exists at the location. - pub fn cell_value_at(&self, x: u32, y: u32) -> Option { - if self.spill_error { - Some(CellValue::Blank) - } else { - self.cell_value_ref_at(x, y).cloned() - } - } - - /// Returns the output value of a code run at the relative location (ie, (0,0) is the top of the code run result). - /// A spill or error returns `None`. Note: this assumes a [`CellValue::Code`] exists at the location. - pub fn cell_value_ref_at(&self, x: u32, y: u32) -> Option<&CellValue> { - if self.spill_error { - None - } else { - self.result.as_std_ref().ok()?.get(x, y).ok() - } - } - - /// Returns the cell value at a relative location (0-indexed) into the code - /// run output, for use when a formula references a cell. - pub fn get_cell_for_formula(&self, x: u32, y: u32) -> CellValue { - if self.spill_error { - CellValue::Blank - } else { - match &self.result { - CodeRunResult::Ok(value) => match value { - Value::Single(v) => v.clone(), - Value::Array(a) => a.get(x, y).cloned().unwrap_or(CellValue::Blank), - Value::Tuple(_) => CellValue::Error(Box::new( - // should never happen - RunErrorMsg::InternalError("tuple saved as code run result".into()) - .without_span(), - )), - }, - CodeRunResult::Err(e) => CellValue::Error(Box::new(e.clone())), - } - } - } - - /// Returns the size of the output array, or defaults to `_1X1` (since output always includes the code_cell). - /// Note: this does not take spill_error into account. - pub fn output_size(&self) -> ArraySize { - match &self.result { - CodeRunResult::Ok(Value::Array(a)) => a.size(), - CodeRunResult::Ok(Value::Single(_) | Value::Tuple(_)) | CodeRunResult::Err(_) => { - ArraySize::_1X1 - } - } - } - - pub fn is_html(&self) -> bool { - if let Some(code_cell_value) = self.cell_value_at(0, 0) { - code_cell_value.is_html() - } else { - false - } - } - - pub fn is_image(&self) -> bool { - if let Some(code_cell_value) = self.cell_value_at(0, 0) { - code_cell_value.is_image() - } else { - false - } - } - - /// returns a SheetRect for the output size of a code cell (defaults to 1x1) - /// Note: this returns a 1x1 if there is a spill_error. - pub fn output_sheet_rect(&self, sheet_pos: SheetPos, ignore_spill: bool) -> SheetRect { - if !ignore_spill && self.spill_error { - SheetRect::from_sheet_pos_and_size(sheet_pos, ArraySize::_1X1) - } else { - SheetRect::from_sheet_pos_and_size(sheet_pos, self.output_size()) - } - } - - /// returns a SheetRect for the output size of a code cell (defaults to 1x1) - /// Note: this returns a 1x1 if there is a spill_error. - pub fn output_rect(&self, pos: Pos, ignore_spill: bool) -> Rect { - if !ignore_spill && self.spill_error { - Rect::from_pos_and_size(pos, ArraySize::_1X1) - } else { - Rect::from_pos_and_size(pos, self.output_size()) - } - } - /// Returns any error in a code run. pub fn get_error(&self) -> Option { - match &self.result { - CodeRunResult::Ok { .. } => None, - CodeRunResult::Err(error) => Some(error.to_owned()), - } + self.error.clone() } } @@ -159,135 +64,33 @@ impl wasm_bindgen::convert::IntoWasmAbi for ConnectionKind { } } -/// Custom version of [`std::result::Result`] that serializes the way we want. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum CodeRunResult { - Ok(Value), - Err(RunError), -} -impl CodeRunResult { - /// Converts into a [`std::result::Result`] by value. - pub fn into_std(self) -> Result { - match self { - CodeRunResult::Ok(v) => Ok(v), - CodeRunResult::Err(e) => Err(e), - } - } - /// Converts into a [`std::result::Result`] by reference. - pub fn as_std_ref(&self) -> Result<&Value, &RunError> { - match self { - CodeRunResult::Ok(v) => Ok(v), - CodeRunResult::Err(e) => Err(e), - } - } -} +// /// Custom version of [`std::result::Result`] that serializes the way we want. +// #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +// #[serde(untagged)] +// pub enum CodeRunResult { +// Ok(Value), +// Err(RunError), +// } +// impl CodeRunResult { +// /// Converts into a [`std::result::Result`] by value. +// pub fn into_std(self) -> Result { +// match self { +// CodeRunResult::Ok(v) => Ok(v), +// CodeRunResult::Err(e) => Err(e), +// } +// } +// /// Converts into a [`std::result::Result`] by reference. +// pub fn as_std_ref(&self) -> Result<&Value, &RunError> { +// match self { +// CodeRunResult::Ok(v) => Ok(v), +// CodeRunResult::Err(e) => Err(e), +// } +// } +// } #[cfg(test)] mod test { - use super::*; - use crate::{grid::SheetId, Array}; - use serial_test::parallel; - - #[test] - #[parallel] - fn test_output_size() { - let sheet_id = SheetId::new(); - let code_run = CodeRun { - std_out: None, - std_err: None, - formatted_code_string: None, - cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Single(CellValue::Number(1.into()))), - return_type: Some("number".into()), - line_number: None, - output_type: None, - spill_error: false, - last_modified: Utc::now(), - }; - assert_eq!(code_run.output_size(), ArraySize::_1X1); - assert_eq!( - code_run.output_sheet_rect( - SheetPos { - x: -1, - y: -2, - sheet_id - }, - false - ), - SheetRect::from_numbers(-1, -2, 1, 1, sheet_id) - ); - - let code_run = CodeRun { - std_out: None, - std_err: None, - formatted_code_string: None, - cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Array(Array::new_empty( - ArraySize::new(10, 11).unwrap(), - ))), - return_type: Some("number".into()), - line_number: None, - output_type: None, - spill_error: false, - last_modified: Utc::now(), - }; - assert_eq!(code_run.output_size().w.get(), 10); - assert_eq!(code_run.output_size().h.get(), 11); - assert_eq!( - code_run.output_sheet_rect( - SheetPos { - x: 1, - y: 2, - sheet_id - }, - false - ), - SheetRect::from_numbers(1, 2, 10, 11, sheet_id) - ); - } - - #[test] - #[parallel] - fn test_output_sheet_rect_spill_error() { - let sheet_id = SheetId::new(); - let code_run = CodeRun { - formatted_code_string: None, - std_out: None, - std_err: None, - cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Array(Array::new_empty( - ArraySize::new(10, 11).unwrap(), - ))), - return_type: Some("number".into()), - line_number: None, - output_type: None, - spill_error: true, - last_modified: Utc::now(), - }; - assert_eq!(code_run.output_size().w.get(), 10); - assert_eq!(code_run.output_size().h.get(), 11); - assert_eq!( - code_run.output_sheet_rect( - SheetPos { - x: 1, - y: 2, - sheet_id - }, - false - ), - SheetRect::from_numbers(1, 2, 1, 1, sheet_id) - ); - assert_eq!( - code_run.output_sheet_rect( - SheetPos { - x: 1, - y: 2, - sheet_id - }, - true - ), - SheetRect::from_numbers(1, 2, 10, 11, sheet_id) - ); - } + // use super::*; + // use crate::{grid::SheetId, Array}; + // use serial_test::parallel; } diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs new file mode 100644 index 0000000000..29f8685eb3 --- /dev/null +++ b/quadratic-core/src/grid/data_table.rs @@ -0,0 +1,251 @@ +//! CodeRun is the output of a CellValue::Code type +//! +//! This lives in sheet.data_tables. CodeRun is optional within sheet.data_tables for +//! any given CellValue::Code type (ie, if it doesn't exist then a run hasn't been +//! performed yet). + +use crate::grid::CodeRun; +use crate::{ArraySize, CellValue, Pos, Rect, RunError, RunErrorMsg, SheetPos, SheetRect, Value}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +struct DataTableColumn { + name: String, + display: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum DataTableKind { + CodeRun(CodeRun), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DataTable { + pub kind: DataTableKind, + pub value: Value, + pub spill_error: bool, + pub last_modified: DateTime, +} + +impl DataTable { + pub fn code_run(&self) -> Option<&CodeRun> { + match self.kind { + DataTableKind::CodeRun(ref code_run) => Some(code_run), + } + } + + pub fn has_error(&self) -> bool { + match self.kind { + DataTableKind::CodeRun(ref code_run) => code_run.error.is_some(), + } + } + + pub fn get_error(&self) -> Option { + self.code_run() + .and_then(|code_run| code_run.error.to_owned()) + } + + /// Returns the output value of a code run at the relative location (ie, (0,0) is the top of the code run result). + /// A spill or error returns [`CellValue::Blank`]. Note: this assumes a [`CellValue::Code`] exists at the location. + pub fn cell_value_at(&self, x: u32, y: u32) -> Option { + if self.spill_error { + Some(CellValue::Blank) + } else { + self.cell_value_ref_at(x, y).cloned() + } + } + + /// Returns the output value of a code run at the relative location (ie, (0,0) is the top of the code run result). + /// A spill or error returns `None`. Note: this assumes a [`CellValue::Code`] exists at the location. + pub fn cell_value_ref_at(&self, x: u32, y: u32) -> Option<&CellValue> { + if self.spill_error { + None + } else { + self.value.get(x, y).ok() + } + } + + /// Returns the cell value at a relative location (0-indexed) into the code + /// run output, for use when a formula references a cell. + pub fn get_cell_for_formula(&self, x: u32, y: u32) -> CellValue { + if self.spill_error { + CellValue::Blank + } else { + match &self.value { + Value::Single(v) => v.clone(), + Value::Array(a) => a.get(x, y).cloned().unwrap_or(CellValue::Blank), + Value::Tuple(_) => CellValue::Error(Box::new( + // should never happen + RunErrorMsg::InternalError("tuple saved as code run result".into()) + .without_span(), + )), + } + } + } + + /// Returns the size of the output array, or defaults to `_1X1` (since output always includes the code_cell). + /// Note: this does not take spill_error into account. + pub fn output_size(&self) -> ArraySize { + match &self.value { + Value::Array(a) => a.size(), + Value::Single(_) | Value::Tuple(_) => ArraySize::_1X1, + } + } + + pub fn is_html(&self) -> bool { + if let Some(code_cell_value) = self.cell_value_at(0, 0) { + code_cell_value.is_html() + } else { + false + } + } + + pub fn is_image(&self) -> bool { + if let Some(code_cell_value) = self.cell_value_at(0, 0) { + code_cell_value.is_image() + } else { + false + } + } + + /// returns a SheetRect for the output size of a code cell (defaults to 1x1) + /// Note: this returns a 1x1 if there is a spill_error. + pub fn output_sheet_rect(&self, sheet_pos: SheetPos, ignore_spill: bool) -> SheetRect { + if !ignore_spill && self.spill_error { + SheetRect::from_sheet_pos_and_size(sheet_pos, ArraySize::_1X1) + } else { + SheetRect::from_sheet_pos_and_size(sheet_pos, self.output_size()) + } + } + + /// returns a SheetRect for the output size of a code cell (defaults to 1x1) + /// Note: this returns a 1x1 if there is a spill_error. + pub fn output_rect(&self, pos: Pos, ignore_spill: bool) -> Rect { + if !ignore_spill && self.spill_error { + Rect::from_pos_and_size(pos, ArraySize::_1X1) + } else { + Rect::from_pos_and_size(pos, self.output_size()) + } + } +} + +#[cfg(test)] +mod test { + use std::collections::HashSet; + + use super::*; + use crate::{grid::SheetId, Array}; + use serial_test::parallel; + + #[test] + #[parallel] + fn test_output_size() { + let sheet_id = SheetId::new(); + let code_run = CodeRun { + std_out: None, + std_err: None, + formatted_code_string: None, + cells_accessed: HashSet::new(), + error: None, + return_type: Some("number".into()), + line_number: None, + output_type: None, + }; + let code_run = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Number(1.into())), + spill_error: false, + last_modified: Utc::now(), + }; + assert_eq!(code_run.output_size(), ArraySize::_1X1); + assert_eq!( + code_run.output_sheet_rect( + SheetPos { + x: -1, + y: -2, + sheet_id + }, + false + ), + SheetRect::from_numbers(-1, -2, 1, 1, sheet_id) + ); + + let code_run = CodeRun { + std_out: None, + std_err: None, + formatted_code_string: None, + cells_accessed: HashSet::new(), + error: None, + return_type: Some("number".into()), + line_number: None, + output_type: None, + }; + + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(Array::new_empty(ArraySize::new(10, 11).unwrap())), + spill_error: false, + last_modified: Utc::now(), + }; + assert_eq!(data_table.output_size().w.get(), 10); + assert_eq!(data_table.output_size().h.get(), 11); + assert_eq!( + data_table.output_sheet_rect( + SheetPos { + x: 1, + y: 2, + sheet_id + }, + false + ), + SheetRect::from_numbers(1, 2, 10, 11, sheet_id) + ); + } + + #[test] + #[parallel] + fn test_output_sheet_rect_spill_error() { + let sheet_id = SheetId::new(); + let code_run = CodeRun { + formatted_code_string: None, + std_out: None, + std_err: None, + cells_accessed: HashSet::new(), + error: None, + return_type: Some("number".into()), + line_number: None, + output_type: None, + }; + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(Array::new_empty(ArraySize::new(10, 11).unwrap())), + spill_error: true, + last_modified: Utc::now(), + }; + assert_eq!(data_table.output_size().w.get(), 10); + assert_eq!(data_table.output_size().h.get(), 11); + assert_eq!( + data_table.output_sheet_rect( + SheetPos { + x: 1, + y: 2, + sheet_id + }, + false + ), + SheetRect::from_numbers(1, 2, 1, 1, sheet_id) + ); + assert_eq!( + data_table.output_sheet_rect( + SheetPos { + x: 1, + y: 2, + sheet_id + }, + true + ), + SheetRect::from_numbers(1, 2, 10, 11, sheet_id) + ); + } +} diff --git a/quadratic-core/src/grid/file/mod.rs b/quadratic-core/src/grid/file/mod.rs index fb5450a699..99d83645de 100644 --- a/quadratic-core/src/grid/file/mod.rs +++ b/quadratic-core/src/grid/file/mod.rs @@ -386,7 +386,13 @@ mod tests { ); assert_eq!(sheet.data_tables.len(), 10); assert_eq!( - sheet.data_tables.get(&Pos { x: 2, y: 6 }).unwrap().std_err, + sheet + .data_tables + .get(&Pos { x: 2, y: 6 }) + .unwrap() + .code_run() + .unwrap() + .std_err, Some("x is not defined".into()) ); } diff --git a/quadratic-core/src/grid/file/serialize/code_cell.rs b/quadratic-core/src/grid/file/serialize/code_cell.rs deleted file mode 100644 index 583224c02f..0000000000 --- a/quadratic-core/src/grid/file/serialize/code_cell.rs +++ /dev/null @@ -1,121 +0,0 @@ -use anyhow::Result; -use chrono::Utc; -use indexmap::IndexMap; -use itertools::Itertools; - -use crate::{ - grid::{CodeRun, CodeRunResult}, - Pos, Value, -}; - -use super::{ - cell_value::{export_cell_value, import_cell_value}, - current, -}; - -pub(crate) fn import_code_cell_builder( - code_runs: Vec<(current::PosSchema, current::CodeRunSchema)>, -) -> Result> { - let mut new_code_runs = IndexMap::new(); - - code_runs.into_iter().for_each(|(pos, code_run)| { - let cells_accessed = code_run - .cells_accessed - .into_iter() - .map(crate::SheetRect::from) - .collect(); - - let result = match code_run.result { - current::CodeRunResultSchema::Ok(output) => CodeRunResult::Ok(match output { - current::OutputValueSchema::Single(value) => { - Value::Single(import_cell_value(value.to_owned())) - } - current::OutputValueSchema::Array(current::OutputArraySchema { size, values }) => { - Value::Array(crate::Array::from( - values - .into_iter() - .chunks(size.w as usize) - .into_iter() - .map(|row| row.into_iter().map(import_cell_value).collect::>()) - .collect::>>(), - )) - } - }), - current::CodeRunResultSchema::Err(error) => CodeRunResult::Err(error.to_owned().into()), - }; - new_code_runs.insert( - Pos { x: pos.x, y: pos.y }, - CodeRun { - formatted_code_string: code_run.formatted_code_string, - last_modified: code_run.last_modified.unwrap_or(Utc::now()), // this is required but fall back to now if failed - std_out: code_run.std_out, - std_err: code_run.std_err, - spill_error: code_run.spill_error, - cells_accessed, - result, - return_type: code_run.return_type, - line_number: code_run.line_number, - output_type: code_run.output_type, - }, - ); - }); - Ok(new_code_runs) -} - -pub(crate) fn export_rows_code_runs( - code_runs: IndexMap, -) -> Vec<(current::PosSchema, current::CodeRunSchema)> { - code_runs - .into_iter() - .map(|(pos, code_run)| { - let result = match code_run.result { - CodeRunResult::Ok(output) => match output { - Value::Single(cell_value) => current::CodeRunResultSchema::Ok( - current::OutputValueSchema::Single(export_cell_value(cell_value)), - ), - Value::Array(array) => current::CodeRunResultSchema::Ok( - current::OutputValueSchema::Array(current::OutputArraySchema { - size: current::OutputSizeSchema { - w: array.width() as i64, - h: array.height() as i64, - }, - values: array - .rows() - .flat_map(|row| { - row.iter().map(|value| export_cell_value(value.to_owned())) - }) - .collect(), - }), - ), - Value::Tuple(_) => current::CodeRunResultSchema::Err(current::RunErrorSchema { - span: None, - msg: current::RunErrorMsgSchema::Unexpected("tuple as cell output".into()), - }), - }, - CodeRunResult::Err(error) => current::CodeRunResultSchema::Err( - current::RunErrorSchema::from_grid_run_error(error), - ), - }; - - ( - current::PosSchema::from(pos), - current::CodeRunSchema { - formatted_code_string: code_run.formatted_code_string, - last_modified: Some(code_run.last_modified), - std_out: code_run.std_out, - std_err: code_run.std_err, - spill_error: code_run.spill_error, - cells_accessed: code_run - .cells_accessed - .into_iter() - .map(current::SheetRectSchema::from) - .collect(), - result, - return_type: code_run.return_type, - line_number: code_run.line_number, - output_type: code_run.output_type, - }, - ) - }) - .collect() -} diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs new file mode 100644 index 0000000000..4a20f6616d --- /dev/null +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -0,0 +1,327 @@ +use anyhow::{anyhow, Result}; +use chrono::Utc; +use indexmap::IndexMap; +use itertools::Itertools; + +use crate::{ + grid::{CodeRun, DataTable, DataTableKind}, + ArraySize, Axis, Pos, RunError, RunErrorMsg, Value, +}; + +use super::{ + cell_value::{export_cell_value, import_cell_value}, + current, +}; + +pub(crate) fn import_run_error_msg_builder( + run_error_msg: current::RunErrorMsgSchema, +) -> Result { + let run_error_msg = match run_error_msg { + current::RunErrorMsgSchema::PythonError(msg) => RunErrorMsg::PythonError(msg), + current::RunErrorMsgSchema::Unexpected(msg) => RunErrorMsg::Unexpected(msg), + current::RunErrorMsgSchema::Spill => RunErrorMsg::Spill, + current::RunErrorMsgSchema::Unimplemented(msg) => RunErrorMsg::Unimplemented(msg), + current::RunErrorMsgSchema::UnknownError => RunErrorMsg::UnknownError, + current::RunErrorMsgSchema::InternalError(msg) => RunErrorMsg::InternalError(msg), + current::RunErrorMsgSchema::Unterminated(msg) => RunErrorMsg::Unterminated(msg), + current::RunErrorMsgSchema::Expected { expected, got } => { + RunErrorMsg::Expected { expected, got } + } + current::RunErrorMsgSchema::TooManyArguments { + func_name, + max_arg_count, + } => RunErrorMsg::TooManyArguments { + func_name, + max_arg_count, + }, + current::RunErrorMsgSchema::MissingRequiredArgument { + func_name, + arg_name, + } => RunErrorMsg::MissingRequiredArgument { + func_name, + arg_name, + }, + current::RunErrorMsgSchema::BadFunctionName => RunErrorMsg::BadFunctionName, + current::RunErrorMsgSchema::BadCellReference => RunErrorMsg::BadCellReference, + current::RunErrorMsgSchema::BadNumber => RunErrorMsg::BadNumber, + current::RunErrorMsgSchema::BadOp { + op, + ty1, + ty2, + use_duration_instead, + } => RunErrorMsg::BadOp { + op, + ty1, + ty2, + use_duration_instead, + }, + current::RunErrorMsgSchema::NaN => RunErrorMsg::NaN, + current::RunErrorMsgSchema::ExactArraySizeMismatch { expected, got } => { + RunErrorMsg::ExactArraySizeMismatch { + expected: ArraySize::try_from((expected.w, expected.h)).map_err(|e| anyhow!(e))?, + got: ArraySize::try_from((got.w, got.h)).map_err(|e| anyhow!(e))?, + } + } + current::RunErrorMsgSchema::ExactArrayAxisMismatch { + axis, + expected, + got, + } => RunErrorMsg::ExactArrayAxisMismatch { + axis: Axis::from(>::into(axis)), + expected, + got, + }, + current::RunErrorMsgSchema::ArrayAxisMismatch { + axis, + expected, + got, + } => RunErrorMsg::ArrayAxisMismatch { + axis: Axis::from(>::into(axis)), + expected, + got, + }, + current::RunErrorMsgSchema::EmptyArray => RunErrorMsg::EmptyArray, + current::RunErrorMsgSchema::NonRectangularArray => RunErrorMsg::NonRectangularArray, + current::RunErrorMsgSchema::NonLinearArray => RunErrorMsg::NonLinearArray, + current::RunErrorMsgSchema::ArrayTooBig => RunErrorMsg::ArrayTooBig, + current::RunErrorMsgSchema::CircularReference => RunErrorMsg::CircularReference, + current::RunErrorMsgSchema::Overflow => RunErrorMsg::Overflow, + current::RunErrorMsgSchema::DivideByZero => RunErrorMsg::DivideByZero, + current::RunErrorMsgSchema::NegativeExponent => RunErrorMsg::NegativeExponent, + current::RunErrorMsgSchema::NotANumber => RunErrorMsg::NotANumber, + current::RunErrorMsgSchema::Infinity => RunErrorMsg::Infinity, + current::RunErrorMsgSchema::IndexOutOfBounds => RunErrorMsg::IndexOutOfBounds, + current::RunErrorMsgSchema::NoMatch => RunErrorMsg::NoMatch, + current::RunErrorMsgSchema::InvalidArgument => RunErrorMsg::InvalidArgument, + }; + + Ok(run_error_msg) +} + +pub(crate) fn import_code_run_builder(code_run: current::CodeRunSchema) -> Result { + let cells_accessed = code_run + .cells_accessed + .into_iter() + .map(crate::SheetRect::from) + .collect(); + + let error = if let Some(error) = code_run.error { + Some(RunError { + span: error.span.map(|span| crate::Span { + start: span.start, + end: span.end, + }), + msg: import_run_error_msg_builder(error.msg)?, + }) + } else { + None + }; + let code_run = CodeRun { + formatted_code_string: code_run.formatted_code_string, + std_out: code_run.std_out, + std_err: code_run.std_err, + error, + cells_accessed, + return_type: code_run.return_type, + line_number: code_run.line_number, + output_type: code_run.output_type, + }; + + Ok(code_run) +} + +pub(crate) fn import_data_table_builder( + data_tables: Vec<(current::PosSchema, current::DataTableSchema)>, +) -> Result> { + let mut new_data_tables = IndexMap::new(); + + for (pos, data_table) in data_tables.into_iter() { + let value = match data_table.value { + current::OutputValueSchema::Single(value) => { + Value::Single(import_cell_value(value.to_owned())) + } + current::OutputValueSchema::Array(current::OutputArraySchema { size, values }) => { + Value::Array(crate::Array::from( + values + .into_iter() + .chunks(size.w as usize) + .into_iter() + .map(|row| row.into_iter().map(import_cell_value).collect::>()) + .collect::>>(), + )) + } + }; + + let data_table = DataTable { + kind: match data_table.kind { + current::DataTableKindSchema::CodeRun(code_run) => { + DataTableKind::CodeRun(import_code_run_builder(code_run)?) + } + }, + last_modified: data_table.last_modified.unwrap_or(Utc::now()), // this is required but fall back to now if failed + spill_error: data_table.spill_error, + value, + }; + + new_data_tables.insert(Pos { x: pos.x, y: pos.y }, data_table); + } + + Ok(new_data_tables) +} + +pub(crate) fn export_run_error_msg(run_error_msg: RunErrorMsg) -> current::RunErrorMsgSchema { + match run_error_msg { + RunErrorMsg::PythonError(msg) => current::RunErrorMsgSchema::PythonError(msg), + RunErrorMsg::Unexpected(msg) => current::RunErrorMsgSchema::Unexpected(msg), + RunErrorMsg::Spill => current::RunErrorMsgSchema::Spill, + RunErrorMsg::Unimplemented(msg) => current::RunErrorMsgSchema::Unimplemented(msg), + RunErrorMsg::UnknownError => current::RunErrorMsgSchema::UnknownError, + RunErrorMsg::InternalError(msg) => current::RunErrorMsgSchema::InternalError(msg), + RunErrorMsg::Unterminated(msg) => current::RunErrorMsgSchema::Unterminated(msg), + RunErrorMsg::Expected { expected, got } => { + current::RunErrorMsgSchema::Expected { expected, got } + } + RunErrorMsg::TooManyArguments { + func_name, + max_arg_count, + } => current::RunErrorMsgSchema::TooManyArguments { + func_name, + max_arg_count, + }, + RunErrorMsg::MissingRequiredArgument { + func_name, + arg_name, + } => current::RunErrorMsgSchema::MissingRequiredArgument { + func_name, + arg_name, + }, + RunErrorMsg::BadFunctionName => current::RunErrorMsgSchema::BadFunctionName, + RunErrorMsg::BadCellReference => current::RunErrorMsgSchema::BadCellReference, + RunErrorMsg::BadNumber => current::RunErrorMsgSchema::BadNumber, + RunErrorMsg::BadOp { + op, + ty1, + ty2, + use_duration_instead, + } => current::RunErrorMsgSchema::BadOp { + op, + ty1, + ty2, + use_duration_instead, + }, + RunErrorMsg::NaN => current::RunErrorMsgSchema::NaN, + RunErrorMsg::ExactArraySizeMismatch { expected, got } => { + current::RunErrorMsgSchema::ExactArraySizeMismatch { + expected: (expected.w, expected.h).into(), + got: (got.w, got.h).into(), + } + } + RunErrorMsg::ExactArrayAxisMismatch { + axis, + expected, + got, + } => current::RunErrorMsgSchema::ExactArrayAxisMismatch { + axis: current::AxisSchema::from(>::into(axis)), + expected, + got, + }, + RunErrorMsg::ArrayAxisMismatch { + axis, + expected, + got, + } => current::RunErrorMsgSchema::ArrayAxisMismatch { + axis: current::AxisSchema::from(>::into(axis)), + expected, + got, + }, + RunErrorMsg::EmptyArray => current::RunErrorMsgSchema::EmptyArray, + RunErrorMsg::NonRectangularArray => current::RunErrorMsgSchema::NonRectangularArray, + RunErrorMsg::NonLinearArray => current::RunErrorMsgSchema::NonLinearArray, + RunErrorMsg::ArrayTooBig => current::RunErrorMsgSchema::ArrayTooBig, + RunErrorMsg::CircularReference => current::RunErrorMsgSchema::CircularReference, + RunErrorMsg::Overflow => current::RunErrorMsgSchema::Overflow, + RunErrorMsg::DivideByZero => current::RunErrorMsgSchema::DivideByZero, + RunErrorMsg::NegativeExponent => current::RunErrorMsgSchema::NegativeExponent, + RunErrorMsg::NotANumber => current::RunErrorMsgSchema::NotANumber, + RunErrorMsg::Infinity => current::RunErrorMsgSchema::Infinity, + RunErrorMsg::IndexOutOfBounds => current::RunErrorMsgSchema::IndexOutOfBounds, + RunErrorMsg::NoMatch => current::RunErrorMsgSchema::NoMatch, + RunErrorMsg::InvalidArgument => current::RunErrorMsgSchema::InvalidArgument, + } +} + +pub(crate) fn export_code_run(code_run: CodeRun) -> current::CodeRunSchema { + let error = if let Some(error) = code_run.error { + Some(current::RunErrorSchema { + span: error.span.map(|span| current::SpanSchema { + start: span.start, + end: span.end, + }), + msg: export_run_error_msg(error.msg), + }) + } else { + None + }; + + current::CodeRunSchema { + formatted_code_string: code_run.formatted_code_string, + std_out: code_run.std_out, + std_err: code_run.std_err, + error, + cells_accessed: code_run + .cells_accessed + .into_iter() + .map(current::SheetRectSchema::from) + .collect(), + return_type: code_run.return_type, + line_number: code_run.line_number, + output_type: code_run.output_type, + } +} + +pub(crate) fn export_data_table_runs( + data_tables: IndexMap, +) -> Vec<(current::PosSchema, current::DataTableSchema)> { + data_tables + .into_iter() + .map(|(pos, data_table)| { + let value = match data_table.value { + Value::Single(cell_value) => { + current::OutputValueSchema::Single(export_cell_value(cell_value)) + } + Value::Array(array) => { + current::OutputValueSchema::Array(current::OutputArraySchema { + size: current::OutputSizeSchema { + w: array.width() as i64, + h: array.height() as i64, + }, + values: array + .rows() + .flat_map(|row| { + row.iter().map(|value| export_cell_value(value.to_owned())) + }) + .collect(), + }) + } + Value::Tuple(_) => { + current::OutputValueSchema::Single(current::CellValueSchema::Blank) + } + }; + + let data_table = match data_table.kind { + DataTableKind::CodeRun(code_run) => { + let code_run = export_code_run(code_run); + + current::DataTableSchema { + kind: current::DataTableKindSchema::CodeRun(code_run), + last_modified: Some(data_table.last_modified), + spill_error: data_table.spill_error, + value, + } + } + }; + + (current::PosSchema::from(pos), data_table) + }) + .collect() +} diff --git a/quadratic-core/src/grid/file/serialize/mod.rs b/quadratic-core/src/grid/file/serialize/mod.rs index add3e02aee..e99c21a15f 100644 --- a/quadratic-core/src/grid/file/serialize/mod.rs +++ b/quadratic-core/src/grid/file/serialize/mod.rs @@ -8,8 +8,8 @@ use super::CURRENT_VERSION; pub(crate) mod borders; pub(crate) mod cell_value; -pub(crate) mod code_cell; pub(crate) mod column; +pub(crate) mod data_table; pub(crate) mod format; pub(crate) mod selection; pub mod sheets; diff --git a/quadratic-core/src/grid/file/serialize/sheets.rs b/quadratic-core/src/grid/file/serialize/sheets.rs index ce6ca75db5..25315fa852 100644 --- a/quadratic-core/src/grid/file/serialize/sheets.rs +++ b/quadratic-core/src/grid/file/serialize/sheets.rs @@ -9,9 +9,9 @@ use crate::{ use super::{ borders::{export_borders, import_borders}, - code_cell::{export_rows_code_runs, import_code_cell_builder}, column::{export_column_builder, import_column_builder}, current, + data_table::{export_data_table_runs, import_data_table_builder}, format::{ export_format, export_formats, export_rows_size, import_format, import_formats, import_rows_size, @@ -28,7 +28,7 @@ pub fn import_sheet(sheet: current::SheetSchema) -> Result { offsets: SheetOffsets::import(sheet.offsets), columns: import_column_builder(sheet.columns)?, - data_tables: import_code_cell_builder(sheet.code_runs)?, + data_tables: import_data_table_builder(sheet.data_tables)?, data_bounds: GridBounds::Empty, format_bounds: GridBounds::Empty, @@ -60,7 +60,7 @@ pub(crate) fn export_sheet(sheet: Sheet) -> current::SheetSchema { validations: export_validations(sheet.validations), rows_resize: export_rows_size(sheet.rows_resize), borders: export_borders(sheet.borders), - code_runs: export_rows_code_runs(sheet.data_tables), + data_tables: export_data_table_runs(sheet.data_tables), columns: export_column_builder(sheet.columns), } } diff --git a/quadratic-core/src/grid/file/v1_4/schema.rs b/quadratic-core/src/grid/file/v1_4/schema.rs index f2538446fa..7b07c0937b 100644 --- a/quadratic-core/src/grid/file/v1_4/schema.rs +++ b/quadratic-core/src/grid/file/v1_4/schema.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, fmt::{self, Display}, + num::NonZeroU32, }; use serde::{Deserialize, Serialize}; @@ -127,6 +128,14 @@ pub struct OutputSize { pub w: i64, pub h: i64, } +impl From<(NonZeroU32, NonZeroU32)> for OutputSize { + fn from((w, h): (NonZeroU32, NonZeroU32)) -> Self { + Self { + w: w.get().into(), + h: h.get().into(), + } + } +} #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RenderSize { diff --git a/quadratic-core/src/grid/file/v1_6/file.rs b/quadratic-core/src/grid/file/v1_6/file.rs index 2c88c68544..712c9321f4 100644 --- a/quadratic-core/src/grid/file/v1_6/file.rs +++ b/quadratic-core/src/grid/file/v1_6/file.rs @@ -75,6 +75,146 @@ fn upgrade_borders(borders: current::Borders) -> Result { Ok(borders) } +fn upgrade_code_runs( + code_runs: Vec<(current::Pos, current::CodeRun)>, +) -> Result> { + code_runs + .into_iter() + .map(|(pos, code_run)| { + let error = if let current::CodeRunResult::Err(error) = &code_run.result { + let new_error_msg = match error.msg.to_owned() { + current::RunErrorMsg::PythonError(msg) => { + v1_7::RunErrorMsgSchema::PythonError(msg) + } + current::RunErrorMsg::Unexpected(msg) => { + v1_7::RunErrorMsgSchema::Unexpected(msg) + } + current::RunErrorMsg::Spill => v1_7::RunErrorMsgSchema::Spill, + current::RunErrorMsg::Unimplemented(msg) => { + v1_7::RunErrorMsgSchema::Unimplemented(msg) + } + current::RunErrorMsg::UnknownError => v1_7::RunErrorMsgSchema::UnknownError, + current::RunErrorMsg::InternalError(msg) => { + v1_7::RunErrorMsgSchema::InternalError(msg) + } + current::RunErrorMsg::Unterminated(msg) => { + v1_7::RunErrorMsgSchema::Unterminated(msg) + } + current::RunErrorMsg::Expected { expected, got } => { + v1_7::RunErrorMsgSchema::Expected { expected, got } + } + current::RunErrorMsg::TooManyArguments { + func_name, + max_arg_count, + } => v1_7::RunErrorMsgSchema::TooManyArguments { + func_name, + max_arg_count, + }, + current::RunErrorMsg::MissingRequiredArgument { + func_name, + arg_name, + } => v1_7::RunErrorMsgSchema::MissingRequiredArgument { + func_name, + arg_name, + }, + current::RunErrorMsg::BadFunctionName => { + v1_7::RunErrorMsgSchema::BadFunctionName + } + current::RunErrorMsg::BadCellReference => { + v1_7::RunErrorMsgSchema::BadCellReference + } + current::RunErrorMsg::BadNumber => v1_7::RunErrorMsgSchema::BadNumber, + current::RunErrorMsg::BadOp { + op, + ty1, + ty2, + use_duration_instead, + } => v1_7::RunErrorMsgSchema::BadOp { + op, + ty1, + ty2, + use_duration_instead, + }, + current::RunErrorMsg::NaN => v1_7::RunErrorMsgSchema::NaN, + current::RunErrorMsg::ExactArraySizeMismatch { expected, got } => { + v1_7::RunErrorMsgSchema::ExactArraySizeMismatch { expected, got } + } + current::RunErrorMsg::ExactArrayAxisMismatch { + axis, + expected, + got, + } => v1_7::RunErrorMsgSchema::ExactArrayAxisMismatch { + axis, + expected, + got, + }, + current::RunErrorMsg::ArrayAxisMismatch { + axis, + expected, + got, + } => v1_7::RunErrorMsgSchema::ArrayAxisMismatch { + axis, + expected, + got, + }, + current::RunErrorMsg::EmptyArray => v1_7::RunErrorMsgSchema::EmptyArray, + current::RunErrorMsg::NonRectangularArray => { + v1_7::RunErrorMsgSchema::NonRectangularArray + } + current::RunErrorMsg::NonLinearArray => v1_7::RunErrorMsgSchema::NonLinearArray, + current::RunErrorMsg::ArrayTooBig => v1_7::RunErrorMsgSchema::ArrayTooBig, + current::RunErrorMsg::CircularReference => { + v1_7::RunErrorMsgSchema::CircularReference + } + current::RunErrorMsg::Overflow => v1_7::RunErrorMsgSchema::Overflow, + current::RunErrorMsg::DivideByZero => v1_7::RunErrorMsgSchema::DivideByZero, + current::RunErrorMsg::NegativeExponent => { + v1_7::RunErrorMsgSchema::NegativeExponent + } + current::RunErrorMsg::NotANumber => v1_7::RunErrorMsgSchema::NotANumber, + current::RunErrorMsg::Infinity => v1_7::RunErrorMsgSchema::Infinity, + current::RunErrorMsg::IndexOutOfBounds => { + v1_7::RunErrorMsgSchema::IndexOutOfBounds + } + current::RunErrorMsg::NoMatch => v1_7::RunErrorMsgSchema::NoMatch, + current::RunErrorMsg::InvalidArgument => { + v1_7::RunErrorMsgSchema::InvalidArgument + } + }; + let new_error = v1_7::RunErrorSchema { + span: None, + msg: new_error_msg, + }; + Some(new_error) + } else { + None + }; + let new_code_run = v1_7::CodeRunSchema { + formatted_code_string: code_run.formatted_code_string, + std_out: code_run.std_out, + std_err: code_run.std_err, + cells_accessed: code_run.cells_accessed, + error, + return_type: code_run.return_type, + line_number: code_run.line_number, + output_type: code_run.output_type, + }; + let value = if let current::CodeRunResult::Ok(value) = &code_run.result { + value.to_owned() + } else { + v1_7::OutputValueSchema::Single(v1_7::CellValueSchema::Blank) + }; + let new_data_table = v1_7::DataTableSchema { + kind: v1_7::DataTableKindSchema::CodeRun(new_code_run), + value, + spill_error: code_run.spill_error, + last_modified: code_run.last_modified, + }; + Ok((v1_7::PosSchema::from(pos), new_data_table)) + }) + .collect::>>() +} + pub fn upgrade_sheet(sheet: current::Sheet) -> Result { Ok(v1_7::SheetSchema { id: sheet.id, @@ -83,7 +223,7 @@ pub fn upgrade_sheet(sheet: current::Sheet) -> Result { order: sheet.order, offsets: sheet.offsets, columns: sheet.columns, - code_runs: sheet.code_runs, + data_tables: upgrade_code_runs(sheet.code_runs)?, formats_all: sheet.formats_all, formats_columns: sheet.formats_columns, formats_rows: sheet.formats_rows, diff --git a/quadratic-core/src/grid/file/v1_6/schema.rs b/quadratic-core/src/grid/file/v1_6/schema.rs index ae1ba85761..0edeb45c23 100644 --- a/quadratic-core/src/grid/file/v1_6/schema.rs +++ b/quadratic-core/src/grid/file/v1_6/schema.rs @@ -8,7 +8,9 @@ use std::{ use uuid::Uuid; use super::schema_validation::Validations; +pub use crate::grid::file::v1_5::run_error::Axis; pub use v1_5::RunErrorMsg; +pub use v1_5::Span; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GridSchema { diff --git a/quadratic-core/src/grid/file/v1_7/schema.rs b/quadratic-core/src/grid/file/v1_7/schema.rs index a09f309def..b495ae75c1 100644 --- a/quadratic-core/src/grid/file/v1_7/schema.rs +++ b/quadratic-core/src/grid/file/v1_7/schema.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use crate::grid::file::v1_6::schema as v1_6; use crate::grid::file::v1_6::schema_validation as v1_6_validation; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; pub type IdSchema = v1_6::Id; @@ -13,7 +14,6 @@ pub type RunErrorSchema = v1_6::RunError; pub type FormatSchema = v1_6::Format; pub type ValidationsSchema = v1_6_validation::Validations; pub type ResizeSchema = v1_6::Resize; -pub type CodeRunSchema = v1_6::CodeRun; pub type CodeRunResultSchema = v1_6::CodeRunResult; pub type OutputValueSchema = v1_6::OutputValue; pub type OutputArraySchema = v1_6::OutputArray; @@ -33,6 +33,8 @@ pub type CellBorderSchema = v1_6::CellBorder; pub type ColumnRepeatSchema = v1_6::ColumnRepeat; pub type RenderSizeSchema = v1_6::RenderSize; pub type RunErrorMsgSchema = v1_6::RunErrorMsg; +pub type AxisSchema = v1_6::Axis; +pub type SpanSchema = v1_6::Span; pub type SelectionSchema = v1_6_validation::Selection; @@ -113,7 +115,7 @@ pub struct SheetSchema { pub order: String, pub offsets: OffsetsSchema, pub columns: Vec<(i64, ColumnSchema)>, - pub code_runs: Vec<(PosSchema, CodeRunSchema)>, + pub data_tables: Vec<(PosSchema, DataTableSchema)>, pub formats_all: Option, pub formats_columns: Vec<(i64, (FormatSchema, i64))>, pub formats_rows: Vec<(i64, (FormatSchema, i64))>, @@ -121,3 +123,47 @@ pub struct SheetSchema { pub validations: ValidationsSchema, pub borders: BordersSchema, } + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CodeRunSchema { + pub formatted_code_string: Option, + pub std_out: Option, + pub std_err: Option, + pub cells_accessed: Vec, + pub error: Option, + pub return_type: Option, + pub line_number: Option, + pub output_type: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum DataTableKindSchema { + CodeRun(CodeRunSchema), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DataTableSchema { + pub kind: DataTableKindSchema, + pub value: OutputValueSchema, + pub spill_error: bool, + pub last_modified: Option>, +} + +impl From for AxisSchema { + fn from(val: i8) -> Self { + match val { + 0 => AxisSchema::X, + 1 => AxisSchema::Y, + _ => panic!("Invalid Axis value: {}", val), + } + } +} + +impl Into for AxisSchema { + fn into(self) -> i8 { + match self { + AxisSchema::X => 0, + AxisSchema::Y => 1, + } + } +} diff --git a/quadratic-core/src/grid/mod.rs b/quadratic-core/src/grid/mod.rs index fe48d88b3f..112cdcbc4e 100644 --- a/quadratic-core/src/grid/mod.rs +++ b/quadratic-core/src/grid/mod.rs @@ -9,6 +9,7 @@ pub use borders::{ pub use bounds::GridBounds; pub use code_run::*; pub use column::{Column, ColumnData}; +pub use data_table::*; pub use formatting::{ Bold, CellAlign, CellFmtAttr, CellVerticalAlign, CellWrap, FillColor, Italic, NumericCommas, NumericDecimals, NumericFormat, NumericFormatKind, RenderSize, TextColor, @@ -24,6 +25,7 @@ mod borders; mod bounds; mod code_run; mod column; +mod data_table; pub mod file; pub mod formats; pub mod formatting; diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 1ac98aac6f..286e1e2220 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -10,6 +10,7 @@ use validations::Validations; use super::bounds::GridBounds; use super::column::Column; +use super::data_table::DataTable; use super::formats::format::Format; use super::formatting::CellFmtAttr; use super::ids::SheetId; @@ -35,6 +36,7 @@ pub mod row_resize; pub mod search; pub mod selection; pub mod send_render; +#[cfg(test)] pub mod sheet_test; pub mod summarize; pub mod validations; @@ -52,7 +54,7 @@ pub struct Sheet { pub columns: BTreeMap, #[serde(with = "crate::util::indexmap_serde")] - pub data_tables: IndexMap, + pub data_tables: IndexMap, // todo: we need to redo this struct to track the timestamp for all formats // applied to column and rows to properly use the latest column or row @@ -203,6 +205,15 @@ impl Sheet { self.columns.iter() } + pub fn iter_code_runs(&self) -> impl Iterator { + let result = self + .data_tables + .iter() + .flat_map(|(pos, data_table)| data_table.code_run().map(|code_run| (pos, code_run))); + + result + } + /// Returns the cell_value at a Pos using both column.values and data_tables (i.e., what would be returned if code asked /// for it). pub fn display_value(&self, pos: Pos) -> Option { diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 318159df6b..88a3f41333 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -4,8 +4,9 @@ use super::Sheet; use crate::{ formulas::replace_internal_cell_references, grid::{ + data_table::DataTable, js_types::{JsCodeCell, JsReturnInfo}, - CodeCellLanguage, CodeRun, RenderSize, + CodeCellLanguage, DataTableKind, RenderSize, }, CellValue, Pos, Rect, }; @@ -14,16 +15,16 @@ impl Sheet { /// Sets or deletes a code run. /// /// Returns the old value if it was set. - pub fn set_data_table(&mut self, pos: Pos, code_run: Option) -> Option { - if let Some(code_run) = code_run { - self.data_tables.insert(pos, code_run) + pub fn set_data_table(&mut self, pos: Pos, data_table: Option) -> Option { + if let Some(data_table) = data_table { + self.data_tables.insert(pos, data_table) } else { self.data_tables.shift_remove(&pos) } } /// Returns a DatatTable at a Pos - pub fn data_table(&self, pos: Pos) -> Option<&CodeRun> { + pub fn data_table(&self, pos: Pos) -> Option<&DataTable> { self.data_tables.get(&pos) } @@ -31,8 +32,8 @@ impl Sheet { pub fn code_columns_bounds(&self, column_start: i64, column_end: i64) -> Option> { let mut min: Option = None; let mut max: Option = None; - for (pos, code_run) in &self.data_tables { - let output_rect = code_run.output_rect(*pos, false); + for (pos, data_table) in &self.data_tables { + let output_rect = data_table.output_rect(*pos, false); if output_rect.min.x <= column_end && output_rect.max.x >= column_start { min = min .map(|min| Some(min.min(output_rect.min.y))) @@ -53,8 +54,8 @@ impl Sheet { pub fn code_rows_bounds(&self, row_start: i64, row_end: i64) -> Option> { let mut min: Option = None; let mut max: Option = None; - for (pos, code_run) in &self.data_tables { - let output_rect = code_run.output_rect(*pos, false); + for (pos, data_table) in &self.data_tables { + let output_rect = data_table.output_rect(*pos, false); if output_rect.min.y <= row_end && output_rect.max.y >= row_start { min = min .map(|min| Some(min.min(output_rect.min.x))) @@ -77,9 +78,9 @@ impl Sheet { pub fn get_code_cell_value(&self, pos: Pos) -> Option { self.data_tables .iter() - .find_map(|(code_cell_pos, code_run)| { - if code_run.output_rect(*code_cell_pos, false).contains(pos) { - code_run.cell_value_at( + .find_map(|(code_cell_pos, data_table)| { + if data_table.output_rect(*code_cell_pos, false).contains(pos) { + data_table.cell_value_at( (pos.x - code_cell_pos.x) as u32, (pos.y - code_cell_pos.y) as u32, ) @@ -89,7 +90,7 @@ impl Sheet { }) } - pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { + pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { self.data_tables .iter() .filter_map(move |(pos, code_cell_value)| { @@ -107,14 +108,14 @@ impl Sheet { } /// Returns whether a rect overlaps the output of a code cell. - /// It will only check code_cells until it finds the code_run at code_pos (since later data_tables do not cause spills in earlier ones) + /// It will only check code_cells until it finds the data_table at code_pos (since later data_tables do not cause spills in earlier ones) pub fn has_code_cell_in_rect(&self, rect: &Rect, code_pos: Pos) -> bool { - for (pos, code_run) in &self.data_tables { + for (pos, data_table) in &self.data_tables { if pos == &code_pos { // once we reach the code_cell, we can stop checking return false; } - if code_run.output_rect(*pos, false).intersects(*rect) { + if data_table.output_rect(*pos, false).intersects(*rect) { return true; } } @@ -130,8 +131,8 @@ impl Sheet { } else { self.data_tables .iter() - .find_map(|(code_cell_pos, code_run)| { - if code_run.output_rect(*code_cell_pos, false).contains(pos) { + .find_map(|(code_cell_pos, data_table)| { + if data_table.output_rect(*code_cell_pos, false).contains(pos) { if let Some(code_value) = self.cell_value(*code_cell_pos) { code_pos = *code_cell_pos; Some(code_value) @@ -154,17 +155,24 @@ impl Sheet { code_cell.code = replaced; } - if let Some(code_run) = self.data_table(code_pos) { + if let Some(data_table) = self.data_table(code_pos) { let evaluation_result = - serde_json::to_string(&code_run.result).unwrap_or("".into()); - let spill_error = if code_run.spill_error { + serde_json::to_string(&data_table.value).unwrap_or("".into()); + let spill_error = if data_table.spill_error { Some(self.find_spill_error_reasons( - &code_run.output_rect(code_pos, true), + &data_table.output_rect(code_pos, true), code_pos, )) } else { None }; + + #[allow(unreachable_patterns)] + let code_run = match &data_table.kind { + DataTableKind::CodeRun(code_run) => code_run, + _ => unreachable!(), + }; + Some(JsCodeCell { x: code_pos.x, y: code_pos.y, @@ -205,7 +213,7 @@ mod test { use super::*; use crate::{ controller::GridController, - grid::{js_types::JsRenderCellSpecial, CodeCellLanguage, CodeRunResult, RenderSize}, + grid::{js_types::JsRenderCellSpecial, CodeCellLanguage, CodeRun, RenderSize}, Array, CodeCellValue, SheetPos, Value, }; use bigdecimal::BigDecimal; @@ -247,7 +255,7 @@ mod test { #[test] #[parallel] - fn test_set_code_run() { + fn test_set_data_table() { let mut gc = GridController::test(); let sheet_id = gc.sheet_ids()[0]; let sheet = gc.grid_mut().try_sheet_mut(sheet_id).unwrap(); @@ -255,24 +263,28 @@ mod test { std_out: None, std_err: None, formatted_code_string: None, - last_modified: Utc::now(), cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Single(CellValue::Number(BigDecimal::from(2)))), + error: None, return_type: Some("number".into()), line_number: None, output_type: None, + }; + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Number(BigDecimal::from(2))), spill_error: false, + last_modified: Utc::now(), }; - let old = sheet.set_data_table(Pos { x: 0, y: 0 }, Some(code_run.clone())); + let old = sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!(old, None); - assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&code_run)); - assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&code_run)); + assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&data_table)); + assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&data_table)); assert_eq!(sheet.data_table(Pos { x: 1, y: 0 }), None); } #[test] #[parallel] - fn test_get_code_run() { + fn test_get_data_table() { let mut gc = GridController::test(); let sheet_id = gc.sheet_ids()[0]; let sheet = gc.grid_mut().try_sheet_mut(sheet_id).unwrap(); @@ -281,19 +293,23 @@ mod test { std_out: None, formatted_code_string: None, cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Single(CellValue::Number(BigDecimal::from(2)))), + error: None, return_type: Some("number".into()), line_number: None, output_type: None, + }; + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Number(BigDecimal::from(2))), spill_error: false, last_modified: Utc::now(), }; - sheet.set_data_table(Pos { x: 0, y: 0 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!( sheet.get_code_cell_value(Pos { x: 0, y: 0 }), Some(CellValue::Number(BigDecimal::from(2))) ); - assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&code_run)); + assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&data_table)); assert_eq!(sheet.data_table(Pos { x: 1, y: 1 }), None); } @@ -315,14 +331,18 @@ mod test { std_out: None, formatted_code_string: None, cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Array(Array::from(vec![vec!["1", "2", "3"]]))), + error: None, return_type: Some("number".into()), line_number: None, output_type: None, + }; + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(Array::from(vec![vec!["1", "2", "3"]])), spill_error: false, last_modified: Utc::now(), }; - sheet.set_data_table(Pos { x: 0, y: 0 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!( sheet.edit_code_value(Pos { x: 0, y: 0 }), Some(JsCodeCell { @@ -402,20 +422,20 @@ mod test { std_out: None, formatted_code_string: None, cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Array(Array::from(vec![ - vec!["1"], - vec!["2"], - vec!["3"], - ]))), + error: None, return_type: Some("number".into()), line_number: None, output_type: None, + }; + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(Array::from(vec![vec!["1"], vec!["2"], vec!["3"]])), spill_error: false, last_modified: Utc::now(), }; - sheet.set_data_table(Pos { x: 0, y: 0 }, Some(code_run.clone())); - sheet.set_data_table(Pos { x: 1, y: 1 }, Some(code_run.clone())); - sheet.set_data_table(Pos { x: 2, y: 3 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); + sheet.set_data_table(Pos { x: 1, y: 1 }, Some(data_table.clone())); + sheet.set_data_table(Pos { x: 2, y: 3 }, Some(data_table.clone())); assert_eq!(sheet.code_columns_bounds(0, 0), Some(0..3)); assert_eq!(sheet.code_columns_bounds(1, 1), Some(1..4)); @@ -437,16 +457,20 @@ mod test { std_out: None, formatted_code_string: None, cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Array(Array::from(vec![vec!["1", "2", "3'"]]))), + error: None, return_type: Some("number".into()), line_number: None, output_type: None, + }; + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(Array::from(vec![vec!["1", "2", "3'"]])), spill_error: false, last_modified: Utc::now(), }; - sheet.set_data_table(Pos { x: 0, y: 0 }, Some(code_run.clone())); - sheet.set_data_table(Pos { x: 1, y: 1 }, Some(code_run.clone())); - sheet.set_data_table(Pos { x: 3, y: 2 }, Some(code_run.clone())); + sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); + sheet.set_data_table(Pos { x: 1, y: 1 }, Some(data_table.clone())); + sheet.set_data_table(Pos { x: 3, y: 2 }, Some(data_table.clone())); assert_eq!(sheet.code_rows_bounds(0, 0), Some(0..3)); assert_eq!(sheet.code_rows_bounds(1, 1), Some(1..4)); diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 34abe77e07..68c55900b7 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -1,14 +1,11 @@ -use code_run::CodeRunResult; - use crate::{ grid::{ - code_run, formats::format::Format, js_types::{ JsHtmlOutput, JsNumber, JsRenderCell, JsRenderCellSpecial, JsRenderCodeCell, JsRenderCodeCellState, JsRenderFill, JsSheetFill, JsValidationWarning, }, - CellAlign, CodeCellLanguage, CodeRun, Column, + CellAlign, CodeCellLanguage, Column, DataTable, }, renderer_constants::{CELL_SHEET_HEIGHT, CELL_SHEET_WIDTH}, CellValue, Pos, Rect, RunError, RunErrorMsg, Value, @@ -158,13 +155,13 @@ impl Sheet { fn get_code_cells( &self, code: &CellValue, - run: &CodeRun, + data_table: &DataTable, output_rect: &Rect, code_rect: &Rect, ) -> Vec { let mut cells = vec![]; if let CellValue::Code(code) = code { - if run.spill_error { + if data_table.spill_error { cells.push(self.get_render_cell( code_rect.min.x, code_rect.min.y, @@ -175,7 +172,7 @@ impl Sheet { })), Some(code.language.to_owned()), )); - } else if let Some(error) = run.get_error() { + } else if let Some(error) = data_table.get_error() { cells.push(self.get_render_cell( code_rect.min.x, code_rect.min.y, @@ -208,7 +205,7 @@ impl Sheet { for x in x_start..=x_end { let column = self.get_column(x); for y in y_start..=y_end { - let value = run.cell_value_at( + let value = data_table.cell_value_at( (x - code_rect.min.x) as u32, (y - code_rect.min.y) as u32, ); @@ -376,11 +373,11 @@ impl Sheet { } pub fn get_render_code_cell(&self, pos: Pos) -> Option { - let run = self.data_tables.get(&pos)?; + let data_table = self.data_tables.get(&pos)?; let code = self.cell_value(pos)?; - let output_size = run.output_size(); - let (state, w, h, spill_error) = if run.spill_error { - let reasons = self.find_spill_error_reasons(&run.output_rect(pos, true), pos); + let output_size = data_table.output_size(); + let (state, w, h, spill_error) = if data_table.spill_error { + let reasons = self.find_spill_error_reasons(&data_table.output_rect(pos, true), pos); ( JsRenderCodeCellState::SpillError, output_size.w.get(), @@ -388,16 +385,17 @@ impl Sheet { Some(reasons), ) } else { - match run.result { - CodeRunResult::Err(_) | CodeRunResult::Ok(Value::Single(CellValue::Error(_))) => { - (JsRenderCodeCellState::RunError, 1, 1, None) - } - CodeRunResult::Ok(_) => ( + if data_table.has_error() + || matches!(data_table.value, Value::Single(CellValue::Error(_))) + { + (JsRenderCodeCellState::RunError, 1, 1, None) + } else { + ( JsRenderCodeCellState::Success, output_size.w.get(), output_size.h.get(), None, - ), + ) } }; Some(JsRenderCodeCell { @@ -418,14 +416,16 @@ impl Sheet { pub fn get_all_render_code_cells(&self) -> Vec { self.data_tables .iter() - .filter_map(|(pos, run)| { + .filter_map(|(pos, data_table)| { if let Some(code) = self.cell_value(*pos) { match &code { CellValue::Code(code) => { - let output_size = run.output_size(); - let (state, w, h, spill_error) = if run.spill_error { - let reasons = self - .find_spill_error_reasons(&run.output_rect(*pos, true), *pos); + let output_size = data_table.output_size(); + let (state, w, h, spill_error) = if data_table.spill_error { + let reasons = self.find_spill_error_reasons( + &data_table.output_rect(*pos, true), + *pos, + ); ( JsRenderCodeCellState::SpillError, output_size.w.get(), @@ -433,16 +433,15 @@ impl Sheet { Some(reasons), ) } else { - match run.result { - CodeRunResult::Ok(_) => ( + if data_table.has_error() { + (JsRenderCodeCellState::RunError, 1, 1, None) + } else { + ( JsRenderCodeCellState::Success, output_size.w.get(), output_size.h.get(), None, - ), - CodeRunResult::Err(_) => { - (JsRenderCodeCellState::RunError, 1, 1, None) - } + ) } }; Some(JsRenderCodeCell { @@ -612,7 +611,7 @@ mod tests { validation::{Validation, ValidationStyle}, validation_rules::{validation_logical::ValidationLogical, ValidationRule}, }, - Bold, CellVerticalAlign, CellWrap, Italic, RenderSize, + Bold, CellVerticalAlign, CellWrap, CodeRun, DataTableKind, Italic, RenderSize, }, selection::Selection, wasm_bindings::js::{clear_js_calls, expect_js_call, expect_js_call_count, hash_test}, @@ -645,18 +644,22 @@ mod tests { code: "1 + 1".to_string(), }), ); + let code_run = CodeRun { + formatted_code_string: None, + std_err: None, + std_out: None, + cells_accessed: HashSet::new(), + error: None, + return_type: Some("text".into()), + line_number: None, + output_type: None, + }; sheet.set_data_table( Pos { x: 2, y: 3 }, - Some(CodeRun { - formatted_code_string: None, - std_err: None, - std_out: None, + Some(DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Text("hello".to_string())), spill_error: false, - cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Single(CellValue::Text("hello".to_string()))), - return_type: Some("text".into()), - line_number: None, - output_type: None, last_modified: Utc::now(), }), ); @@ -858,27 +861,29 @@ mod tests { language: CodeCellLanguage::Python, code: "".to_string(), }); - - // code_run is always 3x2 let code_run = CodeRun { std_out: None, std_err: None, formatted_code_string: None, - last_modified: Utc::now(), cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Array( - vec![vec!["1", "2", "3"], vec!["4", "5", "6"]].into(), - )), + error: None, return_type: Some("text".into()), - spill_error: false, line_number: None, output_type: None, }; + // data_table is always 3x2 + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(vec![vec!["1", "2", "3"], vec!["4", "5", "6"]].into()), + spill_error: false, + last_modified: Utc::now(), + }; + // render rect is larger than code rect let code_cells = sheet.get_code_cells( &code_cell, - &code_run, + &data_table, &Rect::from_numbers(0, 0, 10, 10), &Rect::from_numbers(5, 5, 3, 2), ); @@ -892,7 +897,7 @@ mod tests { // code rect overlaps render rect to the top-left let code_cells = sheet.get_code_cells( &code_cell, - &code_run, + &data_table, &Rect::from_numbers(2, 1, 10, 10), &Rect::from_numbers(0, 0, 3, 2), ); @@ -903,7 +908,7 @@ mod tests { // code rect overlaps render rect to the bottom-right let code_cells = sheet.get_code_cells( &code_cell, - &code_run, + &data_table, &Rect::from_numbers(0, 0, 3, 2), &Rect::from_numbers(2, 1, 10, 10), ); @@ -915,14 +920,19 @@ mod tests { std_out: None, std_err: None, formatted_code_string: None, - last_modified: Utc::now(), cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Single(CellValue::Number(1.into()))), + error: None, return_type: Some("number".into()), - spill_error: false, line_number: None, output_type: None, }; + + let code_run = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Number(1.into())), + spill_error: false, + last_modified: Utc::now(), + }; let code_cells = sheet.get_code_cells( &code_cell, &code_run, @@ -1031,19 +1041,23 @@ mod tests { language: CodeCellLanguage::Python, code: "1 + 1".to_string(), }); - let run = CodeRun { + let code_run = CodeRun { std_out: None, std_err: None, formatted_code_string: None, - last_modified: Utc::now(), cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Single(CellValue::Number(2.into()))), + error: None, return_type: Some("number".into()), - spill_error: false, line_number: None, output_type: None, }; - sheet.set_data_table(pos, Some(run)); + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Number(2.into())), + spill_error: false, + last_modified: Utc::now(), + }; + sheet.set_data_table(pos, Some(data_table)); sheet.set_cell_value(pos, code); let rendering = sheet.get_render_code_cell(pos); assert_eq!( @@ -1076,19 +1090,23 @@ mod tests { let pos = (0, 0).into(); let image = "image".to_string(); let code = CellValue::Image(image.clone()); - let run = CodeRun { + let code_run = CodeRun { std_out: None, std_err: None, formatted_code_string: None, - last_modified: Utc::now(), cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Single(CellValue::Image(image.clone()))), + error: None, return_type: Some("image".into()), - spill_error: false, line_number: None, output_type: None, }; - sheet.set_data_table(pos, Some(run)); + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(CellValue::Image(image.clone())), + spill_error: false, + last_modified: Utc::now(), + }; + sheet.set_data_table(pos, Some(data_table)); sheet.set_cell_value(pos, code); sheet.send_all_images(); expect_js_call( diff --git a/quadratic-core/src/grid/sheet/search.rs b/quadratic-core/src/grid/sheet/search.rs index bbc15b3f67..26826cfaa5 100644 --- a/quadratic-core/src/grid/sheet/search.rs +++ b/quadratic-core/src/grid/sheet/search.rs @@ -1,6 +1,6 @@ -use crate::{grid::CodeRunResult, CellValue, Pos, SheetPos, Value}; - use super::Sheet; +use crate::{CellValue, Pos, SheetPos, Value}; + use serde::{Deserialize, Serialize}; #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Eq)] @@ -116,47 +116,44 @@ impl Sheet { let mut results = vec![]; self.data_tables .iter() - .for_each(|(pos, data_table)| match &data_table.result { - CodeRunResult::Ok(value) => match value { - Value::Single(v) => { - if self.compare_cell_value( - v, - query, - *pos, - case_sensitive, - whole_cell, - false, // data_tables can never have code within them (although that would be cool if they did ;) - ) { - results.push(pos.to_sheet_pos(self.id)); - } + .for_each(|(pos, data_table)| match &data_table.value { + Value::Single(v) => { + if self.compare_cell_value( + v, + query, + *pos, + case_sensitive, + whole_cell, + false, // data_tables can never have code within them (although that would be cool if they did ;) + ) { + results.push(pos.to_sheet_pos(self.id)); } - Value::Array(array) => { - for y in 0..array.size().h.get() { - for x in 0..array.size().w.get() { - let cell_value = array.get(x, y).unwrap(); - if self.compare_cell_value( - cell_value, - query, - Pos { - x: pos.x + x as i64, - y: pos.y + y as i64, - }, - case_sensitive, - whole_cell, - false, // data_tables can never have code within them (although that would be cool if they did ;) - ) { - results.push(SheetPos { - x: pos.x + x as i64, - y: pos.y + y as i64, - sheet_id: self.id, - }); - } + } + Value::Array(array) => { + for y in 0..array.size().h.get() { + for x in 0..array.size().w.get() { + let cell_value = array.get(x, y).unwrap(); + if self.compare_cell_value( + cell_value, + query, + Pos { + x: pos.x + x as i64, + y: pos.y + y as i64, + }, + case_sensitive, + whole_cell, + false, // data_tables can never have code within them (although that would be cool if they did ;) + ) { + results.push(SheetPos { + x: pos.x + x as i64, + y: pos.y + y as i64, + sheet_id: self.id, + }); } } } - Value::Tuple(_) => {} // Tuples are not spilled onto the grid - }, - CodeRunResult::Err(_) => (), + } + Value::Tuple(_) => {} // Tuples are not spilled onto the grid); }); results } @@ -196,7 +193,7 @@ mod test { use crate::{ controller::GridController, - grid::{CodeCellLanguage, CodeRun}, + grid::{CodeCellLanguage, CodeRun, DataTable, DataTableKind}, Array, CodeCellValue, }; @@ -463,17 +460,21 @@ mod test { ); let code_run = CodeRun { formatted_code_string: None, - result: CodeRunResult::Ok(Value::Single("world".into())), + error: None, std_out: None, std_err: None, cells_accessed: HashSet::new(), - spill_error: false, return_type: None, line_number: None, output_type: None, + }; + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single("world".into()), + spill_error: false, last_modified: Utc::now(), }; - sheet.set_data_table(Pos { x: 1, y: 2 }, Some(code_run)); + sheet.set_data_table(Pos { x: 1, y: 2 }, Some(data_table)); let results = sheet.search( &"hello".into(), @@ -502,20 +503,24 @@ mod test { let mut sheet = Sheet::test(); let code_run = CodeRun { formatted_code_string: None, - result: CodeRunResult::Ok(Value::Array(Array::from(vec![ - vec!["abc", "def", "ghi"], - vec!["jkl", "mno", "pqr"], - ]))), + error: None, std_out: None, std_err: None, cells_accessed: HashSet::new(), - spill_error: false, return_type: None, line_number: None, output_type: None, + }; + let data_table = DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(Array::from(vec![ + vec!["abc", "def", "ghi"], + vec!["jkl", "mno", "pqr"], + ])), + spill_error: false, last_modified: Utc::now(), }; - sheet.set_data_table(Pos { x: 1, y: 2 }, Some(code_run)); + sheet.set_data_table(Pos { x: 1, y: 2 }, Some(data_table)); let results = sheet.search( &"abc".into(), diff --git a/quadratic-core/src/grid/sheet/selection.rs b/quadratic-core/src/grid/sheet/selection.rs index 5c5704ad71..0b0165a1ed 100644 --- a/quadratic-core/src/grid/sheet/selection.rs +++ b/quadratic-core/src/grid/sheet/selection.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use indexmap::IndexMap; use crate::{ - grid::{formats::format::Format, CodeRunResult, GridBounds}, + grid::{formats::format::Format, GridBounds}, selection::Selection, CellValue, Pos, Rect, Value, }; @@ -70,34 +70,19 @@ impl Sheet { } if !skip_code_runs { for (pos, code_run) in self.data_tables.iter() { - match code_run.result { - CodeRunResult::Ok(ref value) => match value { - Value::Single(v) => { - count += 1; - if count >= max_count.unwrap_or(i64::MAX) { - return None; - } - cells.insert(*pos, v); + match &code_run.value { + Value::Single(v) => { + count += 1; + if count >= max_count.unwrap_or(i64::MAX) { + return None; } - Value::Array(a) => { - for x in 0..a.width() { - for y in 0..a.height() { - if let Ok(entry) = a.get(x, y) { - if include_blanks || !matches!(entry, &CellValue::Blank) - { - count += 1; - if count >= max_count.unwrap_or(i64::MAX) { - return None; - } - cells.insert( - Pos { - x: x as i64 + pos.x, - y: y as i64 + pos.y, - }, - entry, - ); - } - } else if include_blanks { + cells.insert(*pos, &v); + } + Value::Array(a) => { + for x in 0..a.width() { + for y in 0..a.height() { + if let Ok(entry) = a.get(x, y) { + if include_blanks || !matches!(entry, &CellValue::Blank) { count += 1; if count >= max_count.unwrap_or(i64::MAX) { return None; @@ -107,15 +92,26 @@ impl Sheet { x: x as i64 + pos.x, y: y as i64 + pos.y, }, - &CellValue::Blank, + entry, ); } + } else if include_blanks { + count += 1; + if count >= max_count.unwrap_or(i64::MAX) { + return None; + } + cells.insert( + Pos { + x: x as i64 + pos.x, + y: y as i64 + pos.y, + }, + &CellValue::Blank, + ); } } } - Value::Tuple(_) => {} // Tuples are not spilled onto the grid - }, - CodeRunResult::Err(_) => {} + } + Value::Tuple(_) => {} // Tuples are not spilled onto the grid } } } diff --git a/quadratic-core/src/grid/sheet/sheet_test.rs b/quadratic-core/src/grid/sheet/sheet_test.rs index 00c50f84a7..adfac4eb10 100644 --- a/quadratic-core/src/grid/sheet/sheet_test.rs +++ b/quadratic-core/src/grid/sheet/sheet_test.rs @@ -1,13 +1,18 @@ use super::Sheet; +use crate::{ + grid::{ + formats::format_update::FormatUpdate, CodeCellLanguage, CodeRun, DataTable, DataTableKind, + }, + Array, ArraySize, CellValue, CodeCellValue, Pos, Value, +}; +use bigdecimal::BigDecimal; +use chrono::Utc; +use std::{collections::HashSet, str::FromStr}; + impl Sheet { /// Sets a test value in the sheet of &str converted to a BigDecimal. - #[cfg(test)] pub fn test_set_value_number(&mut self, x: i64, y: i64, s: &str) { - use crate::{CellValue, Pos}; - use bigdecimal::BigDecimal; - use std::str::FromStr; - if s.is_empty() { return; } @@ -22,7 +27,6 @@ impl Sheet { /// Sets values in a rectangle starting at (x, y) with width w and height h. /// Rectangle is formed row first (so for x then for y). - #[cfg(test)] pub fn test_set_values(&mut self, x: i64, y: i64, w: i64, h: i64, s: Vec<&str>) { assert!( w * h == s.len() as i64, @@ -39,38 +43,36 @@ impl Sheet { self.calculate_bounds(); } - #[cfg(test)] - pub fn test_set_format( - &mut self, - x: i64, - y: i64, - update: crate::grid::formats::format_update::FormatUpdate, - ) { + pub fn test_set_format(&mut self, x: i64, y: i64, update: FormatUpdate) { self.set_format_cell(crate::grid::Pos { x, y }, &update, true); } /// Sets a code run and CellValue::Code with an empty code string, a single value result. - #[cfg(test)] pub fn test_set_code_run_single(&mut self, x: i64, y: i64, value: crate::grid::CellValue) { self.set_cell_value( crate::Pos { x, y }, - crate::CellValue::Code(crate::CodeCellValue { - language: crate::grid::CodeCellLanguage::Formula, + CellValue::Code(CodeCellValue { + language: CodeCellLanguage::Formula, code: "".to_string(), }), ); + let code_run = CodeRun { + std_out: None, + std_err: None, + formatted_code_string: None, + cells_accessed: std::collections::HashSet::new(), + error: None, + return_type: Some("number".into()), + line_number: None, + output_type: None, + }; + self.set_data_table( crate::Pos { x, y }, - Some(crate::grid::CodeRun { - std_out: None, - std_err: None, - formatted_code_string: None, - cells_accessed: std::collections::HashSet::new(), - result: crate::grid::CodeRunResult::Ok(crate::Value::Single(value)), - return_type: Some("number".into()), - line_number: None, - output_type: None, + Some(DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Single(value), spill_error: false, last_modified: chrono::Utc::now(), }), @@ -78,28 +80,12 @@ impl Sheet { } /// Sets a code run and CellValue::Code with an empty code string and a single value BigDecimal::from_str(n) result. - #[cfg(test)] pub fn test_set_code_run_number(&mut self, x: i64, y: i64, n: &str) { - use std::str::FromStr; - - self.test_set_code_run_single( - x, - y, - crate::grid::CellValue::Number(bigdecimal::BigDecimal::from_str(n).unwrap()), - ); + self.test_set_code_run_single(x, y, CellValue::Number(BigDecimal::from_str(n).unwrap())); } /// Sets a code run array with code string of "" and an array output of the given values. - #[cfg(test)] pub fn test_set_code_run_array(&mut self, x: i64, y: i64, n: Vec<&str>, vertical: bool) { - use crate::{ - grid::{CodeCellLanguage, CodeRun, CodeRunResult}, - Array, ArraySize, CellValue, CodeCellValue, Pos, Value, - }; - use bigdecimal::BigDecimal; - use chrono::Utc; - use std::{collections::HashSet, str::FromStr}; - let array_size = if vertical { ArraySize::new(1, n.len() as u32).unwrap() } else { @@ -127,33 +113,30 @@ impl Sheet { code: "code".to_string(), }), ); + + let code_run = CodeRun { + std_out: None, + std_err: None, + formatted_code_string: None, + cells_accessed: HashSet::new(), + error: None, + return_type: Some("number".into()), + line_number: None, + output_type: None, + }; + self.set_data_table( Pos { x, y }, - Some(CodeRun { - std_out: None, - std_err: None, - formatted_code_string: None, - cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Array(array)), - return_type: Some("number".into()), - line_number: None, - output_type: None, + Some(DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(array), spill_error: false, last_modified: Utc::now(), }), ); } - #[cfg(test)] pub fn test_set_code_run_array_2d(&mut self, x: i64, y: i64, w: u32, h: u32, n: Vec<&str>) { - use crate::{ - grid::{CodeCellLanguage, CodeRun, CodeRunResult}, - Array, ArraySize, CellValue, CodeCellValue, Pos, Value, - }; - use bigdecimal::BigDecimal; - use chrono::Utc; - use std::{collections::HashSet, str::FromStr}; - self.set_cell_value( Pos { x, y }, CellValue::Code(CodeCellValue { @@ -175,17 +158,22 @@ impl Sheet { } } + let code_run = CodeRun { + std_out: None, + std_err: None, + formatted_code_string: None, + cells_accessed: HashSet::new(), + error: None, + return_type: Some("number".into()), + line_number: None, + output_type: None, + }; + self.set_data_table( Pos { x, y }, - Some(CodeRun { - std_out: None, - std_err: None, - formatted_code_string: None, - cells_accessed: HashSet::new(), - result: CodeRunResult::Ok(Value::Array(array)), - return_type: Some("number".into()), - line_number: None, - output_type: None, + Some(DataTable { + kind: DataTableKind::CodeRun(code_run), + value: Value::Array(array), spill_error: false, last_modified: Utc::now(), }), diff --git a/quadratic-core/src/values/array_size.rs b/quadratic-core/src/values/array_size.rs index d74e1c0660..9a968f37ce 100644 --- a/quadratic-core/src/values/array_size.rs +++ b/quadratic-core/src/values/array_size.rs @@ -49,6 +49,15 @@ impl TryFrom<(u32, u32)> for ArraySize { Self::new_or_err(w, h) } } +impl TryFrom<(i64, i64)> for ArraySize { + type Error = RunErrorMsg; + + fn try_from((w, h): (i64, i64)) -> Result { + let w = w.try_into().map_err(|_| RunErrorMsg::ArrayTooBig)?; + let h = h.try_into().map_err(|_| RunErrorMsg::ArrayTooBig)?; + Self::new_or_err(w, h) + } +} // TODO(ddimaria):`[][0]` is now being detected by clippy, fix this #[allow(clippy::out_of_bounds_indexing)] @@ -139,3 +148,20 @@ impl Axis { ) } } +impl From for Axis { + fn from(val: i8) -> Self { + match val { + 0 => Axis::X, + 1 => Axis::Y, + _ => unreachable!(), + } + } +} +impl Into for Axis { + fn into(self) -> i8 { + match self { + Axis::X => 0, + Axis::Y => 1, + } + } +} From d1f414603f58aebd886e7f65056042970dfcafae Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 27 Sep 2024 10:22:13 -0600 Subject: [PATCH 004/373] Fix broken tests --- .../src/controller/execution/run_code/mod.rs | 19 +++++++++++-------- .../execution/run_code/run_formula.rs | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index bb6a99c649..b325e89da8 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -273,6 +273,7 @@ impl GridController { last_modified: Utc::now(), }; }; + let value = if js_code_result.success { let result = if let Some(array_output) = js_code_result.output_array { let (array, ops) = Array::from_string_list(start.into(), sheet, array_output); @@ -299,7 +300,7 @@ impl GridController { Value::Single(CellValue::Blank) // TODO(ddimaria): this will eventually be an empty vec }; - let error = { + let error = (!js_code_result.success).then_some({ let error_msg = js_code_result .std_err .clone() @@ -310,17 +311,19 @@ impl GridController { end: line_number, }); RunError { span, msg } - }; + }); - let return_type = match value { - Value::Single(ref cell_value) => Some(cell_value.type_name().into()), - Value::Array(_) => Some("array".into()), - Value::Tuple(_) => Some("tuple".into()), - }; + let return_type = js_code_result.success.then_some({ + match value { + Value::Single(ref cell_value) => cell_value.type_name().into(), + Value::Array(_) => "array".into(), + Value::Tuple(_) => "tuple".into(), + } + }); let code_run = CodeRun { formatted_code_string: None, - error: Some(error), + error, return_type, line_number: js_code_result.line_number, output_type: js_code_result.output_display_type, diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index 9e04242986..c16cd01f45 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -255,7 +255,7 @@ mod test { std_err: None, formatted_code_string: None, cells_accessed: HashSet::new(), - return_type: None, + return_type: Some("number".into()), line_number: None, output_type: None, error: None, From 88eb67e9ec20193f362574aebce353b326ee6ed9 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 27 Sep 2024 10:43:37 -0600 Subject: [PATCH 005/373] Clippy lints --- .../src/controller/execution/run_code/mod.rs | 9 ++--- quadratic-core/src/grid/file/v1_7/schema.rs | 6 +-- quadratic-core/src/grid/sheet/rendering.rs | 40 +++++++++---------- quadratic-core/src/grid/sheet/selection.rs | 2 +- quadratic-core/src/values/array_size.rs | 6 +-- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index b325e89da8..f6da63e05f 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -198,8 +198,7 @@ impl GridController { let code_run = sheet .data_table(pos) - .map(|data_table| data_table.code_run()) - .flatten(); + .and_then(|data_table| data_table.code_run()); let new_code_run = match code_run { Some(old_code_run) => { @@ -275,7 +274,8 @@ impl GridController { }; let value = if js_code_result.success { - let result = if let Some(array_output) = js_code_result.output_array { + + if let Some(array_output) = js_code_result.output_array { let (array, ops) = Array::from_string_list(start.into(), sheet, array_output); transaction.reverse_operations.extend(ops); if let Some(array) = array { @@ -294,8 +294,7 @@ impl GridController { Value::Single(cell_value) } else { Value::Single(CellValue::Blank) - }; - result + } } else { Value::Single(CellValue::Blank) // TODO(ddimaria): this will eventually be an empty vec }; diff --git a/quadratic-core/src/grid/file/v1_7/schema.rs b/quadratic-core/src/grid/file/v1_7/schema.rs index b495ae75c1..4c6994b28a 100644 --- a/quadratic-core/src/grid/file/v1_7/schema.rs +++ b/quadratic-core/src/grid/file/v1_7/schema.rs @@ -159,9 +159,9 @@ impl From for AxisSchema { } } -impl Into for AxisSchema { - fn into(self) -> i8 { - match self { +impl From for i8 { + fn from(val: AxisSchema) -> Self { + match val { AxisSchema::X => 0, AxisSchema::Y => 1, } diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 09bc3aa151..aab77dd359 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -383,19 +383,17 @@ impl Sheet { output_size.h.get(), Some(reasons), ) + } else if data_table.has_error() + || matches!(data_table.value, Value::Single(CellValue::Error(_))) + { + (JsRenderCodeCellState::RunError, 1, 1, None) } else { - if data_table.has_error() - || matches!(data_table.value, Value::Single(CellValue::Error(_))) - { - (JsRenderCodeCellState::RunError, 1, 1, None) - } else { - ( - JsRenderCodeCellState::Success, - output_size.w.get(), - output_size.h.get(), - None, - ) - } + ( + JsRenderCodeCellState::Success, + output_size.w.get(), + output_size.h.get(), + None, + ) }; Some(JsRenderCodeCell { x: pos.x as i32, @@ -431,17 +429,15 @@ impl Sheet { output_size.h.get(), Some(reasons), ) + } else if data_table.has_error() { + (JsRenderCodeCellState::RunError, 1, 1, None) } else { - if data_table.has_error() { - (JsRenderCodeCellState::RunError, 1, 1, None) - } else { - ( - JsRenderCodeCellState::Success, - output_size.w.get(), - output_size.h.get(), - None, - ) - } + ( + JsRenderCodeCellState::Success, + output_size.w.get(), + output_size.h.get(), + None, + ) }; Some(JsRenderCodeCell { x: pos.x as i32, diff --git a/quadratic-core/src/grid/sheet/selection.rs b/quadratic-core/src/grid/sheet/selection.rs index 0b0165a1ed..def54564e0 100644 --- a/quadratic-core/src/grid/sheet/selection.rs +++ b/quadratic-core/src/grid/sheet/selection.rs @@ -76,7 +76,7 @@ impl Sheet { if count >= max_count.unwrap_or(i64::MAX) { return None; } - cells.insert(*pos, &v); + cells.insert(*pos, v); } Value::Array(a) => { for x in 0..a.width() { diff --git a/quadratic-core/src/values/array_size.rs b/quadratic-core/src/values/array_size.rs index 9a968f37ce..c7d0ade1f5 100644 --- a/quadratic-core/src/values/array_size.rs +++ b/quadratic-core/src/values/array_size.rs @@ -157,9 +157,9 @@ impl From for Axis { } } } -impl Into for Axis { - fn into(self) -> i8 { - match self { +impl From for i8 { + fn from(val: Axis) -> Self { + match val { Axis::X => 0, Axis::Y => 1, } From 80c339ef394bd58649160d94e5cc89f5d3015e0f Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 27 Sep 2024 14:41:29 -0600 Subject: [PATCH 006/373] Add CellValue::Import --- quadratic-core/src/formulas/criteria.rs | 2 ++ .../src/grid/file/serialize/cell_value.rs | 7 ++++ quadratic-core/src/grid/file/v1_6/schema.rs | 6 ++++ quadratic-core/src/grid/file/v1_7/schema.rs | 1 + quadratic-core/src/values/cellvalue.rs | 32 ++++++++++++++++++- quadratic-core/src/values/convert.rs | 2 ++ 6 files changed, 49 insertions(+), 1 deletion(-) diff --git a/quadratic-core/src/formulas/criteria.rs b/quadratic-core/src/formulas/criteria.rs index be20680216..7b4eb0a925 100644 --- a/quadratic-core/src/formulas/criteria.rs +++ b/quadratic-core/src/formulas/criteria.rs @@ -32,6 +32,7 @@ impl TryFrom> for Criterion { | CellValue::Number(_) | CellValue::Html(_) | CellValue::Code(_) + | CellValue::Import(_) | CellValue::Logical(_) | CellValue::Instant(_) | CellValue::Date(_) @@ -122,6 +123,7 @@ impl Criterion { CellValue::Html(_) => false, CellValue::Code(_) => false, CellValue::Image(_) => false, + CellValue::Import(_) => false, } } diff --git a/quadratic-core/src/grid/file/serialize/cell_value.rs b/quadratic-core/src/grid/file/serialize/cell_value.rs index 3064a70770..10a7867fa0 100644 --- a/quadratic-core/src/grid/file/serialize/cell_value.rs +++ b/quadratic-core/src/grid/file/serialize/cell_value.rs @@ -1,5 +1,6 @@ use super::current; use crate::{ + cellvalue::Import, grid::{CodeCellLanguage, ConnectionKind}, CellValue, CodeCellValue, }; @@ -40,6 +41,9 @@ pub fn export_cell_value(cell_value: CellValue) -> current::CellValueSchema { current::CellValueSchema::Error(current::RunErrorSchema::from_grid_run_error(*error)) } CellValue::Image(image) => current::CellValueSchema::Image(image), + CellValue::Import(import) => current::CellValueSchema::Import(current::ImportSchema { + file_name: import.file_name, + }), } } @@ -91,6 +95,9 @@ pub fn import_cell_value(value: current::CellValueSchema) -> CellValue { current::CellValueSchema::DateTime(dt) => CellValue::DateTime(dt), current::CellValueSchema::Error(error) => CellValue::Error(Box::new(error.into())), current::CellValueSchema::Image(text) => CellValue::Image(text), + current::CellValueSchema::Import(import) => { + CellValue::Import(Import::new(import.file_name)) + } } } diff --git a/quadratic-core/src/grid/file/v1_6/schema.rs b/quadratic-core/src/grid/file/v1_6/schema.rs index 21fce63f1a..411215b070 100644 --- a/quadratic-core/src/grid/file/v1_6/schema.rs +++ b/quadratic-core/src/grid/file/v1_6/schema.rs @@ -218,6 +218,11 @@ pub struct Column { pub strike_through: HashMap>, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Import { + pub file_name: String, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum CellValue { Blank, @@ -233,6 +238,7 @@ pub enum CellValue { Duration(String), Error(RunError), Image(String), + Import(Import), } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/quadratic-core/src/grid/file/v1_7/schema.rs b/quadratic-core/src/grid/file/v1_7/schema.rs index 4c6994b28a..01e55370bb 100644 --- a/quadratic-core/src/grid/file/v1_7/schema.rs +++ b/quadratic-core/src/grid/file/v1_7/schema.rs @@ -35,6 +35,7 @@ pub type RenderSizeSchema = v1_6::RenderSize; pub type RunErrorMsgSchema = v1_6::RunErrorMsg; pub type AxisSchema = v1_6::Axis; pub type SpanSchema = v1_6::Span; +pub type ImportSchema = v1_6::Import; pub type SelectionSchema = v1_6_validation::Selection; diff --git a/quadratic-core/src/values/cellvalue.rs b/quadratic-core/src/values/cellvalue.rs index d7960e5cec..6a69d00e8f 100644 --- a/quadratic-core/src/values/cellvalue.rs +++ b/quadratic-core/src/values/cellvalue.rs @@ -1,4 +1,7 @@ -use std::{fmt, str::FromStr}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; use anyhow::Result; use bigdecimal::{BigDecimal, Signed, ToPrimitive, Zero}; @@ -22,6 +25,23 @@ pub struct CodeCellValue { pub code: String, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Import { + pub file_name: String, +} + +impl Display for Import { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Import({})", self.file_name) + } +} + +impl Import { + pub fn new(file_name: String) -> Self { + Self { file_name } + } +} + /// Non-array value in the formula language. #[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] @@ -61,6 +81,8 @@ pub enum CellValue { Code(CodeCellValue), #[cfg_attr(test, proptest(skip))] Image(String), + #[cfg_attr(test, proptest(skip))] + Import(Import), } impl fmt::Display for CellValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -79,6 +101,7 @@ impl fmt::Display for CellValue { CellValue::Html(s) => write!(f, "{}", s), CellValue::Code(code) => write!(f, "{:?}", code), CellValue::Image(s) => write!(f, "{}", s), + CellValue::Import(import) => write!(f, "{:?}", import), } } } @@ -107,6 +130,7 @@ impl CellValue { CellValue::Date(_) => "date", CellValue::Time(_) => "time", CellValue::DateTime(_) => "date time", + CellValue::Import(_) => "import", } } /// Returns a formula-source-code representation of the value. @@ -126,6 +150,7 @@ impl CellValue { CellValue::Date(d) => d.to_string(), CellValue::Time(d) => d.to_string(), CellValue::DateTime(d) => d.to_string(), + CellValue::Import(import) => import.to_string(), } } @@ -173,6 +198,7 @@ impl CellValue { CellValue::Date(d) => d.format(DEFAULT_DATE_FORMAT).to_string(), CellValue::Time(d) => d.format(DEFAULT_TIME_FORMAT).to_string(), CellValue::DateTime(d) => d.format(DEFAULT_DATE_TIME_FORMAT).to_string(), + CellValue::Import(import) => import.to_string(), // these should not render CellValue::Code(_) => String::new(), @@ -277,6 +303,7 @@ impl CellValue { CellValue::Duration(d) => d.to_string(), CellValue::Error(_) => "[error]".to_string(), + CellValue::Import(import) => import.to_string(), // this should not be editable CellValue::Code(_) => String::new(), @@ -299,6 +326,7 @@ impl CellValue { CellValue::DateTime(t) => t.format("%Y-%m-%dT%H:%M:%S%.3f").to_string(), CellValue::Duration(d) => d.to_string(), CellValue::Error(_) => "[error]".to_string(), + CellValue::Import(import) => import.to_string(), // these should not return a value CellValue::Code(_) => String::new(), @@ -423,6 +451,7 @@ impl CellValue { | (CellValue::Html(_), _) | (CellValue::Code(_), _) | (CellValue::Image(_), _) + | (CellValue::Import(_), _) | (CellValue::Blank, _) => return Ok(None), })) } @@ -449,6 +478,7 @@ impl CellValue { CellValue::Date(_) => 10, CellValue::Time(_) => 11, CellValue::DateTime(_) => 12, + CellValue::Import(_) => 13, } } diff --git a/quadratic-core/src/values/convert.rs b/quadratic-core/src/values/convert.rs index 29a087b2a9..8d5b2a56b1 100644 --- a/quadratic-core/src/values/convert.rs +++ b/quadratic-core/src/values/convert.rs @@ -132,6 +132,7 @@ impl<'a> TryFrom<&'a CellValue> for String { CellValue::Html(s) => Ok(s.clone()), CellValue::Code(_) => Ok(String::new()), CellValue::Image(_) => Ok(String::new()), + CellValue::Import(_) => Ok(String::new()), } } } @@ -175,6 +176,7 @@ impl<'a> TryFrom<&'a CellValue> for f64 { CellValue::Html(_) => Ok(0.0), CellValue::Code(_) => Ok(0.0), CellValue::Image(_) => Ok(0.0), + CellValue::Import(_) => Ok(0.0), } } } From 6f686b718402ae39aab3a235dfd8648f50a124c9 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 27 Sep 2024 16:53:16 -0600 Subject: [PATCH 007/373] Implement CellValue::Import for CSV imports --- .../src/controller/operations/import.rs | 61 +++++++++++++++---- quadratic-core/src/grid/data_table.rs | 4 ++ .../src/grid/file/serialize/data_table.rs | 13 ++++ quadratic-core/src/grid/file/v1_7/schema.rs | 1 + 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 19236c2e8c..3ad5cf8388 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -1,13 +1,17 @@ -use std::io::Cursor; +use std::{borrow::Cow, io::Cursor}; use anyhow::{anyhow, bail, Result}; -use chrono::{NaiveDate, NaiveTime}; +use chrono::{NaiveDate, NaiveTime, Utc}; use crate::{ cell_values::CellValues, + cellvalue::Import, controller::GridController, - grid::{file::sheet_schema::export_sheet, CodeCellLanguage, Sheet, SheetId}, - CellValue, CodeCellValue, Pos, SheetPos, + grid::{ + file::sheet_schema::export_sheet, CodeCellLanguage, DataTable, DataTableKind, Sheet, + SheetId, + }, + Array, ArraySize, CellValue, CodeCellValue, Pos, SheetPos, Value, }; use bytes::Bytes; use calamine::{Data as ExcelData, Reader as ExcelReader, Xlsx, XlsxError}; @@ -29,8 +33,8 @@ impl GridController { ) -> Result> { let error = |message: String| anyhow!("Error parsing CSV file {}: {}", file_name, message); let file: &[u8] = match String::from_utf8_lossy(&file) { - std::borrow::Cow::Borrowed(_) => &file, - std::borrow::Cow::Owned(_) => { + Cow::Borrowed(_) => &file, + Cow::Owned(_) => { if let Some(utf) = read_utf16(&file) { return self.import_csv_operations( sheet_id, @@ -61,7 +65,7 @@ impl GridController { // then create operations using MAXIMUM_IMPORT_LINES to break up the SetCellValues operations let mut ops = vec![] as Vec; - let mut cell_values = CellValues::new(width, height.min(IMPORT_LINES_PER_OPERATION)); + let mut cell_values = Array::new_empty(ArraySize::new(width, height).unwrap()); let mut current_y = 0; let mut y: u32 = 0; for entry in reader.records() { @@ -78,24 +82,46 @@ impl GridController { value, ); ops.extend(operations); - cell_values.set(x as u32, y, cell_value); + // cell_values.set(x as u32, y, cell_value.clone()); + cell_values.set(x as u32, y, cell_value).unwrap(); } } } + y += 1; + if y >= IMPORT_LINES_PER_OPERATION { - ops.push(Operation::SetCellValues { + // ops.push(Operation::SetCellValues { + // sheet_pos: SheetPos { + // x: insert_at.x, + // y: insert_at.y + current_y as i64, + // sheet_id, + // }, + // values: cell_values, + // }); + let data_table = DataTable { + kind: DataTableKind::Import(Import { + file_name: file_name.to_string(), + }), + value: Value::Array(cell_values), + spill_error: false, + last_modified: Utc::now(), + }; + ops.push(Operation::SetCodeRun { sheet_pos: SheetPos { x: insert_at.x, y: insert_at.y + current_y as i64, sheet_id, }, - values: cell_values, + code_run: Some(data_table), + index: y as usize, }); + current_y += y; y = 0; + let h = (height - current_y).min(IMPORT_LINES_PER_OPERATION); - cell_values = CellValues::new(width, h); + cell_values = Array::new_empty(ArraySize::new(width, h).unwrap()); // update the progress bar every time there's a new operation if cfg!(target_family = "wasm") || cfg!(test) { @@ -113,13 +139,22 @@ impl GridController { } // finally add the final operation - ops.push(Operation::SetCellValues { + let data_table = DataTable { + kind: DataTableKind::Import(Import { + file_name: file_name.to_string(), + }), + value: Value::Array(cell_values), + spill_error: false, + last_modified: Utc::now(), + }; + ops.push(Operation::SetCodeRun { sheet_pos: SheetPos { x: insert_at.x, y: insert_at.y + current_y as i64, sheet_id, }, - values: cell_values, + code_run: Some(data_table), + index: y as usize, }); Ok(ops) } diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 29f8685eb3..5a61a2a940 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -4,6 +4,7 @@ //! any given CellValue::Code type (ie, if it doesn't exist then a run hasn't been //! performed yet). +use crate::cellvalue::Import; use crate::grid::CodeRun; use crate::{ArraySize, CellValue, Pos, Rect, RunError, RunErrorMsg, SheetPos, SheetRect, Value}; use chrono::{DateTime, Utc}; @@ -18,6 +19,7 @@ struct DataTableColumn { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum DataTableKind { CodeRun(CodeRun), + Import(Import), } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -32,12 +34,14 @@ impl DataTable { pub fn code_run(&self) -> Option<&CodeRun> { match self.kind { DataTableKind::CodeRun(ref code_run) => Some(code_run), + _ => None, } } pub fn has_error(&self) -> bool { match self.kind { DataTableKind::CodeRun(ref code_run) => code_run.error.is_some(), + _ => false, } } diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 4a20f6616d..46a7a98c81 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -157,6 +157,11 @@ pub(crate) fn import_data_table_builder( current::DataTableKindSchema::CodeRun(code_run) => { DataTableKind::CodeRun(import_code_run_builder(code_run)?) } + current::DataTableKindSchema::Import(import) => { + DataTableKind::Import(crate::cellvalue::Import { + file_name: import.file_name, + }) + } }, last_modified: data_table.last_modified.unwrap_or(Utc::now()), // this is required but fall back to now if failed spill_error: data_table.spill_error, @@ -319,6 +324,14 @@ pub(crate) fn export_data_table_runs( value, } } + DataTableKind::Import(import) => current::DataTableSchema { + kind: current::DataTableKindSchema::Import(current::ImportSchema { + file_name: import.file_name, + }), + last_modified: Some(data_table.last_modified), + spill_error: data_table.spill_error, + value, + }, }; (current::PosSchema::from(pos), data_table) diff --git a/quadratic-core/src/grid/file/v1_7/schema.rs b/quadratic-core/src/grid/file/v1_7/schema.rs index 01e55370bb..76ff2f6a06 100644 --- a/quadratic-core/src/grid/file/v1_7/schema.rs +++ b/quadratic-core/src/grid/file/v1_7/schema.rs @@ -140,6 +140,7 @@ pub struct CodeRunSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum DataTableKindSchema { CodeRun(CodeRunSchema), + Import(ImportSchema), } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] From 983b2b0d1fa6858b974b0a03e8d06b145b05d33b Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Sat, 28 Sep 2024 08:31:07 -0600 Subject: [PATCH 008/373] Complete import csv into data table --- .../src/controller/operations/import.rs | 77 ++++++++----------- quadratic-core/src/grid/data_table.rs | 15 +++- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 3ad5cf8388..f62f7422c2 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -1,17 +1,14 @@ use std::{borrow::Cow, io::Cursor}; use anyhow::{anyhow, bail, Result}; -use chrono::{NaiveDate, NaiveTime, Utc}; +use chrono::{NaiveDate, NaiveTime}; use crate::{ cell_values::CellValues, cellvalue::Import, controller::GridController, - grid::{ - file::sheet_schema::export_sheet, CodeCellLanguage, DataTable, DataTableKind, Sheet, - SheetId, - }, - Array, ArraySize, CellValue, CodeCellValue, Pos, SheetPos, Value, + grid::{file::sheet_schema::export_sheet, CodeCellLanguage, DataTable, Sheet, SheetId}, + Array, ArraySize, CellValue, CodeCellValue, Pos, SheetPos, }; use bytes::Bytes; use calamine::{Data as ExcelData, Reader as ExcelReader, Xlsx, XlsxError}; @@ -68,6 +65,7 @@ impl GridController { let mut cell_values = Array::new_empty(ArraySize::new(width, height).unwrap()); let mut current_y = 0; let mut y: u32 = 0; + for entry in reader.records() { match entry { Err(e) => return Err(error(format!("line {}: {}", current_y + y + 1, e))), @@ -91,22 +89,9 @@ impl GridController { y += 1; if y >= IMPORT_LINES_PER_OPERATION { - // ops.push(Operation::SetCellValues { - // sheet_pos: SheetPos { - // x: insert_at.x, - // y: insert_at.y + current_y as i64, - // sheet_id, - // }, - // values: cell_values, - // }); - let data_table = DataTable { - kind: DataTableKind::Import(Import { - file_name: file_name.to_string(), - }), - value: Value::Array(cell_values), - spill_error: false, - last_modified: Utc::now(), - }; + let import = Import::new(file_name.into()); + let data_table = DataTable::from((import, cell_values)); + ops.push(Operation::SetCodeRun { sheet_pos: SheetPos { x: insert_at.x, @@ -139,14 +124,9 @@ impl GridController { } // finally add the final operation - let data_table = DataTable { - kind: DataTableKind::Import(Import { - file_name: file_name.to_string(), - }), - value: Value::Array(cell_values), - spill_error: false, - last_modified: Utc::now(), - }; + let import = Import::new(file_name.into()); + let data_table = DataTable::from((import, cell_values)); + ops.push(Operation::SetCodeRun { sheet_pos: SheetPos { x: insert_at.x, @@ -156,6 +136,7 @@ impl GridController { code_run: Some(data_table), index: y as usize, }); + Ok(ops) } @@ -460,23 +441,27 @@ mod test { "smallpop.csv", pos, ); + + let values = vec![ + vec!["city", "Southborough"], + vec!["region", "MA"], + vec!["country", "United States"], + vec!["population", "a lot of people"], + ]; + let import = Import::new("smallpop.csv".into()); + let data_table = DataTable::from((import, values.into())); + let expected = Operation::SetCodeRun { + sheet_pos: SheetPos { + x: 0, + y: 0, + sheet_id, + }, + code_run: Some(data_table), + index: 2, + }; + assert_eq!(ops.as_ref().unwrap().len(), 1); - assert_eq!( - ops.unwrap()[0], - Operation::SetCellValues { - sheet_pos: SheetPos { - x: 0, - y: 0, - sheet_id - }, - values: CellValues::from(vec![ - vec!["city", "Southborough"], - vec!["region", "MA"], - vec!["country", "United States"], - vec!["population", "a lot of people"] - ]), - } - ); + assert_eq!(ops.unwrap()[0], expected); } #[test] diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 5a61a2a940..026665a9cb 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -6,7 +6,9 @@ use crate::cellvalue::Import; use crate::grid::CodeRun; -use crate::{ArraySize, CellValue, Pos, Rect, RunError, RunErrorMsg, SheetPos, SheetRect, Value}; +use crate::{ + Array, ArraySize, CellValue, Pos, Rect, RunError, RunErrorMsg, SheetPos, SheetRect, Value, +}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -30,6 +32,17 @@ pub struct DataTable { pub last_modified: DateTime, } +impl From<(Import, Array)> for DataTable { + fn from((import, cell_values): (Import, Array)) -> Self { + DataTable { + kind: DataTableKind::Import(import), + value: Value::Array(cell_values), + spill_error: false, + last_modified: Utc::now(), + } + } +} + impl DataTable { pub fn code_run(&self) -> Option<&CodeRun> { match self.kind { From 5e1dffe521fed17ea6268f358ca7712a5f44abb6 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 1 Oct 2024 06:31:02 -0600 Subject: [PATCH 009/373] Checkpoint --- docker/postgres/.gitignore | 0 package-lock.json | 10 +-- .../pending_transaction.rs | 15 +++- .../src/controller/execution/run_code/mod.rs | 1 - .../src/controller/operations/import.rs | 15 +++- quadratic-core/src/grid/sheet.rs | 11 +-- quadratic-core/src/grid/sheet/code.rs | 82 +++++++++++++---- quadratic-core/src/grid/sheet/col_row/row.rs | 1 + quadratic-core/src/grid/sheet/rendering.rs | 87 +++++++++++++++++-- quadratic-core/src/values/cellvalue.rs | 2 +- 10 files changed, 183 insertions(+), 41 deletions(-) delete mode 100644 docker/postgres/.gitignore diff --git a/docker/postgres/.gitignore b/docker/postgres/.gitignore deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/package-lock.json b/package-lock.json index 484dbb4032..c223aee18d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quadratic", - "version": "0.5.1", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quadratic", - "version": "0.5.1", + "version": "0.5.2", "workspaces": [ "quadratic-api", "quadratic-shared", @@ -27642,7 +27642,7 @@ } }, "quadratic-api": { - "version": "0.5.1", + "version": "0.5.2", "hasInstallScript": true, "dependencies": { "@anthropic-ai/sdk": "^0.27.2", @@ -27719,7 +27719,7 @@ } }, "quadratic-client": { - "version": "0.5.1", + "version": "0.5.2", "dependencies": { "@amplitude/analytics-browser": "^1.9.4", "@auth0/auth0-spa-js": "^2.1.0", @@ -27996,7 +27996,7 @@ }, "quadratic-rust-client": {}, "quadratic-shared": { - "version": "0.5.1", + "version": "0.5.2", "license": "ISC", "dependencies": { "zod": "^3.23.8" diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index 2cce37a07a..7e1eb93266 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -212,13 +212,20 @@ impl PendingTransaction { } /// Adds a code cell, html cell and image cell to the transaction from a CodeRun - pub fn add_from_code_run(&mut self, sheet_id: SheetId, pos: Pos, code_run: &Option) { - if let Some(code_run) = &code_run { + pub fn add_from_code_run( + &mut self, + sheet_id: SheetId, + pos: Pos, + data_table: &Option, + ) { + if let Some(data_table) = &data_table { self.add_code_cell(sheet_id, pos); - if code_run.is_html() { + + if data_table.is_html() { self.add_html_cell(sheet_id, pos); } - if code_run.is_image() { + + if data_table.is_image() { self.add_image_cell(sheet_id, pos); } } diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index f6da63e05f..58b1e2d6cc 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -274,7 +274,6 @@ impl GridController { }; let value = if js_code_result.success { - if let Some(array_output) = js_code_result.output_array { let (array, ops) = Array::from_string_list(start.into(), sheet, array_output); transaction.reverse_operations.extend(ops); diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index f62f7422c2..17a613a046 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -62,7 +62,9 @@ impl GridController { // then create operations using MAXIMUM_IMPORT_LINES to break up the SetCellValues operations let mut ops = vec![] as Vec; - let mut cell_values = Array::new_empty(ArraySize::new(width, height).unwrap()); + let mut cell_values = Array::new_empty( + ArraySize::new(width, height.min(IMPORT_LINES_PER_OPERATION)).unwrap(), + ); let mut current_y = 0; let mut y: u32 = 0; @@ -137,6 +139,17 @@ impl GridController { index: y as usize, }); + let sheet_pos = SheetPos { + x: insert_at.x, + y: insert_at.y, + sheet_id, + }; + + ops.push(Operation::SetCellValues { + sheet_pos, + values: CellValues::from(CellValue::Import(Import::new(file_name.into()))), + }); + Ok(ops) } diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 1a05aa54ce..18c0ea5433 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -205,13 +205,14 @@ impl Sheet { self.columns.iter() } + pub fn iter_data_tables(&self) -> impl Iterator { + self.data_tables.iter() + } + pub fn iter_code_runs(&self) -> impl Iterator { - let result = self - .data_tables + self.data_tables .iter() - .flat_map(|(pos, data_table)| data_table.code_run().map(|code_run| (pos, code_run))); - - result + .flat_map(|(pos, data_table)| data_table.code_run().map(|code_run| (pos, code_run))) } /// Returns the cell_value at a Pos using both column.values and data_tables (i.e., what would be returned if code asked diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 88a3f41333..53bd3127dc 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -91,13 +91,15 @@ impl Sheet { } pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { + dbgjs!("iter_code_output_in_rect()"); + dbgjs!(&self.data_tables); self.data_tables .iter() - .filter_map(move |(pos, code_cell_value)| { - let output_rect = code_cell_value.output_rect(*pos, false); + .filter_map(move |(pos, data_table)| { + let output_rect = data_table.output_rect(*pos, false); output_rect .intersects(rect) - .then_some((output_rect, code_cell_value)) + .then_some((output_rect, data_table)) }) } @@ -126,15 +128,15 @@ impl Sheet { /// Used for double clicking a cell on the grid. pub fn edit_code_value(&self, pos: Pos) -> Option { let mut code_pos = pos; - let code_cell = if let Some(cell_value) = self.cell_value(pos) { + let cell_value = if let Some(cell_value) = self.cell_value(pos) { Some(cell_value) } else { self.data_tables .iter() - .find_map(|(code_cell_pos, data_table)| { - if data_table.output_rect(*code_cell_pos, false).contains(pos) { - if let Some(code_value) = self.cell_value(*code_cell_pos) { - code_pos = *code_cell_pos; + .find_map(|(data_table_pos, data_table)| { + if data_table.output_rect(*data_table_pos, false).contains(pos) { + if let Some(code_value) = self.cell_value(*data_table_pos) { + code_pos = *data_table_pos; Some(code_value) } else { None @@ -145,14 +147,16 @@ impl Sheet { }) }; - let code_cell = code_cell?; + let cell_value = cell_value?; + dbgjs!("edit_code_value()"); + dbgjs!(&cell_value); - match code_cell { - CellValue::Code(mut code_cell) => { + match cell_value { + CellValue::Code(mut cell_value) => { // replace internal cell references with a1 notation - if matches!(code_cell.language, CodeCellLanguage::Formula) { - let replaced = replace_internal_cell_references(&code_cell.code, code_pos); - code_cell.code = replaced; + if matches!(cell_value.language, CodeCellLanguage::Formula) { + let replaced = replace_internal_cell_references(&cell_value.code, code_pos); + cell_value.code = replaced; } if let Some(data_table) = self.data_table(code_pos) { @@ -176,8 +180,8 @@ impl Sheet { Some(JsCodeCell { x: code_pos.x, y: code_pos.y, - code_string: code_cell.code, - language: code_cell.language, + code_string: cell_value.code, + language: cell_value.language, std_err: code_run.std_err.clone(), std_out: code_run.std_out.clone(), evaluation_result: Some(evaluation_result), @@ -192,8 +196,50 @@ impl Sheet { Some(JsCodeCell { x: code_pos.x, y: code_pos.y, - code_string: code_cell.code, - language: code_cell.language, + code_string: cell_value.code, + language: cell_value.language, + std_err: None, + std_out: None, + evaluation_result: None, + spill_error: None, + return_info: None, + cells_accessed: None, + }) + } + } + CellValue::Import(_) => { + dbgjs!("CellValue::Import"); + dbgjs!(self.data_table(code_pos)); + if let Some(data_table) = self.data_table(code_pos) { + let evaluation_result = + serde_json::to_string(&data_table.value).unwrap_or("".into()); + let spill_error = if data_table.spill_error { + Some(self.find_spill_error_reasons( + &data_table.output_rect(code_pos, true), + code_pos, + )) + } else { + None + }; + + Some(JsCodeCell { + x: code_pos.x, + y: code_pos.y, + code_string: "".into(), + language: CodeCellLanguage::Formula, + std_err: None, + std_out: None, + evaluation_result: Some(evaluation_result), + spill_error, + return_info: None, + cells_accessed: None, + }) + } else { + Some(JsCodeCell { + x: code_pos.x, + y: code_pos.y, + code_string: "".into(), + language: CodeCellLanguage::Formula, std_err: None, std_out: None, evaluation_result: None, diff --git a/quadratic-core/src/grid/sheet/col_row/row.rs b/quadratic-core/src/grid/sheet/col_row/row.rs index e353a5c53b..0981b08966 100644 --- a/quadratic-core/src/grid/sheet/col_row/row.rs +++ b/quadratic-core/src/grid/sheet/col_row/row.rs @@ -438,6 +438,7 @@ impl Sheet { row: i64, copy_formats: CopyFormats, ) { + dbgjs!("insert_row()"); // create undo operations for the inserted column if transaction.is_user_undo_redo() { // reverse operation to delete the row (this will also shift all impacted rows) diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index aab77dd359..c238a1ffd5 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -159,6 +159,10 @@ impl Sheet { code_rect: &Rect, ) -> Vec { let mut cells = vec![]; + + dbgjs!("get_code_cells()"); + dbgjs!(&data_table); + if let CellValue::Code(code) = code { if data_table.spill_error { cells.push(self.get_render_cell( @@ -220,6 +224,63 @@ impl Sheet { } } } + + if let CellValue::Import(import) = code { + if data_table.spill_error { + cells.push(self.get_render_cell( + code_rect.min.x, + code_rect.min.y, + None, + &CellValue::Error(Box::new(RunError { + span: None, + msg: RunErrorMsg::Spill, + })), + None, + )); + } else if let Some(error) = data_table.get_error() { + cells.push(self.get_render_cell( + code_rect.min.x, + code_rect.min.y, + None, + &CellValue::Error(Box::new(error)), + None, + )); + } else { + // find overlap of code_rect into rect + let x_start = if code_rect.min.x > output_rect.min.x { + code_rect.min.x + } else { + output_rect.min.x + }; + let x_end = if code_rect.max.x > output_rect.max.x { + output_rect.max.x + } else { + code_rect.max.x + }; + let y_start = if code_rect.min.y > output_rect.min.y { + code_rect.min.y + } else { + output_rect.min.y + }; + let y_end = if code_rect.max.y > output_rect.max.y { + output_rect.max.y + } else { + code_rect.max.y + }; + for x in x_start..=x_end { + let column = self.get_column(x); + for y in y_start..=y_end { + let value = data_table.cell_value_at( + (x - code_rect.min.x) as u32, + (y - code_rect.min.y) as u32, + ); + if let Some(value) = value { + cells.push(self.get_render_cell(x, y, column, &value, None)); + } + } + } + } + } cells } @@ -234,7 +295,9 @@ impl Sheet { .for_each(|(x, column)| { column.values.range(rect.y_range()).for_each(|(y, value)| { // ignore code cells when rendering since they will be taken care in the next part - if !matches!(value, CellValue::Code(_)) { + if !matches!(value, CellValue::Code(_)) + && !matches!(value, CellValue::Import(_)) + { render_cells.push(self.get_render_cell(x, *y, Some(column), value, None)); } }); @@ -242,13 +305,22 @@ impl Sheet { // Fetch values from code cells self.iter_code_output_in_rect(rect) - .for_each(|(code_rect, code_run)| { + .for_each(|(data_table_rect, data_table)| { + dbgjs!("get_render_cells() 1"); + dbgjs!(&data_table); // sanity check that there is a CellValue::Code for this CodeRun - if let Some(code) = self.cell_value(Pos { - x: code_rect.min.x, - y: code_rect.min.y, + if let Some(cell_value) = self.cell_value(Pos { + x: data_table_rect.min.x, + y: data_table_rect.min.y, }) { - render_cells.extend(self.get_code_cells(&code, code_run, &rect, &code_rect)); + dbgjs!("get_render_cells() 2"); + dbgjs!(&data_table); + render_cells.extend(self.get_code_cells( + &cell_value, + data_table, + &rect, + &data_table_rect, + )); } }); @@ -373,8 +445,11 @@ impl Sheet { pub fn get_render_code_cell(&self, pos: Pos) -> Option { let data_table = self.data_tables.get(&pos)?; + dbgjs!("get_render_code_cell()"); + dbgjs!(&data_table); let code = self.cell_value(pos)?; let output_size = data_table.output_size(); + dbgjs!(&output_size); let (state, w, h, spill_error) = if data_table.spill_error { let reasons = self.find_spill_error_reasons(&data_table.output_rect(pos, true), pos); ( diff --git a/quadratic-core/src/values/cellvalue.rs b/quadratic-core/src/values/cellvalue.rs index 6a69d00e8f..63ae238a24 100644 --- a/quadratic-core/src/values/cellvalue.rs +++ b/quadratic-core/src/values/cellvalue.rs @@ -326,10 +326,10 @@ impl CellValue { CellValue::DateTime(t) => t.format("%Y-%m-%dT%H:%M:%S%.3f").to_string(), CellValue::Duration(d) => d.to_string(), CellValue::Error(_) => "[error]".to_string(), - CellValue::Import(import) => import.to_string(), // these should not return a value CellValue::Code(_) => String::new(), + CellValue::Import(_) => String::new(), CellValue::Image(_) => String::new(), } } From 8409b7033d35e3ef271a24e91c0c54cd68d7ca63 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 1 Oct 2024 12:23:55 -0600 Subject: [PATCH 010/373] Complete csv imports to data table --- .../execute_operation/execute_code.rs | 1 + .../src/controller/operations/import.rs | 20 ++-- quadratic-core/src/grid/code_run.rs | 1 + quadratic-core/src/grid/data_table.rs | 2 + .../src/grid/file/serialize/cell_value.rs | 2 + quadratic-core/src/grid/file/v1_6/schema.rs | 1 + quadratic-core/src/grid/sheet/code.rs | 111 ++++++------------ quadratic-core/src/grid/sheet/rendering.rs | 82 ++----------- quadratic-core/src/test_util.rs | 2 + quadratic-core/src/values/cellvalue.rs | 15 +++ 10 files changed, 79 insertions(+), 158 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs index 3fc1ec750a..e91d4a2374 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs @@ -129,6 +129,7 @@ impl GridController { CodeCellLanguage::Javascript => { self.run_javascript(transaction, sheet_pos, code); } + CodeCellLanguage::Import => {} // no-op } } } diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 17a613a046..ede1730643 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -129,16 +129,6 @@ impl GridController { let import = Import::new(file_name.into()); let data_table = DataTable::from((import, cell_values)); - ops.push(Operation::SetCodeRun { - sheet_pos: SheetPos { - x: insert_at.x, - y: insert_at.y + current_y as i64, - sheet_id, - }, - code_run: Some(data_table), - index: y as usize, - }); - let sheet_pos = SheetPos { x: insert_at.x, y: insert_at.y, @@ -150,6 +140,16 @@ impl GridController { values: CellValues::from(CellValue::Import(Import::new(file_name.into()))), }); + ops.push(Operation::SetCodeRun { + sheet_pos: SheetPos { + x: insert_at.x, + y: insert_at.y + current_y as i64, + sheet_id, + }, + code_run: Some(data_table), + index: y as usize, + }); + Ok(ops) } diff --git a/quadratic-core/src/grid/code_run.rs b/quadratic-core/src/grid/code_run.rs index e0b5c71d45..bf2709e045 100644 --- a/quadratic-core/src/grid/code_run.rs +++ b/quadratic-core/src/grid/code_run.rs @@ -37,6 +37,7 @@ pub enum CodeCellLanguage { Formula, Connection { kind: ConnectionKind, id: String }, Javascript, + Import, } #[derive(Serialize, Deserialize, Display, Copy, Debug, Clone, PartialEq, Eq, Hash)] diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 026665a9cb..378091da2e 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -12,6 +12,8 @@ use crate::{ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use super::CodeCellLanguage; + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct DataTableColumn { name: String, diff --git a/quadratic-core/src/grid/file/serialize/cell_value.rs b/quadratic-core/src/grid/file/serialize/cell_value.rs index 10a7867fa0..c3b44b23aa 100644 --- a/quadratic-core/src/grid/file/serialize/cell_value.rs +++ b/quadratic-core/src/grid/file/serialize/cell_value.rs @@ -29,6 +29,7 @@ pub fn export_cell_value(cell_value: CellValue) -> current::CellValueSchema { id, } } + CodeCellLanguage::Import => current::CodeCellLanguageSchema::Import, }, }), CellValue::Logical(logical) => current::CellValueSchema::Logical(logical), @@ -81,6 +82,7 @@ pub fn import_cell_value(value: current::CellValueSchema) -> CellValue { id, } } + current::CodeCellLanguageSchema::Import => CodeCellLanguage::Import, }, }), current::CellValueSchema::Logical(logical) => CellValue::Logical(logical), diff --git a/quadratic-core/src/grid/file/v1_6/schema.rs b/quadratic-core/src/grid/file/v1_6/schema.rs index 411215b070..01c8f665b6 100644 --- a/quadratic-core/src/grid/file/v1_6/schema.rs +++ b/quadratic-core/src/grid/file/v1_6/schema.rs @@ -270,6 +270,7 @@ pub enum CodeCellLanguage { Formula, Javascript, Connection { kind: ConnectionKind, id: String }, + Import, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 53bd3127dc..af7fb5aacf 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -91,8 +91,6 @@ impl Sheet { } pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { - dbgjs!("iter_code_output_in_rect()"); - dbgjs!(&self.data_tables); self.data_tables .iter() .filter_map(move |(pos, data_table)| { @@ -147,16 +145,13 @@ impl Sheet { }) }; - let cell_value = cell_value?; - dbgjs!("edit_code_value()"); - dbgjs!(&cell_value); - - match cell_value { - CellValue::Code(mut cell_value) => { + match cell_value?.code_cell_value() { + Some(mut code_cell_value) => { // replace internal cell references with a1 notation - if matches!(cell_value.language, CodeCellLanguage::Formula) { - let replaced = replace_internal_cell_references(&cell_value.code, code_pos); - cell_value.code = replaced; + if matches!(code_cell_value.language, CodeCellLanguage::Formula) { + let replaced = + replace_internal_cell_references(&code_cell_value.code, code_pos); + code_cell_value.code = replaced; } if let Some(data_table) = self.data_table(code_pos) { @@ -171,75 +166,41 @@ impl Sheet { None }; - #[allow(unreachable_patterns)] - let code_run = match &data_table.kind { - DataTableKind::CodeRun(code_run) => code_run, - _ => unreachable!(), - }; - - Some(JsCodeCell { - x: code_pos.x, - y: code_pos.y, - code_string: cell_value.code, - language: cell_value.language, - std_err: code_run.std_err.clone(), - std_out: code_run.std_out.clone(), - evaluation_result: Some(evaluation_result), - spill_error, - return_info: Some(JsReturnInfo { - line_number: code_run.line_number, - output_type: code_run.output_type.clone(), + match &data_table.kind { + DataTableKind::CodeRun(code_run) => Some(JsCodeCell { + x: code_pos.x, + y: code_pos.y, + code_string: code_cell_value.code, + language: code_cell_value.language, + std_err: code_run.std_err.clone(), + std_out: code_run.std_out.clone(), + evaluation_result: Some(evaluation_result), + spill_error, + return_info: Some(JsReturnInfo { + line_number: code_run.line_number, + output_type: code_run.output_type.clone(), + }), + cells_accessed: Some(code_run.cells_accessed.iter().copied().collect()), }), - cells_accessed: Some(code_run.cells_accessed.iter().copied().collect()), - }) - } else { - Some(JsCodeCell { - x: code_pos.x, - y: code_pos.y, - code_string: cell_value.code, - language: cell_value.language, - std_err: None, - std_out: None, - evaluation_result: None, - spill_error: None, - return_info: None, - cells_accessed: None, - }) - } - } - CellValue::Import(_) => { - dbgjs!("CellValue::Import"); - dbgjs!(self.data_table(code_pos)); - if let Some(data_table) = self.data_table(code_pos) { - let evaluation_result = - serde_json::to_string(&data_table.value).unwrap_or("".into()); - let spill_error = if data_table.spill_error { - Some(self.find_spill_error_reasons( - &data_table.output_rect(code_pos, true), - code_pos, - )) - } else { - None - }; - - Some(JsCodeCell { - x: code_pos.x, - y: code_pos.y, - code_string: "".into(), - language: CodeCellLanguage::Formula, - std_err: None, - std_out: None, - evaluation_result: Some(evaluation_result), - spill_error, - return_info: None, - cells_accessed: None, - }) + DataTableKind::Import(_) => Some(JsCodeCell { + x: code_pos.x, + y: code_pos.y, + code_string: code_cell_value.code, + language: code_cell_value.language, + std_err: None, + std_out: None, + evaluation_result: Some(evaluation_result), + spill_error, + return_info: None, + cells_accessed: None, + }), + } } else { Some(JsCodeCell { x: code_pos.x, y: code_pos.y, - code_string: "".into(), - language: CodeCellLanguage::Formula, + code_string: code_cell_value.code, + language: code_cell_value.language, std_err: None, std_out: None, evaluation_result: None, diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index c238a1ffd5..c91c85519c 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -160,10 +160,7 @@ impl Sheet { ) -> Vec { let mut cells = vec![]; - dbgjs!("get_code_cells()"); - dbgjs!(&data_table); - - if let CellValue::Code(code) = code { + if let Some(code_cell_value) = code.code_cell_value() { if data_table.spill_error { cells.push(self.get_render_cell( code_rect.min.x, @@ -173,7 +170,7 @@ impl Sheet { span: None, msg: RunErrorMsg::Spill, })), - Some(code.language.to_owned()), + Some(code_cell_value.language), )); } else if let Some(error) = data_table.get_error() { cells.push(self.get_render_cell( @@ -181,7 +178,7 @@ impl Sheet { code_rect.min.y, None, &CellValue::Error(Box::new(error)), - Some(code.language.to_owned()), + Some(code_cell_value.language), )); } else { // find overlap of code_rect into rect @@ -214,7 +211,7 @@ impl Sheet { ); if let Some(value) = value { let language = if x == code_rect.min.x && y == code_rect.min.y { - Some(code.language.to_owned()) + Some(code_cell_value.language.to_owned()) } else { None }; @@ -225,62 +222,6 @@ impl Sheet { } } - if let CellValue::Import(import) = code { - if data_table.spill_error { - cells.push(self.get_render_cell( - code_rect.min.x, - code_rect.min.y, - None, - &CellValue::Error(Box::new(RunError { - span: None, - msg: RunErrorMsg::Spill, - })), - None, - )); - } else if let Some(error) = data_table.get_error() { - cells.push(self.get_render_cell( - code_rect.min.x, - code_rect.min.y, - None, - &CellValue::Error(Box::new(error)), - None, - )); - } else { - // find overlap of code_rect into rect - let x_start = if code_rect.min.x > output_rect.min.x { - code_rect.min.x - } else { - output_rect.min.x - }; - let x_end = if code_rect.max.x > output_rect.max.x { - output_rect.max.x - } else { - code_rect.max.x - }; - let y_start = if code_rect.min.y > output_rect.min.y { - code_rect.min.y - } else { - output_rect.min.y - }; - let y_end = if code_rect.max.y > output_rect.max.y { - output_rect.max.y - } else { - code_rect.max.y - }; - for x in x_start..=x_end { - let column = self.get_column(x); - for y in y_start..=y_end { - let value = data_table.cell_value_at( - (x - code_rect.min.x) as u32, - (y - code_rect.min.y) as u32, - ); - if let Some(value) = value { - cells.push(self.get_render_cell(x, y, column, &value, None)); - } - } - } - } - } cells } @@ -306,15 +247,11 @@ impl Sheet { // Fetch values from code cells self.iter_code_output_in_rect(rect) .for_each(|(data_table_rect, data_table)| { - dbgjs!("get_render_cells() 1"); - dbgjs!(&data_table); // sanity check that there is a CellValue::Code for this CodeRun if let Some(cell_value) = self.cell_value(Pos { x: data_table_rect.min.x, y: data_table_rect.min.y, }) { - dbgjs!("get_render_cells() 2"); - dbgjs!(&data_table); render_cells.extend(self.get_code_cells( &cell_value, data_table, @@ -445,11 +382,8 @@ impl Sheet { pub fn get_render_code_cell(&self, pos: Pos) -> Option { let data_table = self.data_tables.get(&pos)?; - dbgjs!("get_render_code_cell()"); - dbgjs!(&data_table); let code = self.cell_value(pos)?; let output_size = data_table.output_size(); - dbgjs!(&output_size); let (state, w, h, spill_error) = if data_table.spill_error { let reasons = self.find_spill_error_reasons(&data_table.output_rect(pos, true), pos); ( @@ -477,6 +411,7 @@ impl Sheet { h, language: match code { CellValue::Code(code) => code.language, + CellValue::Import(_) => CodeCellLanguage::Import, _ => return None, }, state, @@ -490,8 +425,8 @@ impl Sheet { .iter() .filter_map(|(pos, data_table)| { if let Some(code) = self.cell_value(*pos) { - match &code { - CellValue::Code(code) => { + match code.code_cell_value() { + Some(code_cell_value) => { let output_size = data_table.output_size(); let (state, w, h, spill_error) = if data_table.spill_error { let reasons = self.find_spill_error_reasons( @@ -514,12 +449,13 @@ impl Sheet { None, ) }; + Some(JsRenderCodeCell { x: pos.x as i32, y: pos.y as i32, w, h, - language: code.language.to_owned(), + language: code_cell_value.language, state, spill_error, }) diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index f66f612573..aa7565f74a 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -212,7 +212,9 @@ pub fn print_table_sheet(sheet: &Sheet, rect: Rect) { CodeCellLanguage::Python => code_cell.code.to_string(), CodeCellLanguage::Connection { .. } => code_cell.code.to_string(), CodeCellLanguage::Javascript => code_cell.code.to_string(), + CodeCellLanguage::Import => "import".to_string(), }, + Some(CellValue::Import(import)) => import.to_string(), _ => sheet .display_value(pos) .unwrap_or(CellValue::Blank) diff --git a/quadratic-core/src/values/cellvalue.rs b/quadratic-core/src/values/cellvalue.rs index 63ae238a24..5de0d0cf5d 100644 --- a/quadratic-core/src/values/cellvalue.rs +++ b/quadratic-core/src/values/cellvalue.rs @@ -5,6 +5,7 @@ use std::{ use anyhow::Result; use bigdecimal::{BigDecimal, Signed, ToPrimitive, Zero}; +use calamine::Cell; use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use serde::{Deserialize, Serialize}; @@ -25,6 +26,12 @@ pub struct CodeCellValue { pub code: String, } +impl CodeCellValue { + pub fn new(language: CodeCellLanguage, code: String) -> Self { + Self { language, code } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Import { pub file_name: String, @@ -875,6 +882,14 @@ impl CellValue { } crate::formulas::util::checked_div(span, sum, count as f64) } + + pub fn code_cell_value(&self) -> Option { + match self { + CellValue::Code(code) => Some(code.to_owned()), + CellValue::Import(_) => Some(CodeCellValue::new(CodeCellLanguage::Import, "".into())), + _ => None, + } + } } #[cfg(test)] From 63e3e44784bc1d850fcd9c7eb15e3737995f043c Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 2 Oct 2024 14:15:52 -0600 Subject: [PATCH 011/373] Import parquet, fix tests, add test utils --- .../src/controller/operations/import.rs | 276 +++++++++--------- .../src/controller/user_actions/import.rs | 133 +++++---- quadratic-core/src/grid/data_table.rs | 2 - quadratic-core/src/pos.rs | 10 + quadratic-core/src/test_util.rs | 79 ++++- quadratic-core/src/values/cellvalue.rs | 1 - quadratic-core/src/wasm_bindings/js.rs | 2 + .../data/parquet/simple.parquet | Bin 0 -> 2157 bytes 8 files changed, 298 insertions(+), 205 deletions(-) create mode 100644 quadratic-rust-shared/data/parquet/simple.parquet diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index ede1730643..3423add29c 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -4,6 +4,7 @@ use anyhow::{anyhow, bail, Result}; use chrono::{NaiveDate, NaiveTime}; use crate::{ + arrow::arrow_col_to_cell_value_vec, cell_values::CellValues, cellvalue::Import, controller::GridController, @@ -29,6 +30,7 @@ impl GridController { insert_at: Pos, ) -> Result> { let error = |message: String| anyhow!("Error parsing CSV file {}: {}", file_name, message); + let import = Import::new(file_name.into()); let file: &[u8] = match String::from_utf8_lossy(&file) { Cow::Borrowed(_) => &file, Cow::Owned(_) => { @@ -43,107 +45,79 @@ impl GridController { &file } }; + let reader = || { + csv::ReaderBuilder::new() + .has_headers(false) + .flexible(true) + .from_reader(file) + }; // first get the total number of lines so we can provide progress - let mut reader = csv::ReaderBuilder::new() - .has_headers(false) - .from_reader(file); - let height = reader.records().count() as u32; - - let mut reader = csv::ReaderBuilder::new() - .has_headers(false) - .flexible(true) - .from_reader(file); + let height = reader().records().count() as u32; + let width = reader().headers()?.len() as u32; - let width = reader.headers()?.len() as u32; if width == 0 { bail!("empty files cannot be processed"); } - // then create operations using MAXIMUM_IMPORT_LINES to break up the SetCellValues operations let mut ops = vec![] as Vec; - let mut cell_values = Array::new_empty( - ArraySize::new(width, height.min(IMPORT_LINES_PER_OPERATION)).unwrap(), - ); - let mut current_y = 0; + let array_size = ArraySize::new_or_err(width, height).map_err(|e| error(e.to_string()))?; + let mut cell_values = Array::new_empty(array_size); let mut y: u32 = 0; - for entry in reader.records() { + for entry in reader().records() { match entry { - Err(e) => return Err(error(format!("line {}: {}", current_y + y + 1, e))), + Err(e) => return Err(error(format!("line {}: {}", y + 1, e))), Ok(record) => { for (x, value) in record.iter().enumerate() { let (operations, cell_value) = self.string_to_cell_value( SheetPos { x: insert_at.x + x as i64, - y: insert_at.y + current_y as i64 + y as i64, + y: insert_at.y + y as i64, sheet_id, }, value, ); ops.extend(operations); - // cell_values.set(x as u32, y, cell_value.clone()); - cell_values.set(x as u32, y, cell_value).unwrap(); + cell_values + .set(x as u32, y, cell_value) + .map_err(|e| error(e.to_string()))?; } } } y += 1; - if y >= IMPORT_LINES_PER_OPERATION { - let import = Import::new(file_name.into()); - let data_table = DataTable::from((import, cell_values)); - - ops.push(Operation::SetCodeRun { - sheet_pos: SheetPos { - x: insert_at.x, - y: insert_at.y + current_y as i64, - sheet_id, - }, - code_run: Some(data_table), - index: y as usize, - }); - - current_y += y; - y = 0; - - let h = (height - current_y).min(IMPORT_LINES_PER_OPERATION); - cell_values = Array::new_empty(ArraySize::new(width, h).unwrap()); - - // update the progress bar every time there's a new operation - if cfg!(target_family = "wasm") || cfg!(test) { - crate::wasm_bindings::js::jsImportProgress( - file_name, - current_y, - height, - insert_at.x, - insert_at.y, - width, - height, - ); - } + // update the progress bar every time there's a new batch + let should_update = y % IMPORT_LINES_PER_OPERATION == 0; + + if should_update && (cfg!(target_family = "wasm") || cfg!(test)) { + crate::wasm_bindings::js::jsImportProgress( + file_name, + y, + height, + insert_at.x, + insert_at.y, + width, + height, + ); } } // finally add the final operation - let import = Import::new(file_name.into()); - let data_table = DataTable::from((import, cell_values)); - - let sheet_pos = SheetPos { - x: insert_at.x, - y: insert_at.y, - sheet_id, - }; + let data_table = DataTable::from((import.to_owned(), cell_values)); + let sheet_pos = SheetPos::from((insert_at, sheet_id)); + // this operation must be before the SetCodeRun operations ops.push(Operation::SetCellValues { sheet_pos, - values: CellValues::from(CellValue::Import(Import::new(file_name.into()))), + values: CellValues::from(CellValue::Import(import)), }); ops.push(Operation::SetCodeRun { sheet_pos: SheetPos { x: insert_at.x, - y: insert_at.y + current_y as i64, + y: insert_at.y, sheet_id, }, code_run: Some(data_table), @@ -330,6 +304,9 @@ impl GridController { insert_at: Pos, ) -> Result> { let mut ops = vec![] as Vec; + let import = Import::new(file_name.into()); + let error = + |message: String| anyhow!("Error parsing Parquet file {}: {}", file_name, message); // this is not expensive let bytes = Bytes::from(file); @@ -342,15 +319,22 @@ impl GridController { let headers: Vec = fields.iter().map(|f| f.name().into()).collect(); let mut width = headers.len() as u32; - ops.push(Operation::SetCellValues { - sheet_pos: (insert_at.x, insert_at.y, sheet_id).into(), - values: CellValues::from_flat_array(headers.len() as u32, 1, headers), - }); + // add 1 to the height for the headers + let array_size = + ArraySize::new_or_err(width, total_size + 1).map_err(|e| error(e.to_string()))?; + let mut cell_values = Array::new_empty(array_size); - let reader = builder.build()?; + // add the headers to the first row + for (x, header) in headers.into_iter().enumerate() { + cell_values + .set(x as u32, 0, header) + .map_err(|e| error(e.to_string()))? + } + let reader = builder.build()?; let mut height = 0; let mut current_size = 0; + for (row_index, batch) in reader.enumerate() { let batch = batch?; let num_rows = batch.num_rows(); @@ -362,20 +346,15 @@ impl GridController { for col_index in 0..num_cols { let col = batch.column(col_index); - - // arrow.rs has the `impl TryFrom<&ArrayRef> for CellValues` block - let values: CellValues = col.try_into()?; - - let operations = Operation::SetCellValues { - sheet_pos: ( - insert_at.x + col_index as i64, - insert_at.y + (row_index * num_rows) as i64 + 1, - sheet_id, - ) - .into(), - values, - }; - ops.push(operations); + let values = arrow_col_to_cell_value_vec(col)?; + let x = col_index as u32; + let y = (row_index * num_rows) as u32 + 1; + + for (index, value) in values.into_iter().enumerate() { + cell_values + .set(x, y + index as u32, value) + .map_err(|e| error(e.to_string()))?; + } // update the progress bar every time there's a new operation if cfg!(target_family = "wasm") || cfg!(test) { @@ -392,6 +371,21 @@ impl GridController { } } + let sheet_pos = SheetPos::from((insert_at, sheet_id)); + let data_table = DataTable::from((import.to_owned(), cell_values)); + + // this operation must be before the SetCodeRun operations + ops.push(Operation::SetCellValues { + sheet_pos, + values: CellValues::from(CellValue::Import(import)), + }); + + ops.push(Operation::SetCodeRun { + sheet_pos, + code_run: Some(data_table), + index: 0, + }); + Ok(ops) } } @@ -424,7 +418,10 @@ fn read_utf16(bytes: &[u8]) -> Option { #[cfg(test)] mod test { use super::{read_utf16, *}; - use crate::CellValue; + use crate::{ + test_util::{assert_data_table_cell_value, assert_display_cell_value}, + CellValue, + }; use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use serial_test::parallel; @@ -444,37 +441,42 @@ mod test { let mut gc = GridController::test(); let sheet_id = gc.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; + let file_name = "simple.csv"; const SIMPLE_CSV: &str = "city,region,country,population\nSouthborough,MA,United States,a lot of people"; - let ops = gc.import_csv_operations( - sheet_id, - SIMPLE_CSV.as_bytes().to_vec(), - "smallpop.csv", - pos, - ); + let ops = gc + .import_csv_operations(sheet_id, SIMPLE_CSV.as_bytes().to_vec(), file_name, pos) + .unwrap(); let values = vec![ - vec!["city", "Southborough"], - vec!["region", "MA"], - vec!["country", "United States"], - vec!["population", "a lot of people"], + vec!["city", "region", "country", "population"], + vec!["Southborough", "MA", "United States", "a lot of people"], ]; - let import = Import::new("smallpop.csv".into()); - let data_table = DataTable::from((import, values.into())); + let import = Import::new(file_name.into()); + let cell_value = CellValue::Import(import.clone()); + let mut expected_data_table = DataTable::from((import, values.into())); + assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); + + let data_table = match ops[1].clone() { + Operation::SetCodeRun { code_run, .. } => code_run.unwrap(), + _ => panic!("Expected SetCodeRun operation"), + }; + expected_data_table.last_modified = data_table.last_modified; + let expected = Operation::SetCodeRun { sheet_pos: SheetPos { x: 0, y: 0, sheet_id, }, - code_run: Some(data_table), + code_run: Some(expected_data_table), index: 2, }; - assert_eq!(ops.as_ref().unwrap().len(), 1); - assert_eq!(ops.unwrap()[0], expected); + assert_eq!(ops.len(), 2); + assert_eq!(ops[1], expected); } #[test] @@ -483,40 +485,32 @@ mod test { let mut gc = GridController::test(); let sheet_id = gc.grid.sheets()[0].id; let pos = Pos { x: 1, y: 2 }; + let file_name = "long.csv"; let mut csv = String::new(); for i in 0..IMPORT_LINES_PER_OPERATION * 2 + 150 { csv.push_str(&format!("city{},MA,United States,{}\n", i, i * 1000)); } - let ops = gc.import_csv_operations(sheet_id, csv.as_bytes().to_vec(), "long.csv", pos); - assert_eq!(ops.as_ref().unwrap().len(), 3); + let ops = gc.import_csv_operations(sheet_id, csv.as_bytes().to_vec(), file_name, pos); - let first_pos = match ops.as_ref().unwrap()[0] { - Operation::SetCellValues { sheet_pos, .. } => sheet_pos, - _ => panic!("Expected SetCellValues operation"), - }; - let second_pos = match ops.as_ref().unwrap()[1] { - Operation::SetCellValues { sheet_pos, .. } => sheet_pos, - _ => panic!("Expected SetCellValues operation"), - }; - let third_pos = match ops.as_ref().unwrap()[2] { - Operation::SetCellValues { sheet_pos, .. } => sheet_pos, - _ => panic!("Expected SetCellValues operation"), - }; - assert_eq!(first_pos.x, 1); - assert_eq!(second_pos.x, 1); - assert_eq!(third_pos.x, 1); - assert_eq!(first_pos.y, 2); - assert_eq!(second_pos.y, 2 + IMPORT_LINES_PER_OPERATION as i64); - assert_eq!(third_pos.y, 2 + IMPORT_LINES_PER_OPERATION as i64 * 2); - - let first_values = match ops.as_ref().unwrap()[0] { - Operation::SetCellValues { ref values, .. } => values, - _ => panic!("Expected SetCellValues operation"), + let import = Import::new(file_name.into()); + let cell_value = CellValue::Import(import.clone()); + assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); + + assert_eq!(ops.as_ref().unwrap().len(), 2); + + let (sheet_pos, data_table) = match &ops.unwrap()[1] { + Operation::SetCodeRun { + sheet_pos, + code_run, + .. + } => (sheet_pos.clone(), code_run.clone().unwrap()), + _ => panic!("Expected SetCodeRun operation"), }; + assert_eq!(sheet_pos.x, 1); assert_eq!( - first_values.get(0, 0), + data_table.cell_value_ref_at(0, 0), Some(&CellValue::Text("city0".into())) ); } @@ -532,27 +526,19 @@ mod test { gc.import_csv(sheet_id, csv.as_bytes().to_vec(), "csv", pos, None) .unwrap(); - assert_eq!( - gc.sheet(sheet_id).cell_value((0, 0).into()), - Some(CellValue::Date( - NaiveDate::parse_from_str("2024-12-21", "%Y-%m-%d").unwrap() - )) - ); - assert_eq!( - gc.sheet(sheet_id).cell_value((1, 0).into()), - Some(CellValue::Time( - NaiveTime::parse_from_str("13:23:00", "%H:%M:%S").unwrap() - )) - ); - assert_eq!( - gc.sheet(sheet_id).cell_value((2, 0).into()), - Some(CellValue::DateTime( - NaiveDate::from_ymd_opt(2024, 12, 21) - .unwrap() - .and_hms_opt(13, 23, 0) - .unwrap() - )) + let value = CellValue::Date(NaiveDate::parse_from_str("2024-12-21", "%Y-%m-%d").unwrap()); + assert_data_table_cell_value(&gc, sheet_id, 0, 0, &value.to_string()); + + let value = CellValue::Time(NaiveTime::parse_from_str("13:23:00", "%H:%M:%S").unwrap()); + assert_data_table_cell_value(&gc, sheet_id, 1, 0, &value.to_string()); + + let value = CellValue::DateTime( + NaiveDate::from_ymd_opt(2024, 12, 21) + .unwrap() + .and_hms_opt(13, 23, 0) + .unwrap(), ); + assert_data_table_cell_value(&gc, sheet_id, 2, 0, &value.to_string()); } #[test] @@ -607,19 +593,19 @@ mod test { // date assert_eq!( - sheet.cell_value((0, 1).into()), + sheet.data_table(pos).unwrap().cell_value_at(0, 1), Some(CellValue::Date( NaiveDate::parse_from_str("2024-12-21", "%Y-%m-%d").unwrap() )) ); assert_eq!( - sheet.cell_value((0, 2).into()), + sheet.data_table(pos).unwrap().cell_value_at(0, 2), Some(CellValue::Date( NaiveDate::parse_from_str("2024-12-22", "%Y-%m-%d").unwrap() )) ); assert_eq!( - sheet.cell_value((0, 3).into()), + sheet.data_table(pos).unwrap().cell_value_at(0, 3), Some(CellValue::Date( NaiveDate::parse_from_str("2024-12-23", "%Y-%m-%d").unwrap() )) diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index b3fd50b32e..018bb9a93d 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -78,8 +78,12 @@ mod tests { use std::str::FromStr; use crate::{ + cellvalue::Import, grid::CodeCellLanguage, - test_util::{assert_cell_value_row, print_table}, + test_util::{ + assert_cell_value_row, assert_data_table_cell_value_row, assert_display_cell_value, + print_data_table, print_table, + }, wasm_bindings::js::clear_js_calls, CellValue, CodeCellValue, Rect, RunError, RunErrorMsg, Span, }; @@ -101,6 +105,7 @@ mod tests { "../quadratic-rust-shared/data/excel/all_excel_functions.xlsx"; // const EXCEL_FILE: &str = "../quadratic-rust-shared/data/excel/financial_sample.xlsx"; const PARQUET_FILE: &str = "../quadratic-rust-shared/data/parquet/all_supported_types.parquet"; + // const SIMPLE_PARQUET_FILE: &str = "../quadratic-rust-shared/data/parquet/simple.parquet"; // const MEDIUM_PARQUET_FILE: &str = "../quadratic-rust-shared/data/parquet/lineitem.parquet"; // const LARGE_PARQUET_FILE: &str = // "../quadratic-rust-shared/data/parquet/flights_1m.parquet"; @@ -112,14 +117,11 @@ mod tests { let mut grid_controller = GridController::test(); let sheet_id = grid_controller.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; + let file_name = "simple.csv"; - let _ = grid_controller.import_csv( - sheet_id, - scv_file.as_slice().to_vec(), - "smallpop.csv", - pos, - None, - ); + grid_controller + .import_csv(sheet_id, scv_file.as_slice().to_vec(), file_name, pos, None) + .unwrap(); print_table( &grid_controller, @@ -127,7 +129,11 @@ mod tests { Rect::new_span(pos, Pos { x: 3, y: 10 }), ); - assert_cell_value_row( + let import = Import::new(file_name.into()); + let cell_value = CellValue::Import(import); + assert_display_cell_value(&grid_controller, sheet_id, 0, 0, &cell_value.to_string()); + + assert_data_table_cell_value_row( &grid_controller, sheet_id, 0, @@ -136,7 +142,7 @@ mod tests { vec!["city", "region", "country", "population"], ); - assert_cell_value_row( + assert_data_table_cell_value_row( &grid_controller, sheet_id, 0, @@ -163,21 +169,30 @@ mod tests { fn import_large_csv() { clear_js_calls(); let mut gc = GridController::test(); + let sheet_id = gc.grid.sheets()[0].id; let mut csv = String::new(); + let file_name = "large.csv"; + for _ in 0..10000 { for x in 0..10 { csv.push_str(&format!("{},", x)); } csv.push_str("done,\n"); } - let result = gc.import_csv( + + gc.import_csv( gc.grid.sheets()[0].id, csv.as_bytes().to_vec(), "large.csv", Pos { x: 0, y: 0 }, None, - ); - assert!(result.is_ok()); + ) + .unwrap(); + + let import = Import::new(file_name.into()); + let cell_value = CellValue::Import(import); + assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); + expect_js_call_count("jsImportProgress", 1, true); } @@ -341,10 +356,15 @@ mod tests { let mut grid_controller = GridController::test(); let sheet_id = grid_controller.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; + let file_name = "alltypes_plain.parquet"; let file: Vec = std::fs::read(PARQUET_FILE).expect("Failed to read file"); - let _ = grid_controller.import_parquet(sheet_id, file, "alltypes_plain.parquet", pos, None); + let _result = grid_controller.import_parquet(sheet_id, file, file_name, pos, None); - assert_cell_value_row( + let import = Import::new(file_name.into()); + let cell_value = CellValue::Import(import); + assert_display_cell_value(&grid_controller, sheet_id, 0, 0, &cell_value.to_string()); + + assert_data_table_cell_value_row( &grid_controller, sheet_id, 0, @@ -437,74 +457,81 @@ mod tests { #[test] #[parallel] fn should_import_with_title_header() { - let scv_file = read_test_csv_file("title_row.csv"); + let file_name = "title_row.csv"; + let scv_file = read_test_csv_file(file_name); let mut gc = GridController::test(); let sheet_id = gc.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; - gc.import_csv( - sheet_id, - scv_file.as_slice().to_vec(), - "test.csv", - pos, - None, - ) - .expect("import_csv"); + gc.import_csv(sheet_id, scv_file.as_slice().to_vec(), file_name, pos, None) + .unwrap(); - print_table(&gc, sheet_id, Rect::new_span(pos, Pos { x: 3, y: 4 })); + print_data_table(&gc, sheet_id, Rect::new_span(pos, Pos { x: 3, y: 4 })); - assert_cell_value_row(&gc, sheet_id, 0, 2, 0, vec!["Sample report ", "", ""]); - assert_cell_value_row(&gc, sheet_id, 0, 2, 2, vec!["c1", " c2", " Sample column3"]); - assert_cell_value_row(&gc, sheet_id, 0, 2, 5, vec!["7", "8", "9"]); + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, vec!["Sample report ", "", ""]); + assert_data_table_cell_value_row( + &gc, + sheet_id, + 0, + 2, + 2, + vec!["c1", " c2", " Sample column3"], + ); + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 5, vec!["7", "8", "9"]); } #[test] #[parallel] fn should_import_with_title_header_and_empty_first_row() { - let scv_file = read_test_csv_file("title_row_empty_first.csv"); + let file_name = "title_row_empty_first.csv"; + let scv_file = read_test_csv_file(file_name); let mut gc = GridController::test(); let sheet_id = gc.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; - gc.import_csv( - sheet_id, - scv_file.as_slice().to_vec(), - "test.csv", - pos, - None, - ) - .expect("import_csv"); + gc.import_csv(sheet_id, scv_file.as_slice().to_vec(), file_name, pos, None) + .unwrap(); + + print_data_table(&gc, sheet_id, Rect::new_span(pos, Pos { x: 3, y: 4 })); - print_table(&gc, sheet_id, Rect::new_span(pos, Pos { x: 3, y: 4 })); + let import = Import::new(file_name.into()); + let cell_value = CellValue::Import(import); + assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); - assert_cell_value_row(&gc, sheet_id, 0, 2, 0, vec!["Sample report ", "", ""]); - assert_cell_value_row(&gc, sheet_id, 0, 2, 2, vec!["c1", " c2", " Sample column3"]); - assert_cell_value_row(&gc, sheet_id, 0, 2, 5, vec!["7", "8", "9"]); + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, vec!["Sample report ", "", ""]); + assert_data_table_cell_value_row( + &gc, + sheet_id, + 0, + 2, + 2, + vec!["c1", " c2", " Sample column3"], + ); + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 5, vec!["7", "8", "9"]); } #[test] #[parallel] fn should_import_utf16_with_invalid_characters() { - let scv_file = read_test_csv_file("encoding_issue.csv"); + let file_name = "encoding_issue.csv"; + let scv_file = read_test_csv_file(&file_name); let mut gc = GridController::test(); let sheet_id = gc.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; - gc.import_csv( - sheet_id, - scv_file.as_slice().to_vec(), - "test.csv", - pos, - None, - ) - .expect("import_csv"); + gc.import_csv(sheet_id, scv_file.as_slice().to_vec(), file_name, pos, None) + .unwrap(); print_table(&gc, sheet_id, Rect::new_span(pos, Pos { x: 2, y: 3 })); - assert_cell_value_row(&gc, sheet_id, 0, 2, 0, vec!["issue", " test", " value"]); - assert_cell_value_row(&gc, sheet_id, 0, 2, 1, vec!["0", " 1", " Invalid"]); - assert_cell_value_row(&gc, sheet_id, 0, 2, 2, vec!["0", " 2", " Valid"]); + let import = Import::new(file_name.into()); + let cell_value = CellValue::Import(import); + assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); + + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, vec!["issue", " test", " value"]); + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 1, vec!["0", " 1", " Invalid"]); + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 2, vec!["0", " 2", " Valid"]); } // #[test]#[parallel] diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 378091da2e..026665a9cb 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -12,8 +12,6 @@ use crate::{ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use super::CodeCellLanguage; - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct DataTableColumn { name: String, diff --git a/quadratic-core/src/pos.rs b/quadratic-core/src/pos.rs index c26afd5682..c8d6dfbe10 100644 --- a/quadratic-core/src/pos.rs +++ b/quadratic-core/src/pos.rs @@ -120,6 +120,16 @@ impl From<(i64, i64, SheetId)> for SheetPos { } } +impl From<(Pos, SheetId)> for SheetPos { + fn from((pos, sheet_id): (Pos, SheetId)) -> Self { + Self { + x: pos.x, + y: pos.y, + sheet_id, + } + } +} + impl FromStr for SheetPos { type Err = String; diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index aa7565f74a..baa8fd03db 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -12,6 +12,7 @@ use tabled::{ }; /// Run an assertion that a cell value is equal to the given value +#[track_caller] #[cfg(test)] pub fn assert_display_cell_value( grid_controller: &GridController, @@ -38,6 +39,7 @@ pub fn assert_display_cell_value( } /// Run an assertion that a cell value is equal to the given value +#[track_caller] #[cfg(test)] pub fn assert_code_cell_value( grid_controller: &GridController, @@ -56,7 +58,35 @@ pub fn assert_code_cell_value( ); } +/// Run an assertion that a cell value is equal to the given value +#[track_caller] +#[cfg(test)] +pub fn assert_data_table_cell_value( + grid_controller: &GridController, + sheet_id: SheetId, + x: i64, + y: i64, + value: &str, +) { + let sheet = grid_controller.sheet(sheet_id); + let cell_value = sheet + .get_code_cell_value(Pos { x, y }) + .map_or_else(|| CellValue::Blank, |v| CellValue::Text(v.to_string())); + let expected_text_or_blank = + |v: &CellValue| v == &CellValue::Text(value.into()) || v == &CellValue::Blank; + + assert!( + expected_text_or_blank(&cell_value), + "Cell at ({}, {}) does not have the value {:?}, it's actually {:?}", + x, + y, + CellValue::Text(value.into()), + cell_value + ); +} + /// Run an assertion that cell values in a given row are equal to the given value +#[track_caller] #[cfg(test)] pub fn assert_cell_value_row( grid_controller: &GridController, @@ -75,6 +105,27 @@ pub fn assert_cell_value_row( } } +/// Run an assertion that cell values in a given row are equal to the given value +#[track_caller] +#[cfg(test)] +pub fn assert_data_table_cell_value_row( + grid_controller: &GridController, + sheet_id: SheetId, + x_start: i64, + x_end: i64, + y: i64, + value: Vec<&str>, +) { + for (index, x) in (x_start..=x_end).enumerate() { + if let Some(cell_value) = value.get(index) { + assert_data_table_cell_value(grid_controller, sheet_id, x, y, cell_value); + } else { + println!("No value at position ({},{})", index, y); + } + } +} + +#[track_caller] #[cfg(test)] pub fn assert_cell_format_bold_row( grid_controller: &GridController, @@ -95,6 +146,7 @@ pub fn assert_cell_format_bold_row( } } +#[track_caller] #[cfg(test)] pub fn assert_cell_format_bold( grid_controller: &GridController, @@ -116,6 +168,7 @@ pub fn assert_cell_format_bold( } // TODO(ddimaria): refactor all format assertions into a generic function +#[track_caller] #[cfg(test)] pub fn assert_cell_format_cell_fill_color_row( grid_controller: &GridController, @@ -136,6 +189,7 @@ pub fn assert_cell_format_cell_fill_color_row( } } +#[track_caller] #[cfg(test)] pub fn assert_cell_format_fill_color( grid_controller: &GridController, @@ -162,21 +216,30 @@ pub fn print_table(grid_controller: &GridController, sheet_id: SheetId, rect: Re println!("Sheet not found"); return; }; - print_table_sheet(sheet, rect); + print_table_sheet(sheet, rect, true); +} + +// Util to print a simple grid to assist in TDD +pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rect: Rect) { + let Some(sheet) = grid_controller.try_sheet(sheet_id) else { + println!("Sheet not found"); + return; + }; + print_table_sheet(sheet, rect, false); } /// Util to print the entire sheet pub fn print_sheet(sheet: &Sheet) { let bounds = sheet.bounds(true); if let GridBounds::NonEmpty(rect) = bounds { - print_table_sheet(sheet, rect); + print_table_sheet(sheet, rect, true); } else { println!("Sheet is empty"); } } /// Util to print a simple grid to assist in TDD -pub fn print_table_sheet(sheet: &Sheet, rect: Rect) { +pub fn print_table_sheet(sheet: &Sheet, rect: Rect, disply_cell_values: bool) { let mut vals = vec![]; let mut builder = Builder::default(); let columns = (rect.x_range()) @@ -204,7 +267,15 @@ pub fn print_table_sheet(sheet: &Sheet, rect: Rect) { fill_colors.push((count_y + 1, count_x + 1, fill_color)); } - let cell_value = match sheet.cell_value(pos) { + let cell_value = match disply_cell_values { + true => sheet.cell_value(pos), + false => sheet + .data_table(rect.min) + .unwrap() + .cell_value_at(x as u32, y as u32), + }; + + let cell_value = match cell_value { Some(CellValue::Code(code_cell)) => match code_cell.language { CodeCellLanguage::Formula => { replace_internal_cell_references(&code_cell.code.to_string(), pos) diff --git a/quadratic-core/src/values/cellvalue.rs b/quadratic-core/src/values/cellvalue.rs index 5de0d0cf5d..19fb5f191d 100644 --- a/quadratic-core/src/values/cellvalue.rs +++ b/quadratic-core/src/values/cellvalue.rs @@ -5,7 +5,6 @@ use std::{ use anyhow::Result; use bigdecimal::{BigDecimal, Signed, ToPrimitive, Zero}; -use calamine::Cell; use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use serde::{Deserialize, Serialize}; diff --git a/quadratic-core/src/wasm_bindings/js.rs b/quadratic-core/src/wasm_bindings/js.rs index 2b82d67591..90a6537972 100644 --- a/quadratic-core/src/wasm_bindings/js.rs +++ b/quadratic-core/src/wasm_bindings/js.rs @@ -152,6 +152,7 @@ lazy_static! { static ref TEST_ARRAY: Mutex> = Mutex::new(vec![]); } +#[track_caller] #[cfg(test)] pub fn expect_js_call(name: &str, args: String, clear: bool) { let result = TestFunction { @@ -173,6 +174,7 @@ pub fn expect_js_call(name: &str, args: String, clear: bool) { } } +#[track_caller] #[cfg(test)] pub fn expect_js_call_count(name: &str, count: usize, clear: bool) { let mut found = 0; diff --git a/quadratic-rust-shared/data/parquet/simple.parquet b/quadratic-rust-shared/data/parquet/simple.parquet new file mode 100644 index 0000000000000000000000000000000000000000..2862a91f508ea225ea9829d4843f330473977134 GIT binary patch literal 2157 zcmcJRUvJ_@5Wo#VBzKDX5S@{cc!;b!bW#b$B;?BJ`miPeOo0SQuyIvI{ug4fjn9AL zkSH&G?kA}Ff%+M$s!x6Fhv@7YXh`Y9>2xc>&d$uv{ARslI7fYsPSGX0*rwBTYLB9r zRDybu_>iRHv9*|Kihe~i1?&W$bdK3sT9}>0!Y4z$-Jk{I%hxzuI+5IZP38YemH#!Bqj#5VJYl=f{FKQ5ww3>7E4Q|BL;7U)Lxp0}zIJ2S z3Y`L;H%TawqIdDkzoFVW$d^fHbJ@ZdJciG!BJ(Fa5uk(l6-8wWOxjf(UGbTpQxB}^ z=*HcZHnT$8@!9wLKRmEg(1FByiZeFY!anOLwgCz!v@BTpf#LTy}YO&e9JyQfmx?u5#GKA5v!gtm`-qfM9yH1V5NQnfRE>~033^=SpL5$ zi0WC2Pb-sTAxG>PN-*>iZ{oFyK}|!W)V((Yk0mnbFj8l}G12iTdTNib@^ocC z?cx2fhC!kbyMSO5j}Xix2+T{T5lDPA5-Zb@)B!ny{ro$@wnDivJd(=YTeUe1RHtK} znY?PuTb=^G!ellUoYswM>y}_fW5KT6sNyB|33c(#Z`Y8i9rkZDrw2L|E!MYMO*j@# zI=)bA-Rq5EgyVE1PRI69x$5g87fR(gw=Au4h9i-)u&?S2eYJ-3$*wHkO{MaNyp1}r z*YBmneno3wCS>Yj#fA7&c_852z{o|$uv#)Cp2QXGs;St+iC!c2K$iM95Ti620B_Sb zWwA0*%Y9e1H4<+)>^IsMWApkMyEX^^dI0CQ_={u3|ghvk5N>EZr>fm??s z`$4+I{Q~{h#Qg(f>4g1iEv(^5sJP$o8vIX4pJ|8$Av$PZJh1C2as7^B;LYHD42|GG lbKU*L>tUZOR*Kxgpo`82&9jSa0e Date: Wed, 2 Oct 2024 19:02:47 -0600 Subject: [PATCH 012/373] Create columns from the first record of a data table --- .../pending_transaction.rs | 23 ++- quadratic-core/src/controller/dependencies.rs | 13 +- .../execution/control_transaction.rs | 8 +- .../execute_operation/execute_formats.rs | 12 +- .../src/controller/execution/run_code/mod.rs | 57 ++++---- .../execution/run_code/run_formula.rs | 39 ++--- .../src/controller/execution/spills.rs | 13 +- quadratic-core/src/grid/data_table.rs | 138 +++++++++++------- .../src/grid/file/serialize/data_table.rs | 20 ++- quadratic-core/src/grid/file/v1_6/file.rs | 1 + quadratic-core/src/grid/file/v1_7/schema.rs | 7 + quadratic-core/src/grid/sheet/code.rs | 61 ++++---- quadratic-core/src/grid/sheet/rendering.rs | 61 ++++---- quadratic-core/src/grid/sheet/search.rs | 29 ++-- quadratic-core/src/grid/sheet/sheet_test.rs | 37 +++-- 15 files changed, 275 insertions(+), 244 deletions(-) diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index 7e1eb93266..0e21211c39 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -283,7 +283,6 @@ mod tests { }; use super::*; - use chrono::Utc; use serial_test::parallel; #[test] @@ -373,12 +372,11 @@ mod tests { output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Html("html".to_string())), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Html("html".to_string())), + false, + ); transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); assert_eq!(transaction.html_cells.len(), 1); @@ -395,12 +393,11 @@ mod tests { output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Image("image".to_string())), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Image("image".to_string())), + false, + ); transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); assert_eq!(transaction.html_cells.len(), 1); diff --git a/quadratic-core/src/controller/dependencies.rs b/quadratic-core/src/controller/dependencies.rs index 9a57921a9d..a2570b1b8c 100644 --- a/quadratic-core/src/controller/dependencies.rs +++ b/quadratic-core/src/controller/dependencies.rs @@ -33,8 +33,6 @@ impl GridController { mod test { use std::collections::HashSet; - use chrono::Utc; - use crate::{ controller::GridController, grid::{CodeCellLanguage, CodeRun, DataTable, DataTableKind}, @@ -79,12 +77,11 @@ mod test { }; sheet.set_data_table( Pos { x: 0, y: 2 }, - Some(DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Text("test".to_string())), - spill_error: false, - last_modified: Utc::now(), - }), + Some(DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Text("test".to_string())), + false, + )), ); let sheet_pos_02 = SheetPos { x: 0, diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index 89f091bd22..e46610242c 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -1,4 +1,3 @@ -use chrono::Utc; use uuid::Uuid; use super::{GridController, TransactionType}; @@ -283,12 +282,7 @@ impl GridController { } else { Value::Array(array.into()) }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value, - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new(DataTableKind::CodeRun(code_run), value, false, false); self.finalize_code_run(&mut transaction, current_sheet_pos, Some(data_table), None); transaction.waiting_for_async = None; diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs index 45e6d578c9..366b8ab56c 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs @@ -179,7 +179,6 @@ impl GridController { mod test { use std::collections::HashSet; - use chrono::Utc; use serial_test::serial; use super::*; @@ -213,12 +212,11 @@ mod test { }; sheet.set_data_table( Pos { x: 0, y: 0 }, - Some(DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Image("image".to_string())), - spill_error: false, - last_modified: Utc::now(), - }), + Some(DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Image("image".to_string())), + false, + )), ); gc.set_cell_render_size( diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 58b1e2d6cc..936222df13 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -1,5 +1,3 @@ -use chrono::Utc; - use crate::controller::active_transactions::pending_transaction::PendingTransaction; use crate::controller::operations::operation::Operation; use crate::controller::transaction_types::JsCodeResult; @@ -228,12 +226,12 @@ impl GridController { cells_accessed: transaction.cells_accessed.clone(), }, }; - let new_data_table = DataTable { - kind: DataTableKind::CodeRun(new_code_run), - value: Value::Single(CellValue::Blank), - spill_error: false, - last_modified: Utc::now(), - }; + let new_data_table = DataTable::new( + DataTableKind::CodeRun(new_code_run), + Value::Single(CellValue::Blank), + false, + false, + ); transaction.waiting_for_async = None; self.finalize_code_run(transaction, sheet_pos, Some(new_data_table), None); @@ -265,12 +263,12 @@ impl GridController { std_err: None, cells_accessed: transaction.cells_accessed.clone(), }; - return DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Blank), // TODO(ddimaria): this will eventually be an empty vec - spill_error: false, - last_modified: Utc::now(), - }; + return DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Blank), // TODO(ddimaria): this will eventually be an empty vec + false, + false, + ); }; let value = if js_code_result.success { @@ -330,12 +328,7 @@ impl GridController { cells_accessed: transaction.cells_accessed.clone(), }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value, - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new(DataTableKind::CodeRun(code_run), value, false, false); transaction.cells_accessed.clear(); data_table } @@ -387,12 +380,12 @@ mod test { output_type: None, cells_accessed: HashSet::new(), }; - let new_data_table = DataTable { - kind: DataTableKind::CodeRun(new_code_run), - value: Value::Single(CellValue::Text("delete me".to_string())), - spill_error: false, - last_modified: Utc::now(), - }; + let new_data_table = DataTable::new( + DataTableKind::CodeRun(new_code_run), + Value::Single(CellValue::Text("delete me".to_string())), + false, + false, + ); gc.finalize_code_run(transaction, sheet_pos, Some(new_data_table.clone()), None); assert_eq!(transaction.forward_operations.len(), 1); assert_eq!(transaction.reverse_operations.len(), 1); @@ -420,12 +413,12 @@ mod test { output_type: None, cells_accessed: HashSet::new(), }; - let new_data_table = DataTable { - kind: DataTableKind::CodeRun(new_code_run), - value: Value::Single(CellValue::Text("replace me".to_string())), - spill_error: false, - last_modified: Utc::now(), - }; + let new_data_table = DataTable::new( + DataTableKind::CodeRun(new_code_run), + Value::Single(CellValue::Text("replace me".to_string())), + false, + false, + ); gc.finalize_code_run(transaction, sheet_pos, Some(new_data_table.clone()), None); assert_eq!(transaction.forward_operations.len(), 1); assert_eq!(transaction.reverse_operations.len(), 1); diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index c16cd01f45..457012fc44 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -1,4 +1,3 @@ -use chrono::Utc; use itertools::Itertools; use crate::{ @@ -33,12 +32,12 @@ impl GridController { line_number: None, output_type: None, }; - let new_data_table = DataTable { - kind: DataTableKind::CodeRun(new_code_run), - value: output.inner, - spill_error: false, - last_modified: Utc::now(), - }; + let new_data_table = DataTable::new( + DataTableKind::CodeRun(new_code_run), + output.inner, + false, + false, + ); self.finalize_code_run(transaction, sheet_pos, Some(new_data_table), None); } Err(error) => { @@ -262,12 +261,13 @@ mod test { }; assert_eq!( result, - DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Number(12.into())), - spill_error: false, - last_modified: result.last_modified, - }, + DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Number(12.into())), + false, + false + ) + .with_last_modified(result.last_modified), ); } @@ -331,12 +331,13 @@ mod test { }; assert_eq!( result, - DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(array), - spill_error: false, - last_modified: result.last_modified, - } + DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(array), + false, + false + ) + .with_last_modified(result.last_modified), ); } diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index 093015f684..eca877c50b 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -109,7 +109,6 @@ impl GridController { mod tests { use std::collections::HashSet; - use chrono::Utc; use serial_test::{parallel, serial}; use crate::controller::active_transactions::pending_transaction::PendingTransaction; @@ -436,12 +435,12 @@ mod tests { cells_accessed: HashSet::new(), formatted_code_string: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(Array::from(vec![vec!["1"]])), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(Array::from(vec![vec!["1"]])), + false, + false, + ); let pos = Pos { x: 0, y: 0 }; let sheet = gc.sheet_mut(sheet_id); sheet.set_data_table(pos, Some(data_table.clone())); diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 026665a9cb..c433b288c3 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -13,9 +13,15 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -struct DataTableColumn { - name: String, - display: bool, +pub struct DataTableColumn { + pub name: String, + pub display: bool, +} + +impl DataTableColumn { + pub fn new(name: String, display: bool) -> Self { + DataTableColumn { name, display } + } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -27,6 +33,7 @@ pub enum DataTableKind { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct DataTable { pub kind: DataTableKind, + pub columns: Option>, pub value: Value, pub spill_error: bool, pub last_modified: DateTime, @@ -34,16 +41,53 @@ pub struct DataTable { impl From<(Import, Array)> for DataTable { fn from((import, cell_values): (Import, Array)) -> Self { - DataTable { - kind: DataTableKind::Import(import), - value: Value::Array(cell_values), - spill_error: false, - last_modified: Utc::now(), - } + DataTable::new( + DataTableKind::Import(import), + Value::Array(cell_values), + false, + false, + ) } } impl DataTable { + pub fn new(kind: DataTableKind, value: Value, spill_error: bool, header: bool) -> Self { + let mut data_table = DataTable { + kind, + columns: None, + value, + spill_error, + last_modified: Utc::now(), + }; + + if header { + data_table.apply_header(); + } + + data_table + } + + pub fn with_last_modified(mut self, last_modified: DateTime) -> Self { + self.last_modified = last_modified; + self + } + + pub fn apply_header(&mut self) -> &mut Self { + self.columns = match &self.value { + Value::Array(array) => array.rows().next().map(|row| { + row.iter() + .map(|value| DataTableColumn::new(value.to_string(), true)) + .collect::>() + }), + Value::Single(value) => Some(vec![DataTableColumn::new(value.to_string(), true)]), + _ => None, + }; + + // TODO(ddimaria): remove first row from array if it's a header + + self + } + pub fn code_run(&self) -> Option<&CodeRun> { match self.kind { DataTableKind::CodeRun(ref code_run) => Some(code_run), @@ -111,18 +155,16 @@ impl DataTable { } pub fn is_html(&self) -> bool { - if let Some(code_cell_value) = self.cell_value_at(0, 0) { - code_cell_value.is_html() - } else { - false + match self.cell_value_at(0, 0) { + Some(code_cell_value) => code_cell_value.is_html(), + None => false, } } pub fn is_image(&self) -> bool { - if let Some(code_cell_value) = self.cell_value_at(0, 0) { - code_cell_value.is_image() - } else { - false + match self.cell_value_at(0, 0) { + Some(code_cell_value) => code_cell_value.is_image(), + None => false, } } @@ -169,15 +211,16 @@ mod test { line_number: None, output_type: None, }; - let code_run = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Number(1.into())), - spill_error: false, - last_modified: Utc::now(), - }; - assert_eq!(code_run.output_size(), ArraySize::_1X1); + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Number(1.into())), + false, + false, + ); + + assert_eq!(data_table.output_size(), ArraySize::_1X1); assert_eq!( - code_run.output_sheet_rect( + data_table.output_sheet_rect( SheetPos { x: -1, y: -2, @@ -199,12 +242,13 @@ mod test { output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(Array::new_empty(ArraySize::new(10, 11).unwrap())), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(Array::new_empty(ArraySize::new(10, 11).unwrap())), + false, + false, + ); + assert_eq!(data_table.output_size().w.get(), 10); assert_eq!(data_table.output_size().h.get(), 11); assert_eq!( @@ -234,34 +278,22 @@ mod test { line_number: None, output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(Array::new_empty(ArraySize::new(10, 11).unwrap())), - spill_error: true, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(Array::new_empty(ArraySize::new(10, 11).unwrap())), + true, + false, + ); + let sheet_pos = SheetPos::from((1, 2, sheet_id)); + assert_eq!(data_table.output_size().w.get(), 10); assert_eq!(data_table.output_size().h.get(), 11); assert_eq!( - data_table.output_sheet_rect( - SheetPos { - x: 1, - y: 2, - sheet_id - }, - false - ), + data_table.output_sheet_rect(sheet_pos, false), SheetRect::from_numbers(1, 2, 1, 1, sheet_id) ); assert_eq!( - data_table.output_sheet_rect( - SheetPos { - x: 1, - y: 2, - sheet_id - }, - true - ), + data_table.output_sheet_rect(sheet_pos, true), SheetRect::from_numbers(1, 2, 10, 11, sheet_id) ); } diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 46a7a98c81..23a4218c3e 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -4,7 +4,7 @@ use indexmap::IndexMap; use itertools::Itertools; use crate::{ - grid::{CodeRun, DataTable, DataTableKind}, + grid::{CodeRun, DataTable, DataTableColumn, DataTableKind}, ArraySize, Axis, Pos, RunError, RunErrorMsg, Value, }; @@ -166,6 +166,12 @@ pub(crate) fn import_data_table_builder( last_modified: data_table.last_modified.unwrap_or(Utc::now()), // this is required but fall back to now if failed spill_error: data_table.spill_error, value, + columns: data_table.columns.map(|columns| { + columns + .into_iter() + .map(|column| DataTableColumn::new(column.name, column.display)) + .collect() + }), }; new_data_tables.insert(Pos { x: pos.x, y: pos.y }, data_table); @@ -313,12 +319,23 @@ pub(crate) fn export_data_table_runs( } }; + let columns = data_table.columns.map(|columns| { + columns + .into_iter() + .map(|column| current::DataTableColumnSchema { + name: column.name, + display: column.display, + }) + .collect() + }); + let data_table = match data_table.kind { DataTableKind::CodeRun(code_run) => { let code_run = export_code_run(code_run); current::DataTableSchema { kind: current::DataTableKindSchema::CodeRun(code_run), + columns, last_modified: Some(data_table.last_modified), spill_error: data_table.spill_error, value, @@ -328,6 +345,7 @@ pub(crate) fn export_data_table_runs( kind: current::DataTableKindSchema::Import(current::ImportSchema { file_name: import.file_name, }), + columns, last_modified: Some(data_table.last_modified), spill_error: data_table.spill_error, value, diff --git a/quadratic-core/src/grid/file/v1_6/file.rs b/quadratic-core/src/grid/file/v1_6/file.rs index 712c9321f4..a7bb425fcc 100644 --- a/quadratic-core/src/grid/file/v1_6/file.rs +++ b/quadratic-core/src/grid/file/v1_6/file.rs @@ -206,6 +206,7 @@ fn upgrade_code_runs( }; let new_data_table = v1_7::DataTableSchema { kind: v1_7::DataTableKindSchema::CodeRun(new_code_run), + columns: None, value, spill_error: code_run.spill_error, last_modified: code_run.last_modified, diff --git a/quadratic-core/src/grid/file/v1_7/schema.rs b/quadratic-core/src/grid/file/v1_7/schema.rs index 76ff2f6a06..aac776a9f1 100644 --- a/quadratic-core/src/grid/file/v1_7/schema.rs +++ b/quadratic-core/src/grid/file/v1_7/schema.rs @@ -137,6 +137,12 @@ pub struct CodeRunSchema { pub output_type: Option, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DataTableColumnSchema { + pub name: String, + pub display: bool, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum DataTableKindSchema { CodeRun(CodeRunSchema), @@ -146,6 +152,7 @@ pub enum DataTableKindSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DataTableSchema { pub kind: DataTableKindSchema, + pub columns: Option>, pub value: OutputValueSchema, pub spill_error: bool, pub last_modified: Option>, diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index af7fb5aacf..5af999e3c2 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -224,7 +224,6 @@ mod test { Array, CodeCellValue, SheetPos, Value, }; use bigdecimal::BigDecimal; - use chrono::Utc; use serial_test::parallel; use std::{collections::HashSet, vec}; @@ -276,12 +275,12 @@ mod test { line_number: None, output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Number(BigDecimal::from(2))), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Number(BigDecimal::from(2))), + false, + false, + ); let old = sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!(old, None); assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&data_table)); @@ -305,12 +304,12 @@ mod test { line_number: None, output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Number(BigDecimal::from(2))), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Number(BigDecimal::from(2))), + false, + false, + ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!( sheet.get_code_cell_value(Pos { x: 0, y: 0 }), @@ -343,12 +342,12 @@ mod test { line_number: None, output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(Array::from(vec![vec!["1", "2", "3"]])), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(Array::from(vec![vec!["1", "2", "3"]])), + false, + false, + ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!( sheet.edit_code_value(Pos { x: 0, y: 0 }), @@ -434,12 +433,12 @@ mod test { line_number: None, output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(Array::from(vec![vec!["1"], vec!["2"], vec!["3"]])), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(Array::from(vec![vec!["1"], vec!["2"], vec!["3"]])), + false, + false, + ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); sheet.set_data_table(Pos { x: 1, y: 1 }, Some(data_table.clone())); sheet.set_data_table(Pos { x: 2, y: 3 }, Some(data_table.clone())); @@ -469,12 +468,12 @@ mod test { line_number: None, output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(Array::from(vec![vec!["1", "2", "3'"]])), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(Array::from(vec![vec!["1", "2", "3'"]])), + false, + false, + ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); sheet.set_data_table(Pos { x: 1, y: 1 }, Some(data_table.clone())); sheet.set_data_table(Pos { x: 3, y: 2 }, Some(data_table.clone())); diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index c91c85519c..ba33e374e6 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -601,7 +601,6 @@ mod tests { use std::collections::HashSet; - use chrono::Utc; use serial_test::{parallel, serial}; use uuid::Uuid; @@ -662,12 +661,12 @@ mod tests { }; sheet.set_data_table( Pos { x: 2, y: 3 }, - Some(DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Text("hello".to_string())), - spill_error: false, - last_modified: Utc::now(), - }), + Some(DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Text("hello".to_string())), + false, + false, + )), ); assert!(sheet.has_render_cells(rect)); @@ -879,12 +878,12 @@ mod tests { }; // data_table is always 3x2 - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(vec![vec!["1", "2", "3"], vec!["4", "5", "6"]].into()), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(vec![vec!["1", "2", "3"], vec!["4", "5", "6"]].into()), + false, + false, + ); // render rect is larger than code rect let code_cells = sheet.get_code_cells( @@ -933,12 +932,12 @@ mod tests { output_type: None, }; - let code_run = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Number(1.into())), - spill_error: false, - last_modified: Utc::now(), - }; + let code_run = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Number(1.into())), + false, + false, + ); let code_cells = sheet.get_code_cells( &code_cell, &code_run, @@ -1057,12 +1056,12 @@ mod tests { line_number: None, output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Number(2.into())), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Number(2.into())), + false, + false, + ); sheet.set_data_table(pos, Some(data_table)); sheet.set_cell_value(pos, code); let rendering = sheet.get_render_code_cell(pos); @@ -1106,12 +1105,12 @@ mod tests { line_number: None, output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(CellValue::Image(image.clone())), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(CellValue::Image(image.clone())), + false, + false, + ); sheet.set_data_table(pos, Some(data_table)); sheet.set_cell_value(pos, code); sheet.send_all_images(); diff --git a/quadratic-core/src/grid/sheet/search.rs b/quadratic-core/src/grid/sheet/search.rs index 26826cfaa5..89927815d4 100644 --- a/quadratic-core/src/grid/sheet/search.rs +++ b/quadratic-core/src/grid/sheet/search.rs @@ -189,15 +189,12 @@ impl Sheet { mod test { use std::collections::HashSet; - use chrono::Utc; - + use super::*; use crate::{ controller::GridController, grid::{CodeCellLanguage, CodeRun, DataTable, DataTableKind}, Array, CodeCellValue, }; - - use super::*; use serial_test::parallel; #[test] @@ -468,12 +465,12 @@ mod test { line_number: None, output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single("world".into()), - spill_error: false, - last_modified: Utc::now(), - }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single("world".into()), + false, + false, + ); sheet.set_data_table(Pos { x: 1, y: 2 }, Some(data_table)); let results = sheet.search( @@ -511,15 +508,15 @@ mod test { line_number: None, output_type: None, }; - let data_table = DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(Array::from(vec![ + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(Array::from(vec![ vec!["abc", "def", "ghi"], vec!["jkl", "mno", "pqr"], ])), - spill_error: false, - last_modified: Utc::now(), - }; + false, + false, + ); sheet.set_data_table(Pos { x: 1, y: 2 }, Some(data_table)); let results = sheet.search( diff --git a/quadratic-core/src/grid/sheet/sheet_test.rs b/quadratic-core/src/grid/sheet/sheet_test.rs index adfac4eb10..01e01ae62a 100644 --- a/quadratic-core/src/grid/sheet/sheet_test.rs +++ b/quadratic-core/src/grid/sheet/sheet_test.rs @@ -7,7 +7,6 @@ use crate::{ Array, ArraySize, CellValue, CodeCellValue, Pos, Value, }; use bigdecimal::BigDecimal; -use chrono::Utc; use std::{collections::HashSet, str::FromStr}; impl Sheet { @@ -70,12 +69,12 @@ impl Sheet { self.set_data_table( crate::Pos { x, y }, - Some(DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Single(value), - spill_error: false, - last_modified: chrono::Utc::now(), - }), + Some(DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Single(value), + false, + false, + )), ); } @@ -127,12 +126,12 @@ impl Sheet { self.set_data_table( Pos { x, y }, - Some(DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(array), - spill_error: false, - last_modified: Utc::now(), - }), + Some(DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(array), + false, + false, + )), ); } @@ -171,12 +170,12 @@ impl Sheet { self.set_data_table( Pos { x, y }, - Some(DataTable { - kind: DataTableKind::CodeRun(code_run), - value: Value::Array(array), - spill_error: false, - last_modified: Utc::now(), - }), + Some(DataTable::new( + DataTableKind::CodeRun(code_run), + Value::Array(array), + false, + false, + )), ); } } From 2b0fef8b6eceb115ee8046681c094bbfe8203ab3 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 2 Oct 2024 19:52:22 -0600 Subject: [PATCH 013/373] Apply the header, remove from the value array --- .../active_transactions/pending_transaction.rs | 2 ++ quadratic-core/src/controller/dependencies.rs | 1 + .../execute_operation/execute_formats.rs | 1 + quadratic-core/src/grid/data_table.rs | 14 ++++++-------- quadratic-core/src/values/array.rs | 16 +++++++++++++++- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index 0e21211c39..fdac8cf965 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -376,6 +376,7 @@ mod tests { DataTableKind::CodeRun(code_run), Value::Single(CellValue::Html("html".to_string())), false, + false, ); transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); @@ -397,6 +398,7 @@ mod tests { DataTableKind::CodeRun(code_run), Value::Single(CellValue::Image("image".to_string())), false, + false, ); transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); diff --git a/quadratic-core/src/controller/dependencies.rs b/quadratic-core/src/controller/dependencies.rs index a2570b1b8c..a5cc47ea7e 100644 --- a/quadratic-core/src/controller/dependencies.rs +++ b/quadratic-core/src/controller/dependencies.rs @@ -81,6 +81,7 @@ mod test { DataTableKind::CodeRun(code_run), Value::Single(CellValue::Text("test".to_string())), false, + false, )), ); let sheet_pos_02 = SheetPos { diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs index 366b8ab56c..36e2646bff 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs @@ -216,6 +216,7 @@ mod test { DataTableKind::CodeRun(code_run), Value::Single(CellValue::Image("image".to_string())), false, + false, )), ); diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index c433b288c3..762bf0f626 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -61,7 +61,7 @@ impl DataTable { }; if header { - data_table.apply_header(); + data_table = data_table.apply_header(); } data_table @@ -72,19 +72,17 @@ impl DataTable { self } - pub fn apply_header(&mut self) -> &mut Self { - self.columns = match &self.value { - Value::Array(array) => array.rows().next().map(|row| { - row.iter() + pub fn apply_header(mut self) -> Self { + self.columns = match self.value { + Value::Array(ref mut array) => array.shift().ok().map(|array| { + array + .iter() .map(|value| DataTableColumn::new(value.to_string(), true)) .collect::>() }), - Value::Single(value) => Some(vec![DataTableColumn::new(value.to_string(), true)]), _ => None, }; - // TODO(ddimaria): remove first row from array if it's a header - self } diff --git a/quadratic-core/src/values/array.rs b/quadratic-core/src/values/array.rs index 7a5a5f4d64..1462aa9f34 100644 --- a/quadratic-core/src/values/array.rs +++ b/quadratic-core/src/values/array.rs @@ -1,6 +1,6 @@ use std::{fmt, num::NonZeroU32}; -use anyhow::Result; +use anyhow::{bail, Result}; use bigdecimal::BigDecimal; use itertools::Itertools; use rand::Rng; @@ -207,6 +207,20 @@ impl Array { pub fn rows(&self) -> std::slice::Chunks<'_, CellValue> { self.values.chunks(self.width() as usize) } + /// Remove the first row of the array and return it. + pub fn shift(&mut self) -> Result> { + let width = (self.width() as usize).min(self.values.len()); + let height = NonZeroU32::new(self.height() - 1); + + match height { + Some(h) => { + let first_row = self.values.drain(0..width).collect(); + self.size.h = h; + Ok(first_row) + } + None => bail!("Cannot shift a single row array"), + } + } /// Returns the only cell value in a 1x1 array, or an error if this is not a /// 1x1 array. From 8e9bfaaf2ae987cbec5abab22719edd932200769 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 2 Oct 2024 19:54:42 -0600 Subject: [PATCH 014/373] Refactor passing self --- quadratic-core/src/grid/data_table.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 762bf0f626..3874499a78 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -61,7 +61,7 @@ impl DataTable { }; if header { - data_table = data_table.apply_header(); + data_table.apply_header(); } data_table @@ -72,7 +72,7 @@ impl DataTable { self } - pub fn apply_header(mut self) -> Self { + pub fn apply_header(&mut self) { self.columns = match self.value { Value::Array(ref mut array) => array.shift().ok().map(|array| { array @@ -82,8 +82,6 @@ impl DataTable { }), _ => None, }; - - self } pub fn code_run(&self) -> Option<&CodeRun> { From 8ab24573937fc157b1969b864a618b0f34f10d9f Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 3 Oct 2024 15:39:38 -0600 Subject: [PATCH 015/373] Complete column/heading work + tests --- quadratic-core/src/grid/data_table.rs | 176 +++++++++++++++++- .../src/grid/file/serialize/data_table.rs | 5 +- quadratic-core/src/grid/file/v1_7/schema.rs | 1 + 3 files changed, 176 insertions(+), 6 deletions(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 3874499a78..9f181865e8 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -9,6 +9,7 @@ use crate::grid::CodeRun; use crate::{ Array, ArraySize, CellValue, Pos, Rect, RunError, RunErrorMsg, SheetPos, SheetRect, Value, }; +use anyhow::anyhow; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -16,11 +17,16 @@ use serde::{Deserialize, Serialize}; pub struct DataTableColumn { pub name: String, pub display: bool, + pub value_index: u32, } impl DataTableColumn { - pub fn new(name: String, display: bool) -> Self { - DataTableColumn { name, display } + pub fn new(name: String, display: bool, value_index: u32) -> Self { + DataTableColumn { + name, + display, + value_index, + } } } @@ -51,6 +57,9 @@ impl From<(Import, Array)> for DataTable { } impl DataTable { + /// Creates a new DataTable with the given kind, value, and spill_error, + /// with the ability to lift the first row as column headings. + /// This handles the most common use cases. Use `new_raw` for more control. pub fn new(kind: DataTableKind, value: Value, spill_error: bool, header: bool) -> Self { let mut data_table = DataTable { kind, @@ -61,29 +70,114 @@ impl DataTable { }; if header { - data_table.apply_header(); + data_table.apply_header_from_first_row(); } data_table } + /// Direcly creates a new DataTable with the given kind, value, spill_error, and columns. + pub fn new_raw( + kind: DataTableKind, + value: Value, + spill_error: bool, + columns: Option>, + ) -> Self { + DataTable { + kind, + columns, + value, + spill_error, + last_modified: Utc::now(), + } + } + + /// Apply a new last modified date to the DataTable. pub fn with_last_modified(mut self, last_modified: DateTime) -> Self { self.last_modified = last_modified; self } - pub fn apply_header(&mut self) { + /// Takes the first row of the array and sets it as the column headings. + /// The source array is shifted up one in place. + pub fn apply_header_from_first_row(&mut self) { self.columns = match self.value { Value::Array(ref mut array) => array.shift().ok().map(|array| { array .iter() - .map(|value| DataTableColumn::new(value.to_string(), true)) + .enumerate() + .map(|(i, value)| DataTableColumn::new(value.to_string(), true, i as u32)) .collect::>() }), _ => None, }; } + /// Apply default column headings to the DataTable. + /// For example, the column headings will be "Column 1", "Column 2", etc. + pub fn apply_default_header(&mut self) { + self.columns = match self.value { + Value::Array(ref mut array) => Some( + (1..=array.width()) + .map(|i| DataTableColumn::new(format!("Column {i}"), true, i - 1)) + .collect::>(), + ), + _ => None, + }; + } + + fn check_index(&mut self, index: usize, apply_default_header: bool) -> anyhow::Result<()> { + match self.columns { + Some(ref mut columns) => { + let column_len = columns.len(); + + if index >= column_len { + return Err(anyhow!("Column {index} out of bounds: {column_len}")); + } + } + // there are no columns, so we need to apply default headers first + None => { + apply_default_header.then(|| self.apply_default_header()); + } + }; + + Ok(()) + } + + pub fn set_header_at( + &mut self, + index: usize, + name: String, + display: bool, + ) -> anyhow::Result<()> { + self.check_index(index, true)?; + + self.columns + .as_mut() + .and_then(|columns| columns.get_mut(index)) + .map(|column| { + column.name = name; + column.display = display; + }); + + Ok(()) + } + + pub fn set_header_display_at(&mut self, index: usize, display: bool) -> anyhow::Result<()> { + self.check_index(index, true)?; + + self.columns + .as_mut() + .and_then(|columns| columns.get_mut(index)) + .map(|column| { + column.display = display; + }); + + Ok(()) + } + + /// Helper functtion to get the CodeRun from the DataTable. + /// Returns `None` if the DataTableKind is not CodeRun. pub fn code_run(&self) -> Option<&CodeRun> { match self.kind { DataTableKind::CodeRun(ref code_run) => Some(code_run), @@ -91,6 +185,8 @@ impl DataTable { } } + /// Helper functtion to deterime if the DataTable's CodeRun has an error. + /// Returns `false` if the DataTableKind is not CodeRun or if there is no error. pub fn has_error(&self) -> bool { match self.kind { DataTableKind::CodeRun(ref code_run) => code_run.error.is_some(), @@ -98,6 +194,8 @@ impl DataTable { } } + /// Helper functtion to get the error in the CodeRun from the DataTable. + /// Returns `None` if the DataTableKind is not CodeRun or if there is no error. pub fn get_error(&self) -> Option { self.code_run() .and_then(|code_run| code_run.error.to_owned()) @@ -193,6 +291,74 @@ mod test { use crate::{grid::SheetId, Array}; use serial_test::parallel; + #[test] + #[parallel] + fn test_import_data_table_and_headers() { + let file_name = "test.csv"; + let values = vec![ + vec!["city", "region", "country", "population"], + vec!["Southborough", "MA", "United States", "1000"], + vec!["Denver", "CO", "United States", "10000"], + vec!["Seattle", "WA", "United States", "100"], + ]; + let import = Import::new(file_name.into()); + let kind = DataTableKind::Import(import.clone()); + + // test data table without column headings + let mut data_table = DataTable::from((import.clone(), values.clone().into())); + let expected_values = Value::Array(values.clone().into()); + let expected_data_table = DataTable::new(kind.clone(), expected_values, false, false) + .with_last_modified(data_table.last_modified); + let expected_array_size = ArraySize::new(4, 4).unwrap(); + assert_eq!(data_table, expected_data_table); + assert_eq!(data_table.output_size(), expected_array_size); + + // test default column headings + data_table.apply_default_header(); + let expected_columns = vec![ + DataTableColumn::new("Column 1".into(), true, 0), + DataTableColumn::new("Column 2".into(), true, 1), + DataTableColumn::new("Column 3".into(), true, 2), + DataTableColumn::new("Column 4".into(), true, 3), + ]; + assert_eq!(data_table.columns, Some(expected_columns)); + + // test column headings taken from first row + let value = Value::Array(values.clone().into()); + let mut data_table = DataTable::new(kind.clone(), value, false, true) + .with_last_modified(data_table.last_modified); + + // array height should be 3 since we lift the first row as column headings + let expected_array_size = ArraySize::new(4, 3).unwrap(); + assert_eq!(data_table.output_size(), expected_array_size); + + let expected_columns = vec![ + DataTableColumn::new("city".into(), true, 0), + DataTableColumn::new("region".into(), true, 1), + DataTableColumn::new("country".into(), true, 2), + DataTableColumn::new("population".into(), true, 3), + ]; + assert_eq!(data_table.columns, Some(expected_columns)); + + let expected_values = values + .clone() + .into_iter() + .skip(1) + .collect::>>(); + assert_eq!( + data_table.value.clone().into_array().unwrap(), + expected_values.into() + ); + + // test setting header at index + data_table.set_header_at(0, "new".into(), true).unwrap(); + assert_eq!(data_table.columns.as_ref().unwrap()[0].name, "new"); + + // test setting header display at index + data_table.set_header_display_at(0, false).unwrap(); + assert_eq!(data_table.columns.as_ref().unwrap()[0].display, false); + } + #[test] #[parallel] fn test_output_size() { diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 23a4218c3e..a2d545ae52 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -169,7 +169,9 @@ pub(crate) fn import_data_table_builder( columns: data_table.columns.map(|columns| { columns .into_iter() - .map(|column| DataTableColumn::new(column.name, column.display)) + .map(|column| { + DataTableColumn::new(column.name, column.display, column.value_index) + }) .collect() }), }; @@ -325,6 +327,7 @@ pub(crate) fn export_data_table_runs( .map(|column| current::DataTableColumnSchema { name: column.name, display: column.display, + value_index: column.value_index, }) .collect() }); diff --git a/quadratic-core/src/grid/file/v1_7/schema.rs b/quadratic-core/src/grid/file/v1_7/schema.rs index aac776a9f1..2c83e8d3e9 100644 --- a/quadratic-core/src/grid/file/v1_7/schema.rs +++ b/quadratic-core/src/grid/file/v1_7/schema.rs @@ -141,6 +141,7 @@ pub struct CodeRunSchema { pub struct DataTableColumnSchema { pub name: String, pub display: bool, + pub value_index: u32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] From 47b5d7a85877fc0a42998035813fb7bdc6d7c558 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 3 Oct 2024 18:03:51 -0600 Subject: [PATCH 016/373] Table metadata --- .../pending_transaction.rs | 2 + quadratic-core/src/controller/dependencies.rs | 1 + .../execution/control_transaction.rs | 8 +++- .../execute_operation/execute_formats.rs | 1 + .../src/controller/execution/run_code/mod.rs | 12 ++++- .../execution/run_code/run_formula.rs | 3 ++ .../src/controller/execution/spills.rs | 1 + .../src/controller/operations/import.rs | 14 ++++-- quadratic-core/src/grid/data_table.rs | 47 +++++++++++++++---- .../src/grid/file/serialize/data_table.rs | 35 +++++++------- quadratic-core/src/grid/file/v1_6/file.rs | 17 +++++-- quadratic-core/src/grid/file/v1_7/schema.rs | 2 + quadratic-core/src/grid/sheet/code.rs | 35 +++++++++++++- quadratic-core/src/grid/sheet/rendering.rs | 5 ++ quadratic-core/src/grid/sheet/search.rs | 2 + quadratic-core/src/grid/sheet/sheet_test.rs | 3 ++ 16 files changed, 150 insertions(+), 38 deletions(-) diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index fdac8cf965..15d92e50b0 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -374,6 +374,7 @@ mod tests { let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Single(CellValue::Html("html".to_string())), false, false, @@ -396,6 +397,7 @@ mod tests { let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Single(CellValue::Image("image".to_string())), false, false, diff --git a/quadratic-core/src/controller/dependencies.rs b/quadratic-core/src/controller/dependencies.rs index a5cc47ea7e..be114ce26e 100644 --- a/quadratic-core/src/controller/dependencies.rs +++ b/quadratic-core/src/controller/dependencies.rs @@ -79,6 +79,7 @@ mod test { Pos { x: 0, y: 2 }, Some(DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Single(CellValue::Text("test".to_string())), false, false, diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index e46610242c..ca66d22da7 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -282,7 +282,13 @@ impl GridController { } else { Value::Array(array.into()) }; - let data_table = DataTable::new(DataTableKind::CodeRun(code_run), value, false, false); + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + "Table 1", + value, + false, + false, + ); self.finalize_code_run(&mut transaction, current_sheet_pos, Some(data_table), None); transaction.waiting_for_async = None; diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs index 36e2646bff..6066d1fd59 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs @@ -214,6 +214,7 @@ mod test { Pos { x: 0, y: 0 }, Some(DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Single(CellValue::Image("image".to_string())), false, false, diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 936222df13..4f4c8b097b 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -228,6 +228,7 @@ impl GridController { }; let new_data_table = DataTable::new( DataTableKind::CodeRun(new_code_run), + "Table 1", Value::Single(CellValue::Blank), false, false, @@ -265,6 +266,7 @@ impl GridController { }; return DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Single(CellValue::Blank), // TODO(ddimaria): this will eventually be an empty vec false, false, @@ -328,7 +330,13 @@ impl GridController { cells_accessed: transaction.cells_accessed.clone(), }; - let data_table = DataTable::new(DataTableKind::CodeRun(code_run), value, false, false); + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + "Table 1", + value, + false, + false, + ); transaction.cells_accessed.clear(); data_table } @@ -382,6 +390,7 @@ mod test { }; let new_data_table = DataTable::new( DataTableKind::CodeRun(new_code_run), + "Table 1", Value::Single(CellValue::Text("delete me".to_string())), false, false, @@ -415,6 +424,7 @@ mod test { }; let new_data_table = DataTable::new( DataTableKind::CodeRun(new_code_run), + "Table 1", Value::Single(CellValue::Text("replace me".to_string())), false, false, diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index 457012fc44..c9d693d3a3 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -34,6 +34,7 @@ impl GridController { }; let new_data_table = DataTable::new( DataTableKind::CodeRun(new_code_run), + "Table 1", output.inner, false, false, @@ -263,6 +264,7 @@ mod test { result, DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Single(CellValue::Number(12.into())), false, false @@ -333,6 +335,7 @@ mod test { result, DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Array(array), false, false diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index eca877c50b..f91b04f03a 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -437,6 +437,7 @@ mod tests { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Array(Array::from(vec![vec!["1"]])), false, false, diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 3423add29c..910f36e5a9 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -105,7 +105,10 @@ impl GridController { } // finally add the final operation - let data_table = DataTable::from((import.to_owned(), cell_values)); + let sheet = self + .try_sheet(sheet_id) + .ok_or_else(|| anyhow!("Sheet {sheet_id} not found"))?; + let data_table = DataTable::from((import.to_owned(), cell_values, sheet)); let sheet_pos = SheetPos::from((insert_at, sheet_id)); // this operation must be before the SetCodeRun operations @@ -370,9 +373,11 @@ impl GridController { } } } - + let sheet = self + .try_sheet(sheet_id) + .ok_or_else(|| anyhow!("Sheet {sheet_id} not found"))?; let sheet_pos = SheetPos::from((insert_at, sheet_id)); - let data_table = DataTable::from((import.to_owned(), cell_values)); + let data_table = DataTable::from((import.to_owned(), cell_values, sheet)); // this operation must be before the SetCodeRun operations ops.push(Operation::SetCellValues { @@ -456,7 +461,8 @@ mod test { ]; let import = Import::new(file_name.into()); let cell_value = CellValue::Import(import.clone()); - let mut expected_data_table = DataTable::from((import, values.into())); + let sheet = gc.try_sheet(sheet_id).unwrap(); + let mut expected_data_table = DataTable::from((import, values.into(), sheet)); assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); let data_table = match ops[1].clone() { diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 9f181865e8..30a2013c2a 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -13,6 +13,8 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use super::Sheet; + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct DataTableColumn { pub name: String, @@ -39,16 +41,21 @@ pub enum DataTableKind { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct DataTable { pub kind: DataTableKind, + pub name: String, pub columns: Option>, pub value: Value, + pub readonly: bool, pub spill_error: bool, pub last_modified: DateTime, } -impl From<(Import, Array)> for DataTable { - fn from((import, cell_values): (Import, Array)) -> Self { +impl From<(Import, Array, &Sheet)> for DataTable { + fn from((import, cell_values, sheet): (Import, Array, &Sheet)) -> Self { + let name = sheet.next_data_table_name(); + DataTable::new( DataTableKind::Import(import), + &name, Value::Array(cell_values), false, false, @@ -60,11 +67,24 @@ impl DataTable { /// Creates a new DataTable with the given kind, value, and spill_error, /// with the ability to lift the first row as column headings. /// This handles the most common use cases. Use `new_raw` for more control. - pub fn new(kind: DataTableKind, value: Value, spill_error: bool, header: bool) -> Self { + pub fn new( + kind: DataTableKind, + name: &str, + value: Value, + spill_error: bool, + header: bool, + ) -> Self { + let readonly = match kind { + DataTableKind::CodeRun(_) => true, + DataTableKind::Import(_) => false, + }; + let mut data_table = DataTable { kind, + name: name.into(), columns: None, value, + readonly, spill_error, last_modified: Utc::now(), }; @@ -79,14 +99,18 @@ impl DataTable { /// Direcly creates a new DataTable with the given kind, value, spill_error, and columns. pub fn new_raw( kind: DataTableKind, + name: &str, + columns: Option>, value: Value, + readonly: bool, spill_error: bool, - columns: Option>, ) -> Self { DataTable { kind, + name: name.into(), columns, value, + readonly, spill_error, last_modified: Utc::now(), } @@ -288,12 +312,13 @@ mod test { use std::collections::HashSet; use super::*; - use crate::{grid::SheetId, Array}; + use crate::{controller::GridController, grid::SheetId, Array}; use serial_test::parallel; #[test] #[parallel] fn test_import_data_table_and_headers() { + let sheet = GridController::test().grid().sheets()[0].clone(); let file_name = "test.csv"; let values = vec![ vec!["city", "region", "country", "population"], @@ -305,10 +330,11 @@ mod test { let kind = DataTableKind::Import(import.clone()); // test data table without column headings - let mut data_table = DataTable::from((import.clone(), values.clone().into())); + let mut data_table = DataTable::from((import.clone(), values.clone().into(), &sheet)); let expected_values = Value::Array(values.clone().into()); - let expected_data_table = DataTable::new(kind.clone(), expected_values, false, false) - .with_last_modified(data_table.last_modified); + let expected_data_table = + DataTable::new(kind.clone(), "Table 1", expected_values, false, false) + .with_last_modified(data_table.last_modified); let expected_array_size = ArraySize::new(4, 4).unwrap(); assert_eq!(data_table, expected_data_table); assert_eq!(data_table.output_size(), expected_array_size); @@ -325,7 +351,7 @@ mod test { // test column headings taken from first row let value = Value::Array(values.clone().into()); - let mut data_table = DataTable::new(kind.clone(), value, false, true) + let mut data_table = DataTable::new(kind.clone(), "Table 1", value, false, true) .with_last_modified(data_table.last_modified); // array height should be 3 since we lift the first row as column headings @@ -375,6 +401,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Single(CellValue::Number(1.into())), false, false, @@ -406,6 +433,7 @@ mod test { let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Array(Array::new_empty(ArraySize::new(10, 11).unwrap())), false, false, @@ -442,6 +470,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Array(Array::new_empty(ArraySize::new(10, 11).unwrap())), true, false, diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index a2d545ae52..961570d037 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -163,6 +163,8 @@ pub(crate) fn import_data_table_builder( }) } }, + name: data_table.name, + readonly: data_table.readonly, last_modified: data_table.last_modified.unwrap_or(Utc::now()), // this is required but fall back to now if failed spill_error: data_table.spill_error, value, @@ -332,27 +334,26 @@ pub(crate) fn export_data_table_runs( .collect() }); - let data_table = match data_table.kind { + let kind = match data_table.kind { DataTableKind::CodeRun(code_run) => { let code_run = export_code_run(code_run); - - current::DataTableSchema { - kind: current::DataTableKindSchema::CodeRun(code_run), - columns, - last_modified: Some(data_table.last_modified), - spill_error: data_table.spill_error, - value, - } + current::DataTableKindSchema::CodeRun(code_run) } - DataTableKind::Import(import) => current::DataTableSchema { - kind: current::DataTableKindSchema::Import(current::ImportSchema { + DataTableKind::Import(import) => { + current::DataTableKindSchema::Import(current::ImportSchema { file_name: import.file_name, - }), - columns, - last_modified: Some(data_table.last_modified), - spill_error: data_table.spill_error, - value, - }, + }) + } + }; + + let data_table = current::DataTableSchema { + kind, + name: data_table.name, + columns, + readonly: data_table.readonly, + last_modified: Some(data_table.last_modified), + spill_error: data_table.spill_error, + value, }; (current::PosSchema::from(pos), data_table) diff --git a/quadratic-core/src/grid/file/v1_6/file.rs b/quadratic-core/src/grid/file/v1_6/file.rs index a7bb425fcc..0d053ca877 100644 --- a/quadratic-core/src/grid/file/v1_6/file.rs +++ b/quadratic-core/src/grid/file/v1_6/file.rs @@ -76,11 +76,13 @@ fn upgrade_borders(borders: current::Borders) -> Result { } fn upgrade_code_runs( - code_runs: Vec<(current::Pos, current::CodeRun)>, + sheet: current::Sheet, ) -> Result> { - code_runs + sheet + .code_runs .into_iter() - .map(|(pos, code_run)| { + .enumerate() + .map(|(i, (pos, code_run))| { let error = if let current::CodeRunResult::Err(error) = &code_run.result { let new_error_msg = match error.msg.to_owned() { current::RunErrorMsg::PythonError(msg) => { @@ -206,8 +208,10 @@ fn upgrade_code_runs( }; let new_data_table = v1_7::DataTableSchema { kind: v1_7::DataTableKindSchema::CodeRun(new_code_run), + name: format!("Table {}", i), columns: None, value, + readonly: true, spill_error: code_run.spill_error, last_modified: code_run.last_modified, }; @@ -217,6 +221,9 @@ fn upgrade_code_runs( } pub fn upgrade_sheet(sheet: current::Sheet) -> Result { + let data_tables = upgrade_code_runs(sheet.clone())?; + let borders = upgrade_borders(sheet.borders.clone())?; + Ok(v1_7::SheetSchema { id: sheet.id, name: sheet.name, @@ -224,13 +231,13 @@ pub fn upgrade_sheet(sheet: current::Sheet) -> Result { order: sheet.order, offsets: sheet.offsets, columns: sheet.columns, - data_tables: upgrade_code_runs(sheet.code_runs)?, + data_tables, formats_all: sheet.formats_all, formats_columns: sheet.formats_columns, formats_rows: sheet.formats_rows, rows_resize: sheet.rows_resize, validations: sheet.validations, - borders: upgrade_borders(sheet.borders)?, + borders, }) } diff --git a/quadratic-core/src/grid/file/v1_7/schema.rs b/quadratic-core/src/grid/file/v1_7/schema.rs index 2c83e8d3e9..2fa300b7a6 100644 --- a/quadratic-core/src/grid/file/v1_7/schema.rs +++ b/quadratic-core/src/grid/file/v1_7/schema.rs @@ -153,8 +153,10 @@ pub enum DataTableKindSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DataTableSchema { pub kind: DataTableKindSchema, + pub name: String, pub columns: Option>, pub value: OutputValueSchema, + pub readonly: bool, pub spill_error: bool, pub last_modified: Option>, } diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 5af999e3c2..86a49c8800 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -8,10 +8,24 @@ use crate::{ js_types::{JsCodeCell, JsReturnInfo}, CodeCellLanguage, DataTableKind, RenderSize, }, - CellValue, Pos, Rect, + CellValue, Pos, Rect, Value, }; impl Sheet { + pub fn new_data_table( + &mut self, + pos: Pos, + kind: DataTableKind, + value: Value, + spill_error: bool, + header: bool, + ) -> Option { + let name = self.next_data_table_name(); + let data_table = DataTable::new(kind, &name, value, spill_error, header); + + self.set_data_table(pos, Some(data_table)) + } + /// Sets or deletes a code run. /// /// Returns the old value if it was set. @@ -23,6 +37,20 @@ impl Sheet { } } + pub fn next_data_table_name(&self) -> String { + let mut i = self.data_tables.len() + 1; + + loop { + let name = format!("Table {}", i); + + if !self.data_tables.values().any(|table| table.name == name) { + return name; + } + + i += 1; + } + } + /// Returns a DatatTable at a Pos pub fn data_table(&self, pos: Pos) -> Option<&DataTable> { self.data_tables.get(&pos) @@ -277,6 +305,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Single(CellValue::Number(BigDecimal::from(2))), false, false, @@ -306,6 +335,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Single(CellValue::Number(BigDecimal::from(2))), false, false, @@ -344,6 +374,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Array(Array::from(vec![vec!["1", "2", "3"]])), false, false, @@ -435,6 +466,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Array(Array::from(vec![vec!["1"], vec!["2"], vec!["3"]])), false, false, @@ -470,6 +502,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1", Value::Array(Array::from(vec![vec!["1", "2", "3'"]])), false, false, diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index ba33e374e6..5a88e88ce1 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -663,6 +663,7 @@ mod tests { Pos { x: 2, y: 3 }, Some(DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1".into(), Value::Single(CellValue::Text("hello".to_string())), false, false, @@ -880,6 +881,7 @@ mod tests { // data_table is always 3x2 let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1".into(), Value::Array(vec![vec!["1", "2", "3"], vec!["4", "5", "6"]].into()), false, false, @@ -934,6 +936,7 @@ mod tests { let code_run = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1".into(), Value::Single(CellValue::Number(1.into())), false, false, @@ -1058,6 +1061,7 @@ mod tests { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1".into(), Value::Single(CellValue::Number(2.into())), false, false, @@ -1107,6 +1111,7 @@ mod tests { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1".into(), Value::Single(CellValue::Image(image.clone())), false, false, diff --git a/quadratic-core/src/grid/sheet/search.rs b/quadratic-core/src/grid/sheet/search.rs index 89927815d4..70b49d6cb5 100644 --- a/quadratic-core/src/grid/sheet/search.rs +++ b/quadratic-core/src/grid/sheet/search.rs @@ -467,6 +467,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1".into(), Value::Single("world".into()), false, false, @@ -510,6 +511,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1".into(), Value::Array(Array::from(vec![ vec!["abc", "def", "ghi"], vec!["jkl", "mno", "pqr"], diff --git a/quadratic-core/src/grid/sheet/sheet_test.rs b/quadratic-core/src/grid/sheet/sheet_test.rs index 01e01ae62a..2fff9d6e48 100644 --- a/quadratic-core/src/grid/sheet/sheet_test.rs +++ b/quadratic-core/src/grid/sheet/sheet_test.rs @@ -71,6 +71,7 @@ impl Sheet { crate::Pos { x, y }, Some(DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1".into(), Value::Single(value), false, false, @@ -128,6 +129,7 @@ impl Sheet { Pos { x, y }, Some(DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1".into(), Value::Array(array), false, false, @@ -172,6 +174,7 @@ impl Sheet { Pos { x, y }, Some(DataTable::new( DataTableKind::CodeRun(code_run), + "Table 1".into(), Value::Array(array), false, false, From 78c7b043eb18ea2395b6860a6b83690898cf2490 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 4 Oct 2024 16:35:04 -0600 Subject: [PATCH 017/373] Edit data tables --- .../interaction/pointer/doubleClickCell.ts | 3 + .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 2 + .../src/app/quadratic-core-types/index.d.ts | 2 +- .../active_transactions/transaction_name.rs | 1 + .../execute_operation/execute_code.rs | 40 +----- .../execute_operation/execute_data_table.rs | 124 ++++++++++++++++++ .../execution/execute_operation/mod.rs | 2 + .../src/controller/operations/cell_value.rs | 10 +- .../src/controller/operations/code_cell.rs | 11 ++ .../src/controller/operations/operation.rs | 18 ++- .../src/controller/user_actions/cells.rs | 45 ++++++- quadratic-core/src/grid/data_table.rs | 19 +++ quadratic-core/src/grid/sheet.rs | 10 +- quadratic-core/src/grid/sheet/code.rs | 15 +++ quadratic-core/src/pos.rs | 8 ++ .../src/wasm_bindings/controller/cells.rs | 37 +++--- 16 files changed, 274 insertions(+), 73 deletions(-) create mode 100644 quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts b/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts index 3d9b8c69ee..d4d1051632 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts @@ -26,6 +26,7 @@ export async function doubleClickCell(options: { // Open the correct code editor if (language) { const formula = language === 'Formula'; + const file_import = language === 'Import'; if (settings.editorInteractionState.showCodeEditor) { settings.setEditorInteractionState({ @@ -49,6 +50,8 @@ export async function doubleClickCell(options: { sheets.sheet.cursor.changePosition({ cursorPosition: { x: column, y: row } }); } settings.changeInput(true, cell); + } else if (hasPermission && file_import) { + settings.changeInput(true, cell); } else { settings.setEditorInteractionState({ ...settings.editorInteractionState, diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index c220852be3..a5b130b41a 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -178,9 +178,11 @@ class PixiAppSettings { this._input.y !== undefined && this._input.sheetId !== undefined ) { + console.log('here'); pixiApp.cellsSheets.showLabel(this._input.x, this._input.y, this._input.sheetId, true); } if (input === true) { + console.log('here 2'); const x = sheets.sheet.cursor.cursorPosition.x; const y = sheets.sheet.cursor.cursorPosition.y; if (multiplayer.cellIsBeingEdited(x, y, sheets.sheet.id)) { diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 4958ac9aa2..2d9f93b146 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -14,7 +14,7 @@ export interface CellRef { sheet: string | null, x: CellRefCoord, y: CellRefCoor export type CellRefCoord = { "type": "Relative", "coord": bigint } | { "type": "Absolute", "coord": bigint }; export type CellVerticalAlign = "top" | "middle" | "bottom"; export type CellWrap = "overflow" | "wrap" | "clip"; -export type CodeCellLanguage = "Python" | "Formula" | { "Connection": { kind: ConnectionKind, id: string, } } | "Javascript"; +export type CodeCellLanguage = "Python" | "Formula" | { "Connection": { kind: ConnectionKind, id: string, } } | "Javascript" | "Import"; export interface ColumnRow { column: number, row: number, } export type ConnectionKind = "POSTGRES" | "MYSQL" | "MSSQL"; export type DateTimeRange = { "DateRange": [bigint | null, bigint | null] } | { "DateEqual": Array } | { "DateNotEqual": Array } | { "TimeRange": [number | null, number | null] } | { "TimeEqual": Array } | { "TimeNotEqual": Array }; diff --git a/quadratic-core/src/controller/active_transactions/transaction_name.rs b/quadratic-core/src/controller/active_transactions/transaction_name.rs index 31d01b87ea..c72bb2216b 100644 --- a/quadratic-core/src/controller/active_transactions/transaction_name.rs +++ b/quadratic-core/src/controller/active_transactions/transaction_name.rs @@ -10,6 +10,7 @@ pub enum TransactionName { SetBorders, SetCells, SetFormats, + SetDataTableAt, CutClipboard, PasteClipboard, SetCode, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs index e91d4a2374..9f2bc7499a 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs @@ -4,7 +4,7 @@ use crate::{ operations::operation::Operation, GridController, }, grid::CodeCellLanguage, - CellValue, Pos, Rect, SheetPos, SheetRect, + CellValue, Pos, SheetPos, SheetRect, }; impl GridController { @@ -38,44 +38,6 @@ impl GridController { }); } - // delete any code runs within the sheet_rect. - pub(super) fn check_deleted_data_tables( - &mut self, - transaction: &mut PendingTransaction, - sheet_rect: &SheetRect, - ) { - let sheet_id = sheet_rect.sheet_id; - let Some(sheet) = self.grid.try_sheet(sheet_id) else { - // sheet may have been deleted - return; - }; - let rect: Rect = (*sheet_rect).into(); - let data_tables_to_delete: Vec = sheet - .data_tables - .iter() - .filter_map(|(pos, _)| { - // only delete code runs that are within the sheet_rect - if rect.contains(*pos) { - // only delete when there's not another code cell in the same position (this maintains the original output until a run completes) - if let Some(value) = sheet.cell_value(*pos) { - if matches!(value, CellValue::Code(_)) { - None - } else { - Some(*pos) - } - } else { - Some(*pos) - } - } else { - None - } - }) - .collect(); - data_tables_to_delete.iter().for_each(|pos| { - self.finalize_code_run(transaction, pos.to_sheet_pos(sheet_id), None, None); - }); - } - pub(super) fn execute_set_code_run( &mut self, transaction: &mut PendingTransaction, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs new file mode 100644 index 0000000000..c282595035 --- /dev/null +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -0,0 +1,124 @@ +use crate::{ + controller::{ + active_transactions::pending_transaction::PendingTransaction, + operations::operation::Operation, GridController, + }, + CellValue, Pos, Rect, SheetRect, +}; + +impl GridController { + // delete any code runs within the sheet_rect. + pub(super) fn check_deleted_data_tables( + &mut self, + transaction: &mut PendingTransaction, + sheet_rect: &SheetRect, + ) { + let sheet_id = sheet_rect.sheet_id; + let Some(sheet) = self.grid.try_sheet(sheet_id) else { + // sheet may have been deleted + return; + }; + let rect: Rect = (*sheet_rect).into(); + let data_tables_to_delete: Vec = sheet + .data_tables + .iter() + .filter_map(|(pos, _)| { + // only delete code runs that are within the sheet_rect + if rect.contains(*pos) { + // only delete when there's not another code cell in the same position (this maintains the original output until a run completes) + if let Some(value) = sheet.cell_value(*pos) { + if matches!(value, CellValue::Code(_)) { + None + } else { + Some(*pos) + } + } else { + Some(*pos) + } + } else { + None + } + }) + .collect(); + data_tables_to_delete.iter().for_each(|pos| { + self.finalize_code_run(transaction, pos.to_sheet_pos(sheet_id), None, None); + }); + } + + pub(super) fn execute_set_data_table_at( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) { + if let Operation::SetDataTableAt { sheet_pos, values } = op { + let sheet_id = sheet_pos.sheet_id; + + if let Some(sheet) = self.try_sheet_mut(sheet_id) { + let pos: Pos = sheet_pos.into(); + + if values.size() != 1 { + return dbgjs!("Only single values are supported for now"); + } + + let value = values.get(0, 0).cloned().unwrap_or_else(|| { + return dbgjs!("No cell value found in CellValues at (0, 0)"); + }); + + let old_value = sheet.get_code_cell_value(pos).unwrap_or_else(|| { + return dbgjs!(format!("No cell value found in Sheet at {:?}", pos)); + }); + + sheet.set_code_cell_value(pos, value.to_owned()); + let sheet_rect = SheetRect::from_numbers( + sheet_pos.x, + sheet_pos.y, + values.w as i64, + values.h as i64, + sheet_id, + ); + + if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() { + self.send_updated_bounds_rect(&sheet_rect, false); + self.add_dirty_hashes_from_sheet_rect(transaction, sheet_rect); + + if transaction.is_user() { + if let Some(sheet) = self.try_sheet(sheet_id) { + let rows = sheet.get_rows_with_wrap_in_rect(&sheet_rect.into()); + if !rows.is_empty() { + let resize_rows = + transaction.resize_rows.entry(sheet_id).or_default(); + resize_rows.extend(rows); + } + } + } + } + + if transaction.is_user_undo_redo() { + transaction + .forward_operations + .push(Operation::SetDataTableAt { + sheet_pos, + values: value.into(), + }); + + transaction + .reverse_operations + .push(Operation::SetDataTableAt { + sheet_pos, + values: old_value.into(), + }); + + if transaction.is_user() { + self.add_compute_operations(transaction, &sheet_rect, Some(sheet_pos)); + self.check_all_spills(transaction, sheet_pos.sheet_id, true); + } + } + + transaction.generate_thumbnail |= self.thumbnail_dirty_sheet_rect(&sheet_rect); + }; + } + } +} + +#[cfg(test)] +mod tests {} diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 121be571c6..de3809a4f6 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -6,6 +6,7 @@ mod execute_borders; mod execute_code; mod execute_col_rows; mod execute_cursor; +mod execute_data_table; mod execute_formats; mod execute_move_cells; mod execute_offsets; @@ -24,6 +25,7 @@ impl GridController { match op { Operation::SetCellValues { .. } => self.execute_set_cell_values(transaction, op), Operation::SetCodeRun { .. } => self.execute_set_code_run(transaction, op), + Operation::SetDataTableAt { .. } => self.execute_set_data_table_at(transaction, op), Operation::ComputeCode { .. } => self.execute_compute_code(transaction, op), Operation::SetCellFormats { .. } => self.execute_set_cell_formats(transaction, op), Operation::SetCellFormatsSelection { .. } => { diff --git a/quadratic-core/src/controller/operations/cell_value.rs b/quadratic-core/src/controller/operations/cell_value.rs index 35a0c427c2..b5d4b28cf2 100644 --- a/quadratic-core/src/controller/operations/cell_value.rs +++ b/quadratic-core/src/controller/operations/cell_value.rs @@ -18,7 +18,7 @@ const MAX_BIG_DECIMAL_SIZE: usize = 10000000; impl GridController { /// Convert string to a cell_value and generate necessary operations pub(super) fn string_to_cell_value( - &mut self, + &self, sheet_pos: SheetPos, value: &str, ) -> (Vec, CellValue) { @@ -195,7 +195,7 @@ mod test { #[test] #[parallel] fn boolean_to_cell_value() { - let mut gc = GridController::test(); + let gc = GridController::test(); let sheet_pos = SheetPos { x: 1, y: 2, @@ -229,7 +229,7 @@ mod test { #[test] #[parallel] fn number_to_cell_value() { - let mut gc = GridController::test(); + let gc = GridController::test(); let sheet_pos = SheetPos { x: 1, y: 2, @@ -264,7 +264,7 @@ mod test { #[test] #[parallel] fn formula_to_cell_value() { - let mut gc = GridController::test(); + let gc = GridController::test(); let sheet_pos = SheetPos { x: 1, y: 2, @@ -305,7 +305,7 @@ mod test { #[test] #[parallel] fn problematic_number() { - let mut gc = GridController::test(); + let gc = GridController::test(); let value = "980E92207901934"; let (_, cell_value) = gc.string_to_cell_value( SheetPos { diff --git a/quadratic-core/src/controller/operations/code_cell.rs b/quadratic-core/src/controller/operations/code_cell.rs index a53f2abfcf..fff27a0822 100644 --- a/quadratic-core/src/controller/operations/code_cell.rs +++ b/quadratic-core/src/controller/operations/code_cell.rs @@ -29,6 +29,17 @@ impl GridController { ] } + pub fn set_data_table_operations_at( + &self, + sheet_pos: SheetPos, + values: String, + ) -> Vec { + let (_, cell_value) = &self.string_to_cell_value(sheet_pos, &values); + let values = CellValues::from(cell_value.to_owned()); + + vec![Operation::SetDataTableAt { sheet_pos, values }] + } + // Returns whether a code_cell is dependent on another code_cell. fn is_dependent_on(&self, current: &DataTable, other_pos: SheetPos) -> bool { current diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index a47d947b53..8e2e4b0b1d 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -5,9 +5,12 @@ use uuid::Uuid; use crate::{ cell_values::CellValues, grid::{ - file::sheet_schema::SheetSchema, formats::Formats, formatting::CellFmtArray, - js_types::JsRowHeight, sheet::borders::BorderStyleCellUpdates, - sheet::validations::validation::Validation, DataTable, Sheet, SheetBorders, SheetId, + file::sheet_schema::SheetSchema, + formats::Formats, + formatting::CellFmtArray, + js_types::JsRowHeight, + sheet::{borders::BorderStyleCellUpdates, validations::validation::Validation}, + DataTable, Sheet, SheetBorders, SheetId, }, selection::Selection, SheetPos, SheetRect, @@ -38,6 +41,10 @@ pub enum Operation { code_run: Option, index: usize, }, + SetDataTableAt { + sheet_pos: SheetPos, + values: CellValues, + }, ComputeCode { sheet_pos: SheetPos, }, @@ -189,6 +196,11 @@ impl fmt::Display for Operation { "SetCellRun {{ sheet_pos: {} code_cell_value: {:?} index: {} }}", sheet_pos, run, index ), + Operation::SetDataTableAt { sheet_pos, values } => write!( + fmt, + "SetDataTableAt {{ sheet_pos: {} values: {:?} }}", + sheet_pos, values + ), Operation::SetCellFormats { .. } => write!(fmt, "SetCellFormats {{ todo }}",), Operation::SetCellFormatsSelection { selection, formats } => { write!( diff --git a/quadratic-core/src/controller/user_actions/cells.rs b/quadratic-core/src/controller/user_actions/cells.rs index 61bf6155c7..c7738f332c 100644 --- a/quadratic-core/src/controller/user_actions/cells.rs +++ b/quadratic-core/src/controller/user_actions/cells.rs @@ -1,10 +1,41 @@ +use anyhow::{anyhow, Result}; + use crate::controller::active_transactions::transaction_name::TransactionName; use crate::controller::GridController; - use crate::selection::Selection; -use crate::SheetPos; +use crate::{CellValue, SheetPos}; impl GridController { + // Using sheet_pos, either set a cell value or a data table value + pub fn set_value( + &mut self, + sheet_pos: SheetPos, + value: String, + cursor: Option, + ) -> Result<()> { + let sheet = self + .try_sheet_mut(sheet_pos.sheet_id) + .ok_or_else(|| anyhow!("Sheet not found"))?; + + let cell_value = sheet + .get_column(sheet_pos.x) + .and_then(|column| column.values.get(&sheet_pos.y)); + + let is_data_table = if let Some(cell_value) = cell_value { + let var_name = matches!(cell_value, CellValue::Code(_) | CellValue::Import(_)); + var_name + } else { + true + }; + + match is_data_table { + true => self.set_data_table_value(sheet_pos, value, cursor), + false => self.set_cell_value(sheet_pos, value, cursor), + }; + + Ok(()) + } + /// Starts a transaction to set the value of a cell by converting a user's String input pub fn set_cell_value(&mut self, sheet_pos: SheetPos, value: String, cursor: Option) { let ops = self.set_cell_value_operations(sheet_pos, value); @@ -39,6 +70,16 @@ impl GridController { self.start_user_transaction(ops, cursor, TransactionName::SetCells); } + pub fn set_data_table_value( + &mut self, + sheet_pos: SheetPos, + value: String, + cursor: Option, + ) { + let ops = self.set_data_table_operations_at(sheet_pos, value); + self.start_user_transaction(ops, cursor, TransactionName::SetDataTableAt); + } + /// Starts a transaction to deletes the cell values and code in a given rect and updates dependent cells. pub fn delete_cells(&mut self, selection: &Selection, cursor: Option) { let ops = self.delete_cells_operations(selection); diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 30a2013c2a..89e2f2eb56 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -263,6 +263,25 @@ impl DataTable { } } + pub fn set_cell_value_at(&mut self, x: u32, y: u32, value: CellValue) -> Option { + if !self.spill_error { + match self.value { + Value::Single(_) => { + self.value = Value::Single(value.to_owned()); + } + Value::Array(ref mut a) => { + // TODO(ddimaria): handle error + a.set(x, y, value.to_owned()).unwrap(); + } + Value::Tuple(_) => {} + } + + return Some(value); + } + + None + } + /// Returns the size of the output array, or defaults to `_1X1` (since output always includes the code_cell). /// Note: this does not take spill_error into account. pub fn output_size(&self) -> ArraySize { diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 18c0ea5433..3aa501ef1d 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -266,10 +266,12 @@ impl Sheet { if let Some(cell_value) = cell_value { match cell_value { - CellValue::Blank | CellValue::Code(_) => match self.data_tables.get(&pos) { - Some(run) => run.get_cell_for_formula(0, 0), - None => CellValue::Blank, - }, + CellValue::Blank | CellValue::Code(_) | CellValue::Import(_) => { + match self.data_tables.get(&pos) { + Some(data_table) => data_table.get_cell_for_formula(0, 0), + None => CellValue::Blank, + } + } other => other.clone(), } } else { diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 86a49c8800..51f6e4b173 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -118,6 +118,21 @@ impl Sheet { }) } + pub fn set_code_cell_value(&mut self, pos: Pos, value: CellValue) -> Option { + self.data_tables + .iter_mut() + .find(|(code_cell_pos, data_table)| { + data_table.output_rect(**code_cell_pos, false).contains(pos) + }) + .and_then(|(code_cell_pos, data_table)| { + let x = (pos.x - code_cell_pos.x) as u32; + let y = (pos.y - code_cell_pos.y) as u32; + data_table.set_cell_value_at(x, y, value.to_owned()); + + Some(value) + }) + } + pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { self.data_tables .iter() diff --git a/quadratic-core/src/pos.rs b/quadratic-core/src/pos.rs index c8d6dfbe10..d84007bdc3 100644 --- a/quadratic-core/src/pos.rs +++ b/quadratic-core/src/pos.rs @@ -77,6 +77,14 @@ impl From<(i64, i64)> for Pos { Pos { x: pos.0, y: pos.1 } } } +impl From<(i32, i32)> for Pos { + fn from(pos: (i32, i32)) -> Self { + Pos { + x: pos.0 as i64, + y: pos.1 as i64, + } + } +} impl From for Pos { fn from(sheet_pos: SheetPos) -> Self { Pos { diff --git a/quadratic-core/src/wasm_bindings/controller/cells.rs b/quadratic-core/src/wasm_bindings/controller/cells.rs index c8008f35d5..211d04e998 100644 --- a/quadratic-core/src/wasm_bindings/controller/cells.rs +++ b/quadratic-core/src/wasm_bindings/controller/cells.rs @@ -17,20 +17,14 @@ impl GridController { y: i32, value: String, cursor: Option, - ) -> Result { - let pos = Pos { - x: x as i64, - y: y as i64, - }; - if let Ok(sheet_id) = SheetId::from_str(&sheet_id) { - Ok(serde_wasm_bindgen::to_value(&self.set_cell_value( - pos.to_sheet_pos(sheet_id), - value, - cursor, - ))?) - } else { - Err(JsValue::from_str("Invalid sheet id")) - } + ) -> Result<(), JsValue> { + let pos = Pos::from((x, y)); + let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; + + self.set_value((pos, sheet_id).into(), value.to_owned(), cursor) + .map_err(|e| e.to_string())?; + + Ok(()) } /// changes the decimal places @@ -68,11 +62,16 @@ impl GridController { let sheet = self .try_sheet_from_string_id(sheet_id) .ok_or(JsValue::UNDEFINED)?; - if let Some(value) = sheet.cell_value(pos) { - Ok(value.to_edit()) - } else { - Ok(String::from("")) - } + + let val = sheet.get_cell_for_formula(pos); + dbgjs!(format!("getEditCell {} {:?}", &pos, val)); + Ok(val.to_edit()) + + // if let Some(value) = sheet.cell_value(pos) { + // Ok(value.to_edit()) + // } else { + // Ok(String::from("")) + // } } /// gets the display value for a cell From 3687aecb14a6ad561c98a0a2a033bcb1e60a1acd Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 4 Oct 2024 17:25:20 -0600 Subject: [PATCH 018/373] Cleanup --- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 2 -- .../src/app/helpers/codeCellLanguage.ts | 1 + .../src/app/quadratic-core-types/index.d.ts | 2 +- .../app/ui/menus/CodeEditor/CodeEditorBody.tsx | 1 + .../execute_operation/execute_data_table.rs | 13 ++++++------- .../src/controller/user_actions/cells.rs | 15 +++++++++++---- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index a5b130b41a..c220852be3 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -178,11 +178,9 @@ class PixiAppSettings { this._input.y !== undefined && this._input.sheetId !== undefined ) { - console.log('here'); pixiApp.cellsSheets.showLabel(this._input.x, this._input.y, this._input.sheetId, true); } if (input === true) { - console.log('here 2'); const x = sheets.sheet.cursor.cursorPosition.x; const y = sheets.sheet.cursor.cursorPosition.y; if (multiplayer.cellIsBeingEdited(x, y, sheets.sheet.id)) { diff --git a/quadratic-client/src/app/helpers/codeCellLanguage.ts b/quadratic-client/src/app/helpers/codeCellLanguage.ts index 73cd2c7e8e..3791d64420 100644 --- a/quadratic-client/src/app/helpers/codeCellLanguage.ts +++ b/quadratic-client/src/app/helpers/codeCellLanguage.ts @@ -4,6 +4,7 @@ const codeCellsById = { Formula: { id: 'Formula', label: 'Formula', type: undefined }, Javascript: { id: 'Javascript', label: 'JavaScript', type: undefined }, Python: { id: 'Python', label: 'Python', type: undefined }, + Import: { id: 'Import', label: 'Import', type: undefined }, POSTGRES: { id: 'POSTGRES', label: 'Postgres', type: 'connection' }, MYSQL: { id: 'MYSQL', label: 'MySQL', type: 'connection' }, MSSQL: { id: 'MSSQL', label: 'MsSQL', type: 'connection' }, diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 2d9f93b146..d7e3cf06fd 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -64,7 +64,7 @@ export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; export type TextMatch = { "Exactly": TextCase } | { "Contains": TextCase } | { "NotContains": TextCase } | { "TextLength": { min: number | null, max: number | null, } }; -export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; +export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; export interface TransientResize { row: bigint | null, column: bigint | null, old_size: number, new_size: number, } export interface Validation { id: string, selection: Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } export interface ValidationDateTime { ignore_blank: boolean, require_date: boolean, require_time: boolean, prohibit_date: boolean, prohibit_time: boolean, ranges: Array, } diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorBody.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorBody.tsx index f2a6a40f9b..9a667f2610 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorBody.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorBody.tsx @@ -50,6 +50,7 @@ let registered: Record, boolean> = { Formula: false, Python: false, Javascript: false, + Import: false, }; export const CodeEditorBody = (props: Props) => { diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index c282595035..276ce26b15 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -52,21 +52,20 @@ impl GridController { ) { if let Operation::SetDataTableAt { sheet_pos, values } = op { let sheet_id = sheet_pos.sheet_id; + let pos: Pos = sheet_pos.into(); if let Some(sheet) = self.try_sheet_mut(sheet_id) { - let pos: Pos = sheet_pos.into(); - if values.size() != 1 { return dbgjs!("Only single values are supported for now"); } - let value = values.get(0, 0).cloned().unwrap_or_else(|| { + let value = if let Some(value) = values.get(0, 0).cloned() { + value + } else { return dbgjs!("No cell value found in CellValues at (0, 0)"); - }); + }; - let old_value = sheet.get_code_cell_value(pos).unwrap_or_else(|| { - return dbgjs!(format!("No cell value found in Sheet at {:?}", pos)); - }); + let old_value = sheet.get_code_cell_value(pos).unwrap_or(CellValue::Blank); sheet.set_code_cell_value(pos, value.to_owned()); let sheet_rect = SheetRect::from_numbers( diff --git a/quadratic-core/src/controller/user_actions/cells.rs b/quadratic-core/src/controller/user_actions/cells.rs index c7738f332c..f8726a1acd 100644 --- a/quadratic-core/src/controller/user_actions/cells.rs +++ b/quadratic-core/src/controller/user_actions/cells.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, Result}; use crate::controller::active_transactions::transaction_name::TransactionName; use crate::controller::GridController; use crate::selection::Selection; -use crate::{CellValue, SheetPos}; +use crate::{CellValue, Pos, SheetPos}; impl GridController { // Using sheet_pos, either set a cell value or a data table value @@ -22,10 +22,17 @@ impl GridController { .and_then(|column| column.values.get(&sheet_pos.y)); let is_data_table = if let Some(cell_value) = cell_value { - let var_name = matches!(cell_value, CellValue::Code(_) | CellValue::Import(_)); - var_name + matches!(cell_value, CellValue::Code(_) | CellValue::Import(_)) } else { - true + sheet + .data_tables + .iter() + .find(|(code_cell_pos, data_table)| { + data_table + .output_rect(**code_cell_pos, false) + .contains(Pos::from(sheet_pos)) + }) + .is_some() }; match is_data_table { From 589e091bd595a3330a8670fabc9185ed506d693a Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Sat, 5 Oct 2024 08:01:37 -0600 Subject: [PATCH 019/373] More cleanup --- quadratic-client/.env.docker | 2 +- .../execution/execute_operation/execute_data_table.rs | 5 +++++ quadratic-core/src/controller/operations/code_cell.rs | 4 ++-- quadratic-core/src/wasm_bindings/controller/cells.rs | 9 +-------- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/quadratic-client/.env.docker b/quadratic-client/.env.docker index 944db6d51b..18549755a0 100644 --- a/quadratic-client/.env.docker +++ b/quadratic-client/.env.docker @@ -1,4 +1,4 @@ -VITE_DEBUG=1 // use =1 to enable debug flags +VITE_DEBUG=1 VITE_QUADRATIC_API_URL=http://localhost:8000 VITE_QUADRATIC_MULTIPLAYER_URL=ws://localhost:3001/ws VITE_QUADRATIC_CONNECTION_URL=http://localhost:3003 diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 276ce26b15..97c1f47e3d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -67,6 +67,11 @@ impl GridController { let old_value = sheet.get_code_cell_value(pos).unwrap_or(CellValue::Blank); + dbgjs!(format!( + "SetDataTableAt sheet_pos: {:?} old_value: {:?} old_value: {:?}", + sheet_pos, old_value, value + )); + sheet.set_code_cell_value(pos, value.to_owned()); let sheet_rect = SheetRect::from_numbers( sheet_pos.x, diff --git a/quadratic-core/src/controller/operations/code_cell.rs b/quadratic-core/src/controller/operations/code_cell.rs index fff27a0822..de091ddb08 100644 --- a/quadratic-core/src/controller/operations/code_cell.rs +++ b/quadratic-core/src/controller/operations/code_cell.rs @@ -34,8 +34,8 @@ impl GridController { sheet_pos: SheetPos, values: String, ) -> Vec { - let (_, cell_value) = &self.string_to_cell_value(sheet_pos, &values); - let values = CellValues::from(cell_value.to_owned()); + let (_, cell_value) = self.string_to_cell_value(sheet_pos, &values); + let values = CellValues::from(cell_value); vec![Operation::SetDataTableAt { sheet_pos, values }] } diff --git a/quadratic-core/src/wasm_bindings/controller/cells.rs b/quadratic-core/src/wasm_bindings/controller/cells.rs index 211d04e998..27f541f987 100644 --- a/quadratic-core/src/wasm_bindings/controller/cells.rs +++ b/quadratic-core/src/wasm_bindings/controller/cells.rs @@ -62,16 +62,9 @@ impl GridController { let sheet = self .try_sheet_from_string_id(sheet_id) .ok_or(JsValue::UNDEFINED)?; - let val = sheet.get_cell_for_formula(pos); - dbgjs!(format!("getEditCell {} {:?}", &pos, val)); - Ok(val.to_edit()) - // if let Some(value) = sheet.cell_value(pos) { - // Ok(value.to_edit()) - // } else { - // Ok(String::from("")) - // } + Ok(val.to_edit()) } /// gets the display value for a cell From 4db4b1a0147925ef2b63ee6f93e29a3156d41d3d Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Sat, 5 Oct 2024 08:26:59 -0600 Subject: [PATCH 020/373] Remove unnecessary allocations, add comments --- quadratic-core/src/grid/data_table.rs | 20 ++++++++++++++------ quadratic-core/src/grid/sheet/code.rs | 11 ++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 89e2f2eb56..028b61db8b 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -150,6 +150,8 @@ impl DataTable { }; } + /// Ensure that the index is within the bounds of the columns. + /// If there are no columns, apply default headers first if `apply_default_header` is true. fn check_index(&mut self, index: usize, apply_default_header: bool) -> anyhow::Result<()> { match self.columns { Some(ref mut columns) => { @@ -168,6 +170,7 @@ impl DataTable { Ok(()) } + /// Replace a column header at the given index in place. pub fn set_header_at( &mut self, index: usize, @@ -187,6 +190,7 @@ impl DataTable { Ok(()) } + /// Set the display of a column header at the given index. pub fn set_header_display_at(&mut self, index: usize, display: bool) -> anyhow::Result<()> { self.check_index(index, true)?; @@ -263,23 +267,27 @@ impl DataTable { } } - pub fn set_cell_value_at(&mut self, x: u32, y: u32, value: CellValue) -> Option { + /// Sets the cell value at a relative location (0-indexed) into the code. + /// Returns `false` if the value cannot be set. + pub fn set_cell_value_at(&mut self, x: u32, y: u32, value: CellValue) -> bool { if !self.spill_error { match self.value { Value::Single(_) => { - self.value = Value::Single(value.to_owned()); + self.value = Value::Single(value); } Value::Array(ref mut a) => { - // TODO(ddimaria): handle error - a.set(x, y, value.to_owned()).unwrap(); + if let Err(error) = a.set(x, y, value) { + dbgjs!(format!("Unable to set cell value at ({x}, {y}): {error}")); + return false; + } } Value::Tuple(_) => {} } - return Some(value); + return true; } - None + false } /// Returns the size of the output array, or defaults to `_1X1` (since output always includes the code_cell). diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 51f6e4b173..8197c3ef26 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -103,6 +103,7 @@ impl Sheet { /// Returns the CellValue for a CodeRun (if it exists) at the Pos. /// /// Note: spill error will return a CellValue::Blank to ensure calculations can continue. + /// TODO(ddimaria): move to DataTable code pub fn get_code_cell_value(&self, pos: Pos) -> Option { self.data_tables .iter() @@ -118,7 +119,10 @@ impl Sheet { }) } - pub fn set_code_cell_value(&mut self, pos: Pos, value: CellValue) -> Option { + /// Sets the CellValue for a DataTable at the Pos. + /// Returns true if the value was set. + /// TODO(ddimaria): move to DataTable code + pub fn set_code_cell_value(&mut self, pos: Pos, value: CellValue) -> bool { self.data_tables .iter_mut() .find(|(code_cell_pos, data_table)| { @@ -127,10 +131,11 @@ impl Sheet { .and_then(|(code_cell_pos, data_table)| { let x = (pos.x - code_cell_pos.x) as u32; let y = (pos.y - code_cell_pos.y) as u32; - data_table.set_cell_value_at(x, y, value.to_owned()); + data_table.set_cell_value_at(x, y, value); - Some(value) + Some(()) }) + .is_some() } pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { From 80fc63bbde511c9b8186b5c0445d01d16bf62dfb Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 8 Oct 2024 12:09:17 -0600 Subject: [PATCH 021/373] Flatten data table + refactors --- quadratic-client/src/app/actions/actions.ts | 1 + .../src/app/actions/columnRowSpec.ts | 4 +- .../src/app/actions/dataTableSpec.ts | 28 ++ .../src/app/actions/defaultActionsSpec.ts | 2 + .../src/app/grid/sheet/SheetCursor.ts | 4 + .../app/gridGL/HTMLGrid/GridContextMenu.tsx | 5 +- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../quadraticCore/coreClientMessages.ts | 11 +- .../quadraticCore/quadraticCore.ts | 12 + .../web-workers/quadraticCore/worker/core.ts | 5 + .../quadraticCore/worker/coreClient.ts | 4 + .../active_transactions/transaction_name.rs | 1 + .../execute_operation/execute_data_table.rs | 276 +++++++++++++----- .../execution/execute_operation/mod.rs | 14 +- .../src/controller/operations/data_table.rs | 15 + .../src/controller/operations/mod.rs | 1 + .../src/controller/operations/operation.rs | 6 + quadratic-core/src/controller/sheets.rs | 13 + .../src/controller/user_actions/cells.rs | 11 +- .../src/controller/user_actions/data_table.rs | 26 ++ .../src/controller/user_actions/import.rs | 30 +- .../src/controller/user_actions/mod.rs | 1 + quadratic-core/src/error_run.rs | 6 + quadratic-core/src/grid/data_table.rs | 28 +- quadratic-core/src/grid/sheet.rs | 1 + quadratic-core/src/grid/sheet/code.rs | 76 +---- quadratic-core/src/grid/sheet/data_table.rs | 154 ++++++++++ quadratic-core/src/util.rs | 1 + quadratic-core/src/values/cell_values.rs | 35 ++- .../wasm_bindings/controller/data_table.rs | 13 + .../src/wasm_bindings/controller/mod.rs | 3 +- 31 files changed, 606 insertions(+), 183 deletions(-) create mode 100644 quadratic-client/src/app/actions/dataTableSpec.ts create mode 100644 quadratic-core/src/controller/operations/data_table.rs create mode 100644 quadratic-core/src/controller/user_actions/data_table.rs create mode 100644 quadratic-core/src/grid/sheet/data_table.rs create mode 100644 quadratic-core/src/wasm_bindings/controller/data_table.rs diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index dfb24ed437..4af4c9250c 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -134,4 +134,5 @@ export enum Action { InsertRowBelow = 'insert_row_below', DeleteRow = 'delete_row', DeleteColumn = 'delete_column', + FlattenDataTable = 'flatten_data_table', } diff --git a/quadratic-client/src/app/actions/columnRowSpec.ts b/quadratic-client/src/app/actions/columnRowSpec.ts index 97fed24a07..3ff2428a03 100644 --- a/quadratic-client/src/app/actions/columnRowSpec.ts +++ b/quadratic-client/src/app/actions/columnRowSpec.ts @@ -1,9 +1,9 @@ import { Action } from '@/app/actions/actions'; import { isEmbed } from '@/app/helpers/isEmbed'; -import { ActionAvailabilityArgs, ActionSpec } from './actionsSpec'; -import { sheets } from '../grid/controller/Sheets'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { AddIcon, DeleteIcon } from '@/shared/components/Icons'; +import { sheets } from '../grid/controller/Sheets'; +import { ActionAvailabilityArgs, ActionSpec } from './actionsSpec'; const isColumnRowAvailable = ({ isAuthenticated }: ActionAvailabilityArgs) => { if (!sheets.sheet.cursor.hasOneColumnRowSelection(true)) return false; diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts new file mode 100644 index 0000000000..db0154509e --- /dev/null +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -0,0 +1,28 @@ +import { Action } from '@/app/actions/actions'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { PersonAddIcon } from '@/shared/components/Icons'; +import { sheets } from '../grid/controller/Sheets'; +import { ActionSpecRecord } from './actionsSpec'; + +type DataTableSpec = Pick; + +export type DataTableActionArgs = { + [Action.FlattenDataTable]: { name: string }; +}; + +// const isColumnRowAvailable = ({ isAuthenticated }: ActionAvailabilityArgs) => { +// if (!sheets.sheet.cursor.hasOneColumnRowSelection(true)) return false; +// return !isEmbed && isAuthenticated; +// }; + +export const dataTableSpec: DataTableSpec = { + [Action.FlattenDataTable]: { + label: 'Flatten Data Table', + Icon: PersonAddIcon, + isAvailable: () => true, + run: async () => { + const { x, y } = sheets.sheet.cursor.cursorPosition; + quadraticCore.flattenDataTable(sheets.sheet.id, x, y, sheets.getCursorPosition()); + }, + }, +}; diff --git a/quadratic-client/src/app/actions/defaultActionsSpec.ts b/quadratic-client/src/app/actions/defaultActionsSpec.ts index 5eca214184..94693d42bc 100644 --- a/quadratic-client/src/app/actions/defaultActionsSpec.ts +++ b/quadratic-client/src/app/actions/defaultActionsSpec.ts @@ -8,6 +8,7 @@ import { insertActionsSpec } from '@/app/actions/insertActionsSpec'; import { selectionActionsSpec } from '@/app/actions/selectionActionsSpec'; import { viewActionsSpec } from '@/app/actions/viewActionsSpec'; import { columnRowSpec } from './columnRowSpec'; +import { dataTableSpec } from './dataTableSpec'; export const defaultActionSpec: ActionSpecRecord = { ...fileActionsSpec, @@ -19,4 +20,5 @@ export const defaultActionSpec: ActionSpecRecord = { ...selectionActionsSpec, ...codeActionsSpec, ...columnRowSpec, + ...dataTableSpec, }; diff --git a/quadratic-client/src/app/grid/sheet/SheetCursor.ts b/quadratic-client/src/app/grid/sheet/SheetCursor.ts index 3b1f5d95f9..8eb66e22cb 100644 --- a/quadratic-client/src/app/grid/sheet/SheetCursor.ts +++ b/quadratic-client/src/app/grid/sheet/SheetCursor.ts @@ -294,4 +294,8 @@ export class SheetCursor { onlySingleSelection(): boolean { return !this.multiCursor?.length && !this.columnRow; } + + hasDataTable(oneCell?: boolean): boolean { + return true; + } } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx index afb5e44ac1..e5f462d9d3 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx @@ -4,6 +4,7 @@ import { Action } from '@/app/actions/actions'; import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; import { gridHeadingAtom } from '@/app/atoms/gridHeadingAtom'; import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; import { focusGrid } from '@/app/helpers/focusGrid'; import { keyboardShortcutEnumToDisplay } from '@/app/helpers/keyboardShortcutsDisplay'; import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; @@ -13,7 +14,6 @@ import { Point } from 'pixi.js'; import { useCallback, useEffect, useRef } from 'react'; import { useRecoilState } from 'recoil'; import { pixiApp } from '../pixiApp/PixiApp'; -import { sheets } from '@/app/grid/controller/Sheets'; export const GridContextMenu = () => { const [show, setShow] = useRecoilState(gridHeadingAtom); @@ -47,6 +47,7 @@ export const GridContextMenu = () => { const ref = useRef(null); const isColumnRowAvailable = sheets.sheet.cursor.hasOneColumnRowSelection(true); + const isDataTable = sheets.sheet.cursor.hasDataTable(true); return (
{ {isColumnRowAvailable && } {isColumnRowAvailable && } + + {isDataTable && }
); diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index d7e3cf06fd..4cb2bd47dd 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -64,7 +64,7 @@ export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; export type TextMatch = { "Exactly": TextCase } | { "Contains": TextCase } | { "NotContains": TextCase } | { "TextLength": { min: number | null, max: number | null, } }; -export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; +export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; export interface TransientResize { row: bigint | null, column: bigint | null, old_size: number, new_size: number, } export interface Validation { id: string, selection: Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } export interface ValidationDateTime { ignore_blank: boolean, require_date: boolean, require_time: boolean, prohibit_date: boolean, prohibit_time: boolean, ranges: Array, } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index d628664c1e..656cd031f1 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -1010,6 +1010,14 @@ export interface ClientCoreInsertRow { cursor: string; } +export interface ClientCoreFlattenDataTable { + type: 'clientCoreFlattenDataTable'; + sheetId: string; + x: number; + y: number; + cursor: string; +} + export type ClientCoreMessage = | ClientCoreLoad | ClientCoreGetCodeCell @@ -1088,7 +1096,8 @@ export type ClientCoreMessage = | ClientCoreDeleteColumns | ClientCoreDeleteRows | ClientCoreInsertColumn - | ClientCoreInsertRow; + | ClientCoreInsertRow + | ClientCoreFlattenDataTable; export type CoreClientMessage = | CoreClientGetCodeCell diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index c1f8118f45..cb2af99a34 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -1155,6 +1155,18 @@ class QuadraticCore { } //#endregion + //#region data tables + + flattenDataTable(sheetId: string, x: number, y: number, cursor: string) { + this.send({ + type: 'clientCoreFlattenDataTable', + sheetId, + x, + y, + cursor, + }); + } + //#endregion } export const quadraticCore = new QuadraticCore(); diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 91571baf19..7abaa2b4db 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1069,6 +1069,11 @@ class Core { if (!this.gridController) throw new Error('Expected gridController to be defined'); this.gridController.insertRow(sheetId, BigInt(row), below, cursor); } + + flattenDataTable(sheetId: string, x: number, y: number, cursor: string) { + if (!this.gridController) throw new Error('Expected gridController to be defined'); + this.gridController.flattenDataTable(sheetId, posToPos(x, y), cursor); + } } export const core = new Core(); diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index b471cfa244..e4eda2a8f1 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -580,6 +580,10 @@ class CoreClient { core.insertRow(e.data.sheetId, e.data.row, e.data.below, e.data.cursor); return; + case 'clientCoreFlattenDataTable': + core.flattenDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.cursor); + return; + default: if (e.data.id !== undefined) { // handle responses from requests to quadratic-core diff --git a/quadratic-core/src/controller/active_transactions/transaction_name.rs b/quadratic-core/src/controller/active_transactions/transaction_name.rs index c72bb2216b..35fb8cc694 100644 --- a/quadratic-core/src/controller/active_transactions/transaction_name.rs +++ b/quadratic-core/src/controller/active_transactions/transaction_name.rs @@ -15,6 +15,7 @@ pub enum TransactionName { PasteClipboard, SetCode, RunCode, + FlattenDataTable, Import, SetSheetMetadata, SheetAdd, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 97c1f47e3d..05d0b6bf24 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -1,12 +1,63 @@ use crate::{ + cell_values::CellValues, controller::{ active_transactions::pending_transaction::PendingTransaction, operations::operation::Operation, GridController, }, - CellValue, Pos, Rect, SheetRect, + ArraySize, CellValue, Pos, Rect, SheetRect, }; +use anyhow::{bail, Result}; + impl GridController { + pub fn send_to_wasm( + &mut self, + transaction: &mut PendingTransaction, + sheet_rect: &SheetRect, + ) -> Result<()> { + if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() { + self.send_updated_bounds_rect(&sheet_rect, false); + self.add_dirty_hashes_from_sheet_rect(transaction, *sheet_rect); + + if transaction.is_user() { + let sheet = self.try_sheet_result(sheet_rect.sheet_id)?; + let rows = sheet.get_rows_with_wrap_in_rect(&(*sheet_rect).into()); + + if !rows.is_empty() { + let resize_rows = transaction + .resize_rows + .entry(sheet_rect.sheet_id) + .or_default(); + resize_rows.extend(rows); + } + } + } + + Ok(()) + } + + pub fn data_table_operations( + &mut self, + transaction: &mut PendingTransaction, + sheet_rect: &SheetRect, + forward_operations: Vec, + reverse_operations: Vec, + ) { + if transaction.is_user_undo_redo() { + transaction.forward_operations.extend(forward_operations); + + if transaction.is_user() { + self.check_deleted_data_tables(transaction, sheet_rect); + self.add_compute_operations(transaction, sheet_rect, None); + self.check_all_spills(transaction, sheet_rect.sheet_id, true); + } + + transaction.reverse_operations.extend(reverse_operations); + } + + transaction.generate_thumbnail |= self.thumbnail_dirty_sheet_rect(&sheet_rect); + } + // delete any code runs within the sheet_rect. pub(super) fn check_deleted_data_tables( &mut self, @@ -49,80 +100,171 @@ impl GridController { &mut self, transaction: &mut PendingTransaction, op: Operation, - ) { + ) -> Result<()> { if let Operation::SetDataTableAt { sheet_pos, values } = op { let sheet_id = sheet_pos.sheet_id; - let pos: Pos = sheet_pos.into(); + let pos = Pos::from(sheet_pos); + let sheet = self.try_sheet_mut_result(sheet_id)?; - if let Some(sheet) = self.try_sheet_mut(sheet_id) { - if values.size() != 1 { - return dbgjs!("Only single values are supported for now"); - } + // TODO(ddimaria): handle multiple values + if values.size() != 1 { + bail!("Only single values are supported for now"); + } - let value = if let Some(value) = values.get(0, 0).cloned() { - value - } else { - return dbgjs!("No cell value found in CellValues at (0, 0)"); - }; - - let old_value = sheet.get_code_cell_value(pos).unwrap_or(CellValue::Blank); - - dbgjs!(format!( - "SetDataTableAt sheet_pos: {:?} old_value: {:?} old_value: {:?}", - sheet_pos, old_value, value - )); - - sheet.set_code_cell_value(pos, value.to_owned()); - let sheet_rect = SheetRect::from_numbers( - sheet_pos.x, - sheet_pos.y, - values.w as i64, - values.h as i64, - sheet_id, - ); - - if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() { - self.send_updated_bounds_rect(&sheet_rect, false); - self.add_dirty_hashes_from_sheet_rect(transaction, sheet_rect); - - if transaction.is_user() { - if let Some(sheet) = self.try_sheet(sheet_id) { - let rows = sheet.get_rows_with_wrap_in_rect(&sheet_rect.into()); - if !rows.is_empty() { - let resize_rows = - transaction.resize_rows.entry(sheet_id).or_default(); - resize_rows.extend(rows); - } - } - } - } + let value = values.safe_get(0, 0).cloned()?; + let old_value = sheet.get_code_cell_value(pos).unwrap_or(CellValue::Blank); - if transaction.is_user_undo_redo() { - transaction - .forward_operations - .push(Operation::SetDataTableAt { - sheet_pos, - values: value.into(), - }); - - transaction - .reverse_operations - .push(Operation::SetDataTableAt { - sheet_pos, - values: old_value.into(), - }); - - if transaction.is_user() { - self.add_compute_operations(transaction, &sheet_rect, Some(sheet_pos)); - self.check_all_spills(transaction, sheet_pos.sheet_id, true); - } - } + // sen the new value + sheet.set_code_cell_value(pos, value.to_owned()); + + let sheet_rect = SheetRect::from_numbers( + sheet_pos.x, + sheet_pos.y, + values.w as i64, + values.h as i64, + sheet_id, + ); + + self.send_to_wasm(transaction, &sheet_rect)?; + + let forward_operations = vec![Operation::SetDataTableAt { + sheet_pos, + values: value.into(), + }]; + + let reverse_operations = vec![Operation::SetDataTableAt { + sheet_pos, + values: old_value.into(), + }]; + + self.data_table_operations( + transaction, + &sheet_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::SetDataTableAt in execute_set_data_table_at"); + } + + pub(super) fn execute_flatten_data_table( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::FlattenDataTable { sheet_pos } = op { + let sheet_id = sheet_pos.sheet_id; + let pos = Pos::from(sheet_pos); + let sheet = self.try_sheet_mut_result(sheet_id)?; + let data_table_pos = sheet.first_data_table_within(pos)?; + + // Pull out the data table via a swap, removing it from the sheet + let data_table = sheet.delete_data_table(data_table_pos)?; - transaction.generate_thumbnail |= self.thumbnail_dirty_sheet_rect(&sheet_rect); + let values = data_table.value.to_owned().into_array()?; + let ArraySize { w, h } = values.size(); + + let sheet_pos = data_table_pos.to_sheet_pos(sheet_id); + let max = Pos { + x: data_table_pos.x - 1 + w.get() as i64, + y: data_table_pos.y - 1 + h.get() as i64, }; - } + let sheet_rect = SheetRect::new_pos_span(data_table_pos, max, sheet_id); + + let old_values = sheet.set_cell_values(sheet_rect.into(), &values); + let old_cell_values = CellValues::from(old_values); + let cell_values = CellValues::from(values); + + // let the client know that the code cell changed to remove the styles + if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() { + transaction.add_code_cell(sheet_id, data_table_pos); + } + + self.send_to_wasm(transaction, &sheet_rect)?; + + let forward_operations = vec![Operation::SetCellValues { + sheet_pos, + values: cell_values, + }]; + + let reverse_operations = vec![ + Operation::SetCellValues { + sheet_pos, + values: old_cell_values, + }, + Operation::SetCodeRun { + sheet_pos, + code_run: Some(data_table), + index: 0, + }, + ]; + + self.data_table_operations( + transaction, + &sheet_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::FlattenDataTable in execute_flatten_data_table"); } } #[cfg(test)] -mod tests {} +mod tests { + use crate::{ + controller::user_actions::import::tests::simple_csv, test_util::print_table, SheetPos, + }; + + use super::*; + + #[test] + fn test_execute_set_data_table_at() { + // let (sheet, data_table) = new_data_table(); + let (mut gc, sheet_id, pos, _) = simple_csv(); + let change_val_pos = Pos::new(1, 1); + let sheet_pos = SheetPos::from((change_val_pos, sheet_id)); + // let values_array = data_table.value.clone().into_array().unwrap(); + // let ArraySize { w, h } = values_array.size(); + // let cell_values = values_array.into_cell_values_vec().into_vec(); + // let values = CellValues::from_flat_array(w.get(), h.get(), cell_values); + + let values = CellValue::Number(1.into()).into(); + let op = Operation::SetDataTableAt { sheet_pos, values }; + let mut transaction = PendingTransaction::default(); + + gc.execute_set_data_table_at(&mut transaction, op); + + assert_eq!(transaction.forward_operations.len(), 1); + assert_eq!(transaction.reverse_operations.len(), 1); + + gc.finalize_transaction(transaction); + + let data_table = gc.sheet(sheet_id).data_table(pos).unwrap(); + let expected = CellValue::Number(1.into()); + assert_eq!(data_table.get_cell_for_formula(1, 1), expected); + } + + #[test] + fn test_execute_flatten_table() { + let (mut gc, sheet_id, pos, _) = simple_csv(); + let sheet_pos = SheetPos::from((pos, sheet_id)); + let op = Operation::FlattenDataTable { sheet_pos }; + let mut transaction = PendingTransaction::default(); + + gc.execute_flatten_data_table(&mut transaction, op); + + // assert_eq!(transaction.forward_operations.len(), 1); + // assert_eq!(transaction.reverse_operations.len(), 1); + + gc.finalize_transaction(transaction); + + print_table(&gc, sheet_id, Rect::new(0, 0, 10, 10)); + } +} diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index de3809a4f6..30d0c6f6fa 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -15,6 +15,13 @@ mod execute_validation; mod execute_values; impl GridController { + #[track_caller] + pub fn handle_execution_operation_result(result: anyhow::Result<()>) { + if let Err(error) = result { + dbgjs!(&format!("Error in execute_operation: {:?}", &error)); + } + } + /// Executes the given operation. /// pub fn execute_operation(&mut self, transaction: &mut PendingTransaction) { @@ -25,7 +32,12 @@ impl GridController { match op { Operation::SetCellValues { .. } => self.execute_set_cell_values(transaction, op), Operation::SetCodeRun { .. } => self.execute_set_code_run(transaction, op), - Operation::SetDataTableAt { .. } => self.execute_set_data_table_at(transaction, op), + Operation::SetDataTableAt { .. } => Self::handle_execution_operation_result( + self.execute_set_data_table_at(transaction, op), + ), + Operation::FlattenDataTable { .. } => Self::handle_execution_operation_result( + self.execute_flatten_data_table(transaction, op), + ), Operation::ComputeCode { .. } => self.execute_compute_code(transaction, op), Operation::SetCellFormats { .. } => self.execute_set_cell_formats(transaction, op), Operation::SetCellFormatsSelection { .. } => { diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs new file mode 100644 index 0000000000..65d72425fd --- /dev/null +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -0,0 +1,15 @@ +use super::operation::Operation; +use crate::{controller::GridController, SheetPos}; + +impl GridController { + pub fn flatten_data_table_operations( + &self, + sheet_pos: SheetPos, + _cursor: Option, + ) -> Vec { + vec![Operation::FlattenDataTable { sheet_pos }] + } +} + +#[cfg(test)] +mod test {} diff --git a/quadratic-core/src/controller/operations/mod.rs b/quadratic-core/src/controller/operations/mod.rs index 2897812ab7..1650c76bf5 100644 --- a/quadratic-core/src/controller/operations/mod.rs +++ b/quadratic-core/src/controller/operations/mod.rs @@ -7,6 +7,7 @@ pub mod borders; pub mod cell_value; pub mod clipboard; pub mod code_cell; +pub mod data_table; pub mod formats; pub mod formatting; pub mod import; diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 8e2e4b0b1d..8f7bc8b03f 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -45,6 +45,9 @@ pub enum Operation { sheet_pos: SheetPos, values: CellValues, }, + FlattenDataTable { + sheet_pos: SheetPos, + }, ComputeCode { sheet_pos: SheetPos, }, @@ -201,6 +204,9 @@ impl fmt::Display for Operation { "SetDataTableAt {{ sheet_pos: {} values: {:?} }}", sheet_pos, values ), + Operation::FlattenDataTable { sheet_pos } => { + write!(fmt, "FlattenDataTable {{ sheet_pos: {} }}", sheet_pos) + } Operation::SetCellFormats { .. } => write!(fmt, "SetCellFormats {{ todo }}",), Operation::SetCellFormatsSelection { selection, formats } => { write!( diff --git a/quadratic-core/src/controller/sheets.rs b/quadratic-core/src/controller/sheets.rs index e6e447585d..1572c6037c 100644 --- a/quadratic-core/src/controller/sheets.rs +++ b/quadratic-core/src/controller/sheets.rs @@ -2,6 +2,8 @@ use super::GridController; use crate::grid::Sheet; use crate::grid::SheetId; +use anyhow::{anyhow, Result}; + impl GridController { pub fn sheet_ids(&self) -> Vec { self.grid.sheets().iter().map(|sheet| sheet.id).collect() @@ -11,10 +13,21 @@ impl GridController { self.grid.try_sheet(sheet_id) } + pub fn try_sheet_result(&self, sheet_id: SheetId) -> Result<&Sheet> { + self.grid + .try_sheet(sheet_id) + .ok_or_else(|| anyhow!("Sheet with id {:?} not found", sheet_id)) + } + pub fn try_sheet_mut(&mut self, sheet_id: SheetId) -> Option<&mut Sheet> { self.grid.try_sheet_mut(sheet_id) } + pub fn try_sheet_mut_result(&mut self, sheet_id: SheetId) -> Result<&mut Sheet> { + self.try_sheet_mut(sheet_id) + .ok_or_else(|| anyhow!("Sheet with id {:?} not found", sheet_id)) + } + pub fn try_sheet_from_name(&mut self, name: String) -> Option<&Sheet> { self.grid.try_sheet_from_name(name) } diff --git a/quadratic-core/src/controller/user_actions/cells.rs b/quadratic-core/src/controller/user_actions/cells.rs index f8726a1acd..6af47f26ff 100644 --- a/quadratic-core/src/controller/user_actions/cells.rs +++ b/quadratic-core/src/controller/user_actions/cells.rs @@ -59,16 +59,11 @@ impl GridController { let mut ops = vec![]; let mut x = sheet_pos.x; let mut y = sheet_pos.y; + for row in values { for value in row { - ops.extend(self.set_cell_value_operations( - SheetPos { - x, - y, - sheet_id: sheet_pos.sheet_id, - }, - value.to_string(), - )); + let op_sheet_pos = SheetPos::new(sheet_pos.sheet_id, x, y); + ops.extend(self.set_cell_value_operations(op_sheet_pos, value.to_string())); x += 1; } x = sheet_pos.x; diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs new file mode 100644 index 0000000000..ccc6d4ba88 --- /dev/null +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -0,0 +1,26 @@ +use crate::{ + controller::{active_transactions::transaction_name::TransactionName, GridController}, + Pos, SheetPos, +}; + +use anyhow::{anyhow, Result}; + +impl GridController { + pub fn data_tables_within(&self, sheet_pos: SheetPos) -> Result> { + let sheet = self + .try_sheet(sheet_pos.sheet_id) + .ok_or_else(|| anyhow!("Sheet not found"))?; + let pos = Pos::from(sheet_pos); + + sheet.data_tables_within(pos) + } + + pub fn flatten_data_table(&mut self, sheet_pos: SheetPos, cursor: Option) { + let ops = self.flatten_data_table_operations(sheet_pos, cursor.to_owned()); + self.start_user_transaction(ops, cursor, TransactionName::FlattenDataTable); + } +} + +#[cfg(test)] +#[serial_test::parallel] +mod tests {} diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index 018bb9a93d..b6679ea24d 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -73,7 +73,7 @@ impl GridController { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use std::str::FromStr; @@ -110,19 +110,25 @@ mod tests { // const LARGE_PARQUET_FILE: &str = // "../quadratic-rust-shared/data/parquet/flights_1m.parquet"; - #[test] - #[parallel] - fn imports_a_simple_csv() { - let scv_file = read_test_csv_file("simple.csv"); + pub(crate) fn simple_csv() -> (GridController, SheetId, Pos, &'static str) { + let csv_file = read_test_csv_file("simple.csv"); let mut grid_controller = GridController::test(); let sheet_id = grid_controller.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; let file_name = "simple.csv"; grid_controller - .import_csv(sheet_id, scv_file.as_slice().to_vec(), file_name, pos, None) + .import_csv(sheet_id, csv_file.as_slice().to_vec(), file_name, pos, None) .unwrap(); + (grid_controller, sheet_id, pos, file_name) + } + + #[test] + #[parallel] + fn imports_a_simple_csv() { + let (grid_controller, sheet_id, pos, file_name) = simple_csv(); + print_table( &grid_controller, sheet_id, @@ -458,12 +464,12 @@ mod tests { #[parallel] fn should_import_with_title_header() { let file_name = "title_row.csv"; - let scv_file = read_test_csv_file(file_name); + let csv_file = read_test_csv_file(file_name); let mut gc = GridController::test(); let sheet_id = gc.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; - gc.import_csv(sheet_id, scv_file.as_slice().to_vec(), file_name, pos, None) + gc.import_csv(sheet_id, csv_file.as_slice().to_vec(), file_name, pos, None) .unwrap(); print_data_table(&gc, sheet_id, Rect::new_span(pos, Pos { x: 3, y: 4 })); @@ -484,12 +490,12 @@ mod tests { #[parallel] fn should_import_with_title_header_and_empty_first_row() { let file_name = "title_row_empty_first.csv"; - let scv_file = read_test_csv_file(file_name); + let csv_file = read_test_csv_file(file_name); let mut gc = GridController::test(); let sheet_id = gc.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; - gc.import_csv(sheet_id, scv_file.as_slice().to_vec(), file_name, pos, None) + gc.import_csv(sheet_id, csv_file.as_slice().to_vec(), file_name, pos, None) .unwrap(); print_data_table(&gc, sheet_id, Rect::new_span(pos, Pos { x: 3, y: 4 })); @@ -514,13 +520,13 @@ mod tests { #[parallel] fn should_import_utf16_with_invalid_characters() { let file_name = "encoding_issue.csv"; - let scv_file = read_test_csv_file(&file_name); + let csv_file = read_test_csv_file(&file_name); let mut gc = GridController::test(); let sheet_id = gc.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; - gc.import_csv(sheet_id, scv_file.as_slice().to_vec(), file_name, pos, None) + gc.import_csv(sheet_id, csv_file.as_slice().to_vec(), file_name, pos, None) .unwrap(); print_table(&gc, sheet_id, Rect::new_span(pos, Pos { x: 2, y: 3 })); diff --git a/quadratic-core/src/controller/user_actions/mod.rs b/quadratic-core/src/controller/user_actions/mod.rs index 2de3c2db19..8cd8938c47 100644 --- a/quadratic-core/src/controller/user_actions/mod.rs +++ b/quadratic-core/src/controller/user_actions/mod.rs @@ -6,6 +6,7 @@ pub mod cells; pub mod clipboard; pub mod code; pub mod col_row; +pub mod data_table; pub mod formats; pub mod formatting; pub mod import; diff --git a/quadratic-core/src/error_run.rs b/quadratic-core/src/error_run.rs index 9d3e6823fe..e00451d886 100644 --- a/quadratic-core/src/error_run.rs +++ b/quadratic-core/src/error_run.rs @@ -292,6 +292,12 @@ impl> From for RunError { } } +impl From for anyhow::Error { + fn from(msg: RunErrorMsg) -> Self { + RunError { span: None, msg }.into() + } +} + /// Handles internal errors. Panics in debug mode for the stack trace, but /// returns a nice error message in release mode or on web. /// diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 028b61db8b..a374fb069c 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -335,16 +335,14 @@ impl DataTable { } #[cfg(test)] -mod test { +pub(crate) mod test { use std::collections::HashSet; use super::*; use crate::{controller::GridController, grid::SheetId, Array}; use serial_test::parallel; - #[test] - #[parallel] - fn test_import_data_table_and_headers() { + pub fn new_data_table() -> (Sheet, DataTable) { let sheet = GridController::test().grid().sheets()[0].clone(); let file_name = "test.csv"; let values = vec![ @@ -354,10 +352,19 @@ mod test { vec!["Seattle", "WA", "United States", "100"], ]; let import = Import::new(file_name.into()); - let kind = DataTableKind::Import(import.clone()); + let data_table = DataTable::from((import.clone(), values.clone().into(), &sheet)); + + (sheet, data_table) + } + #[test] + #[parallel] + fn test_import_data_table_and_headers() { // test data table without column headings - let mut data_table = DataTable::from((import.clone(), values.clone().into(), &sheet)); + let (_, mut data_table) = new_data_table(); + let kind = data_table.kind.clone(); + let values = data_table.value.clone().into_array().unwrap(); + let expected_values = Value::Array(values.clone().into()); let expected_data_table = DataTable::new(kind.clone(), "Table 1", expected_values, false, false) @@ -393,14 +400,11 @@ mod test { ]; assert_eq!(data_table.columns, Some(expected_columns)); - let expected_values = values - .clone() - .into_iter() - .skip(1) - .collect::>>(); + let mut expected_values = values.clone(); + expected_values.shift().unwrap(); assert_eq!( data_table.value.clone().into_array().unwrap(), - expected_values.into() + expected_values ); // test setting header at index diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 3aa501ef1d..5fe17f0c35 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -28,6 +28,7 @@ pub mod cell_values; pub mod clipboard; pub mod code; pub mod col_row; +pub mod data_table; pub mod formats; pub mod formatting; pub mod rendering; diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 8197c3ef26..b654f3eaff 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -8,54 +8,10 @@ use crate::{ js_types::{JsCodeCell, JsReturnInfo}, CodeCellLanguage, DataTableKind, RenderSize, }, - CellValue, Pos, Rect, Value, + CellValue, Pos, Rect, }; impl Sheet { - pub fn new_data_table( - &mut self, - pos: Pos, - kind: DataTableKind, - value: Value, - spill_error: bool, - header: bool, - ) -> Option { - let name = self.next_data_table_name(); - let data_table = DataTable::new(kind, &name, value, spill_error, header); - - self.set_data_table(pos, Some(data_table)) - } - - /// Sets or deletes a code run. - /// - /// Returns the old value if it was set. - pub fn set_data_table(&mut self, pos: Pos, data_table: Option) -> Option { - if let Some(data_table) = data_table { - self.data_tables.insert(pos, data_table) - } else { - self.data_tables.shift_remove(&pos) - } - } - - pub fn next_data_table_name(&self) -> String { - let mut i = self.data_tables.len() + 1; - - loop { - let name = format!("Table {}", i); - - if !self.data_tables.values().any(|table| table.name == name) { - return name; - } - - i += 1; - } - } - - /// Returns a DatatTable at a Pos - pub fn data_table(&self, pos: Pos) -> Option<&DataTable> { - self.data_tables.get(&pos) - } - /// Gets column bounds for data_tables that output to the columns pub fn code_columns_bounds(&self, column_start: i64, column_end: i64) -> Option> { let mut min: Option = None; @@ -307,36 +263,6 @@ mod test { assert_eq!(sheet.render_size(Pos { x: 1, y: 1 }), None); } - #[test] - #[parallel] - fn test_set_data_table() { - let mut gc = GridController::test(); - let sheet_id = gc.sheet_ids()[0]; - let sheet = gc.grid_mut().try_sheet_mut(sheet_id).unwrap(); - let code_run = CodeRun { - std_out: None, - std_err: None, - formatted_code_string: None, - cells_accessed: HashSet::new(), - error: None, - return_type: Some("number".into()), - line_number: None, - output_type: None, - }; - let data_table = DataTable::new( - DataTableKind::CodeRun(code_run), - "Table 1", - Value::Single(CellValue::Number(BigDecimal::from(2))), - false, - false, - ); - let old = sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); - assert_eq!(old, None); - assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&data_table)); - assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&data_table)); - assert_eq!(sheet.data_table(Pos { x: 1, y: 0 }), None); - } - #[test] #[parallel] fn test_get_data_table() { diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs new file mode 100644 index 0000000000..0d1af0033f --- /dev/null +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -0,0 +1,154 @@ +use super::Sheet; +use crate::{ + grid::{data_table::DataTable, DataTableKind}, + Pos, Value, +}; + +use anyhow::{anyhow, bail, Result}; + +impl Sheet { + pub fn new_data_table( + &mut self, + pos: Pos, + kind: DataTableKind, + value: Value, + spill_error: bool, + header: bool, + ) -> Option { + let name = self.next_data_table_name(); + let data_table = DataTable::new(kind, &name, value, spill_error, header); + + self.set_data_table(pos, Some(data_table)) + } + + /// Sets or deletes a code run. + /// + /// Returns the old value if it was set. + pub fn set_data_table(&mut self, pos: Pos, data_table: Option) -> Option { + if let Some(data_table) = data_table { + self.data_tables.insert(pos, data_table) + } else { + self.data_tables.shift_remove(&pos) + } + } + + pub fn next_data_table_name(&self) -> String { + let mut i = self.data_tables.len() + 1; + + loop { + let name = format!("Table {}", i); + + if !self.data_tables.values().any(|table| table.name == name) { + return name; + } + + i += 1; + } + } + + /// Returns a DatatTable at a Pos + pub fn data_table(&self, pos: Pos) -> Option<&DataTable> { + self.data_tables.get(&pos) + } + + pub fn delete_data_table(&mut self, pos: Pos) -> Result { + self.data_tables + .swap_remove(&pos) + .ok_or_else(|| anyhow!("Data table not found at {:?}", pos)) + } + + pub fn data_tables_within(&self, pos: Pos) -> Result> { + let data_tables = self + .data_tables + .iter() + .filter_map(|(data_table_pos, data_table)| { + data_table + .output_rect(*data_table_pos, false) + .contains(pos) + .then(|| *data_table_pos) + }) + .collect(); + + Ok(data_tables) + } + + pub fn first_data_table_within(&self, pos: Pos) -> Result { + let data_tables = self.data_tables_within(pos)?; + + match data_tables.first() { + Some(pos) => Ok(*pos), + None => bail!("No data tables found within {:?}", pos), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{controller::GridController, grid::CodeRun, CellValue, Value}; + use bigdecimal::BigDecimal; + use serial_test::parallel; + use std::{collections::HashSet, vec}; + + #[test] + #[parallel] + fn test_set_data_table() { + let mut gc = GridController::test(); + let sheet_id = gc.sheet_ids()[0]; + let sheet = gc.grid_mut().try_sheet_mut(sheet_id).unwrap(); + let code_run = CodeRun { + std_out: None, + std_err: None, + formatted_code_string: None, + cells_accessed: HashSet::new(), + error: None, + return_type: Some("number".into()), + line_number: None, + output_type: None, + }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + "Table 1", + Value::Single(CellValue::Number(BigDecimal::from(2))), + false, + false, + ); + let old = sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); + assert_eq!(old, None); + assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&data_table)); + assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&data_table)); + assert_eq!(sheet.data_table(Pos { x: 1, y: 0 }), None); + } + + #[test] + #[parallel] + fn test_get_data_table() { + let mut gc = GridController::test(); + let sheet_id = gc.sheet_ids()[0]; + let sheet = gc.grid_mut().try_sheet_mut(sheet_id).unwrap(); + let code_run = CodeRun { + std_err: None, + std_out: None, + formatted_code_string: None, + cells_accessed: HashSet::new(), + error: None, + return_type: Some("number".into()), + line_number: None, + output_type: None, + }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + "Table 1", + Value::Single(CellValue::Number(BigDecimal::from(2))), + false, + false, + ); + sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); + assert_eq!( + sheet.get_code_cell_value(Pos { x: 0, y: 0 }), + Some(CellValue::Number(BigDecimal::from(2))) + ); + assert_eq!(sheet.data_table(Pos { x: 0, y: 0 }), Some(&data_table)); + assert_eq!(sheet.data_table(Pos { x: 1, y: 1 }), None); + } +} diff --git a/quadratic-core/src/util.rs b/quadratic-core/src/util.rs index 889f0c3378..c53f1dd418 100644 --- a/quadratic-core/src/util.rs +++ b/quadratic-core/src/util.rs @@ -216,6 +216,7 @@ pub fn maybe_reverse_range( } /// For debugging both in tests and in the JS console +#[track_caller] pub fn dbgjs(val: impl fmt::Debug) { if cfg!(target_family = "wasm") { crate::wasm_bindings::js::log(&(format!("{:?}", val))); diff --git a/quadratic-core/src/values/cell_values.rs b/quadratic-core/src/values/cell_values.rs index a697c9895f..16e98f211b 100644 --- a/quadratic-core/src/values/cell_values.rs +++ b/quadratic-core/src/values/cell_values.rs @@ -1,7 +1,7 @@ //! CellValues is a 2D array of CellValue used for Operation::SetCellValues. //! The width and height may grow as needed. -use crate::CellValue; +use crate::{Array, ArraySize, CellValue}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -42,6 +42,26 @@ impl CellValues { .and_then(|col| col.get(&(y as u64))) } + pub fn safe_get(&self, x: u32, y: u32) -> anyhow::Result<&CellValue> { + if !(x < self.w && y < self.h) { + anyhow::bail!( + "CellValues::safe_get out of bounds: w={}, h={}, x={}, y={}", + self.w, + self.h, + x, + y + ); + } + + let cell_value = self + .columns + .get(x as usize) + .and_then(|col| col.get(&(y as u64))) + .ok_or_else(|| anyhow::anyhow!("No value found at ({x}, {y})"))?; + + Ok(cell_value) + } + pub fn set(&mut self, x: u32, y: u32, value: CellValue) { if y >= self.h { self.h = y + 1; @@ -70,7 +90,9 @@ impl CellValues { pub fn from_flat_array(w: u32, h: u32, values: Vec) -> Self { assert!( w * h == values.len() as u32, - "CellValues::flat_array size mismatch" + "CellValues::flat_array size mismatch, expected {}, got {}", + w * h, + values.len() ); let mut columns = vec![BTreeMap::new(); w as usize]; for (i, value) in values.into_iter().enumerate() { @@ -172,6 +194,15 @@ impl From for CellValues { } } +impl From for CellValues { + fn from(array: Array) -> Self { + let ArraySize { w, h } = array.size(); + let cell_values_vec = array.into_cell_values_vec().into_vec(); + + CellValues::from_flat_array(w.get(), h.get(), cell_values_vec) + } +} + #[cfg(test)] mod test { use crate::wasm_bindings::js::clear_js_calls; diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs new file mode 100644 index 0000000000..d01e195c0c --- /dev/null +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -0,0 +1,13 @@ +use super::*; + +#[wasm_bindgen] +impl GridController { + /// Flattens a Data Table + #[wasm_bindgen(js_name = "flattenDataTable")] + pub fn js_flatten_data_table(&mut self, sheet_id: String, pos: String, cursor: Option) { + if let Ok(pos) = serde_json::from_str::(&pos) { + let sheet_id = SheetId::from_str(&sheet_id).unwrap(); + self.flatten_data_table(pos.to_sheet_pos(sheet_id), cursor); + } + } +} diff --git a/quadratic-core/src/wasm_bindings/controller/mod.rs b/quadratic-core/src/wasm_bindings/controller/mod.rs index 43a1c17de3..253cfe3d7b 100644 --- a/quadratic-core/src/wasm_bindings/controller/mod.rs +++ b/quadratic-core/src/wasm_bindings/controller/mod.rs @@ -10,6 +10,8 @@ pub mod bounds; pub mod cells; pub mod clipboard; pub mod code; +pub mod col_row; +pub mod data_table; pub mod export; pub mod formatting; pub mod import; @@ -22,7 +24,6 @@ pub mod summarize; pub mod transactions; pub mod validation; pub mod worker; -pub mod col_row; #[wasm_bindgen] impl GridController { From d5f8ae8095f7e8a2e2ce6d12d2163294942de8fe Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 8 Oct 2024 12:45:15 -0600 Subject: [PATCH 022/373] Stub out gridToDataTable --- .../active_transactions/transaction_name.rs | 1 + .../src/controller/operations/data_table.rs | 10 +++++- .../src/controller/operations/operation.rs | 6 ++++ .../src/controller/user_actions/data_table.rs | 7 ++++- .../wasm_bindings/controller/data_table.rs | 31 ++++++++++++++++--- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/quadratic-core/src/controller/active_transactions/transaction_name.rs b/quadratic-core/src/controller/active_transactions/transaction_name.rs index 35fb8cc694..31cfc15ab1 100644 --- a/quadratic-core/src/controller/active_transactions/transaction_name.rs +++ b/quadratic-core/src/controller/active_transactions/transaction_name.rs @@ -16,6 +16,7 @@ pub enum TransactionName { SetCode, RunCode, FlattenDataTable, + GridToDataTable, Import, SetSheetMetadata, SheetAdd, diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index 65d72425fd..1300dcfee0 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -1,5 +1,5 @@ use super::operation::Operation; -use crate::{controller::GridController, SheetPos}; +use crate::{controller::GridController, SheetPos, SheetRect}; impl GridController { pub fn flatten_data_table_operations( @@ -9,6 +9,14 @@ impl GridController { ) -> Vec { vec![Operation::FlattenDataTable { sheet_pos }] } + + pub fn grid_to_data_table_operations( + &self, + sheet_rect: SheetRect, + _cursor: Option, + ) -> Vec { + vec![Operation::GridToDataTable { sheet_rect }] + } } #[cfg(test)] diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 8f7bc8b03f..ae27402b00 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -48,6 +48,9 @@ pub enum Operation { FlattenDataTable { sheet_pos: SheetPos, }, + GridToDataTable { + sheet_rect: SheetRect, + }, ComputeCode { sheet_pos: SheetPos, }, @@ -207,6 +210,9 @@ impl fmt::Display for Operation { Operation::FlattenDataTable { sheet_pos } => { write!(fmt, "FlattenDataTable {{ sheet_pos: {} }}", sheet_pos) } + Operation::GridToDataTable { sheet_rect } => { + write!(fmt, "GridToDataTable {{ sheet_rect: {} }}", sheet_rect) + } Operation::SetCellFormats { .. } => write!(fmt, "SetCellFormats {{ todo }}",), Operation::SetCellFormatsSelection { selection, formats } => { write!( diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index ccc6d4ba88..ffc81d7b26 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -1,6 +1,6 @@ use crate::{ controller::{active_transactions::transaction_name::TransactionName, GridController}, - Pos, SheetPos, + Pos, SheetPos, SheetRect, }; use anyhow::{anyhow, Result}; @@ -19,6 +19,11 @@ impl GridController { let ops = self.flatten_data_table_operations(sheet_pos, cursor.to_owned()); self.start_user_transaction(ops, cursor, TransactionName::FlattenDataTable); } + + pub fn grid_to_data_table(&mut self, sheet_rect: SheetRect, cursor: Option) { + let ops = self.grid_to_data_table_operations(sheet_rect, cursor.to_owned()); + self.start_user_transaction(ops, cursor, TransactionName::GridToDataTable); + } } #[cfg(test)] diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index d01e195c0c..62a38b2d1f 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -4,10 +4,31 @@ use super::*; impl GridController { /// Flattens a Data Table #[wasm_bindgen(js_name = "flattenDataTable")] - pub fn js_flatten_data_table(&mut self, sheet_id: String, pos: String, cursor: Option) { - if let Ok(pos) = serde_json::from_str::(&pos) { - let sheet_id = SheetId::from_str(&sheet_id).unwrap(); - self.flatten_data_table(pos.to_sheet_pos(sheet_id), cursor); - } + pub fn js_flatten_data_table( + &mut self, + sheet_id: String, + pos: String, + cursor: Option, + ) -> Result<(), JsValue> { + let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; + let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; + self.flatten_data_table(pos.to_sheet_pos(sheet_id), cursor); + + Ok(()) + } + + /// Flattens a Data Table + #[wasm_bindgen(js_name = "gridToDataTable")] + pub fn js_grid_to_data_table( + &mut self, + sheet_id: String, + rect: String, + cursor: Option, + ) -> Result<(), JsValue> { + let rect = serde_json::from_str::(&rect).map_err(|e| e.to_string())?; + let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; + self.grid_to_data_table(rect.to_sheet_rect(sheet_id), cursor); + + Ok(()) } } From 9de1e6952193e6e32d8175ae837c6db5b72c63d7 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 11 Oct 2024 14:28:46 -0600 Subject: [PATCH 023/373] Support sorting data tables in Rust --- quadratic-client/src/app/actions/actions.ts | 1 + .../src/app/actions/dataTableSpec.ts | 20 +- .../app/gridGL/HTMLGrid/GridContextMenu.tsx | 1 + .../src/app/gridGL/cells/CellsArray.ts | 8 + .../src/app/gridGL/cells/CellsSheets.ts | 7 + .../src/app/gridGL/pixiApp/PixiApp.ts | 4 + .../src/app/quadratic-core-types/index.d.ts | 2 +- .../quadraticCore/coreClientMessages.ts | 9 +- .../quadraticCore/quadraticCore.ts | 8 + .../web-workers/quadraticCore/worker/core.ts | 5 + .../quadraticCore/worker/coreClient.ts | 4 + .../execute_operation/execute_data_table.rs | 214 ++++++++++++++++-- .../execution/execute_operation/mod.rs | 6 + .../src/controller/execution/run_code/mod.rs | 2 + .../src/controller/operations/data_table.rs | 14 ++ .../src/controller/operations/operation.rs | 16 ++ .../src/controller/user_actions/cells.rs | 16 +- .../src/controller/user_actions/data_table.rs | 21 ++ .../src/controller/user_actions/import.rs | 63 +++--- quadratic-core/src/grid/data_table.rs | 147 +++++++++++- .../src/grid/file/serialize/data_table.rs | 2 + quadratic-core/src/grid/file/v1_6/file.rs | 1 + quadratic-core/src/grid/file/v1_7/schema.rs | 1 + quadratic-core/src/grid/sheet/data_table.rs | 5 + quadratic-core/src/test_util.rs | 6 +- quadratic-core/src/values/array.rs | 40 +++- .../wasm_bindings/controller/data_table.rs | 29 ++- quadratic-rust-shared/src/auto_gen_path.rs | 2 + 28 files changed, 568 insertions(+), 86 deletions(-) create mode 100644 quadratic-rust-shared/src/auto_gen_path.rs diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 4af4c9250c..b2ebebe97b 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -135,4 +135,5 @@ export enum Action { DeleteRow = 'delete_row', DeleteColumn = 'delete_column', FlattenDataTable = 'flatten_data_table', + GridToDataTable = 'grid_to_data_table', } diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index db0154509e..1bc36608c6 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -3,26 +3,34 @@ import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { PersonAddIcon } from '@/shared/components/Icons'; import { sheets } from '../grid/controller/Sheets'; import { ActionSpecRecord } from './actionsSpec'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -type DataTableSpec = Pick; +type DataTableSpec = Pick; export type DataTableActionArgs = { [Action.FlattenDataTable]: { name: string }; }; -// const isColumnRowAvailable = ({ isAuthenticated }: ActionAvailabilityArgs) => { -// if (!sheets.sheet.cursor.hasOneColumnRowSelection(true)) return false; -// return !isEmbed && isAuthenticated; -// }; +const isDataTable = (): boolean => { + return pixiApp.isCursorOnCodeCellOutput(); +}; export const dataTableSpec: DataTableSpec = { [Action.FlattenDataTable]: { label: 'Flatten Data Table', Icon: PersonAddIcon, - isAvailable: () => true, + isAvailable: () => isDataTable(), run: async () => { const { x, y } = sheets.sheet.cursor.cursorPosition; quadraticCore.flattenDataTable(sheets.sheet.id, x, y, sheets.getCursorPosition()); }, }, + [Action.GridToDataTable]: { + label: 'Convert to Data Table', + Icon: PersonAddIcon, + isAvailable: () => !isDataTable(), + run: async () => { + quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); + }, + }, }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx index e5f462d9d3..a17377172e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx @@ -84,6 +84,7 @@ export const GridContextMenu = () => { {isDataTable && } + {isDataTable && } ); diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts index 6ee4763734..7e040c89f4 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsArray.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsArray.ts @@ -281,4 +281,12 @@ export class CellsArray extends Container { isCodeCell(x: number, y: number): boolean { return this.codeCells.has(this.key(x, y)); } + + isCodeCellOutput(x: number, y: number): boolean { + return [...this.codeCells.values()].some((codeCell) => { + let rect = new Rectangle(codeCell.x, codeCell.y, codeCell.x + codeCell.w, codeCell.y + codeCell.h); + + return rect.contains(x, y); + }); + } } diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts index 7e52a7120f..ce7d757735 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts @@ -186,6 +186,13 @@ export class CellsSheets extends Container { return cellsSheet.cellsArray.isCodeCell(cursor.x, cursor.y); } + isCursorOnCodeCellOutput(): boolean { + const cellsSheet = this.current; + if (!cellsSheet) return false; + const cursor = sheets.sheet.cursor.cursorPosition; + return cellsSheet.cellsArray.isCodeCellOutput(cursor.x, cursor.y); + } + update() { this.current?.update(); } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index 397a36ce48..f0c84debc8 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -351,6 +351,10 @@ export class PixiApp { return this.cellsSheets.isCursorOnCodeCell(); } + isCursorOnCodeCellOutput(): boolean { + return this.cellsSheets.isCursorOnCodeCellOutput(); + } + // called when the viewport is loaded from the URL urlViewportLoad(sheetId: string) { const cellsSheet = sheets.getById(sheetId); diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 4cb2bd47dd..592be56da6 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -64,7 +64,7 @@ export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; export type TextMatch = { "Exactly": TextCase } | { "Contains": TextCase } | { "NotContains": TextCase } | { "TextLength": { min: number | null, max: number | null, } }; -export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; +export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "GridToDataTable" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; export interface TransientResize { row: bigint | null, column: bigint | null, old_size: number, new_size: number, } export interface Validation { id: string, selection: Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } export interface ValidationDateTime { ignore_blank: boolean, require_date: boolean, require_time: boolean, prohibit_date: boolean, prohibit_time: boolean, ranges: Array, } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index 656cd031f1..6734a42b2f 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -1018,6 +1018,12 @@ export interface ClientCoreFlattenDataTable { cursor: string; } +export interface ClientCoreGridToDataTable { + type: 'clientCoreGridToDataTable'; + selection: Selection; + cursor: string; +} + export type ClientCoreMessage = | ClientCoreLoad | ClientCoreGetCodeCell @@ -1097,7 +1103,8 @@ export type ClientCoreMessage = | ClientCoreDeleteRows | ClientCoreInsertColumn | ClientCoreInsertRow - | ClientCoreFlattenDataTable; + | ClientCoreFlattenDataTable + | ClientCoreGridToDataTable; export type CoreClientMessage = | CoreClientGetCodeCell diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index cb2af99a34..6dbbfe6e15 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -1166,6 +1166,14 @@ class QuadraticCore { cursor, }); } + + gridToDataTable(selection: Selection, cursor: string) { + this.send({ + type: 'clientCoreGridToDataTable', + selection, + cursor, + }); + } //#endregion } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 7abaa2b4db..970fab0f1f 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1074,6 +1074,11 @@ class Core { if (!this.gridController) throw new Error('Expected gridController to be defined'); this.gridController.flattenDataTable(sheetId, posToPos(x, y), cursor); } + + gridToDataTable(selection: Selection, cursor: string) { + if (!this.gridController) throw new Error('Expected gridController to be defined'); + this.gridController.gridToDataTable(JSON.stringify(selection, bigIntReplacer), cursor); + } } export const core = new Core(); diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index e4eda2a8f1..1bf5030627 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -584,6 +584,10 @@ class CoreClient { core.flattenDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.cursor); return; + case 'clientCoreGridToDataTable': + core.gridToDataTable(e.data.selection, e.data.cursor); + return; + default: if (e.data.id !== undefined) { // handle responses from requests to quadratic-core diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 05d0b6bf24..4942906ebf 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -1,9 +1,12 @@ use crate::{ cell_values::CellValues, + cellvalue::Import, controller::{ active_transactions::pending_transaction::PendingTransaction, - operations::operation::Operation, GridController, + operations::{data_table, operation::Operation}, + GridController, }, + grid::DataTable, ArraySize, CellValue, Pos, Rect, SheetRect, }; @@ -47,7 +50,7 @@ impl GridController { transaction.forward_operations.extend(forward_operations); if transaction.is_user() { - self.check_deleted_data_tables(transaction, sheet_rect); + // self.check_deleted_data_tables(transaction, sheet_rect); self.add_compute_operations(transaction, sheet_rect, None); self.check_all_spills(transaction, sheet_rect.sheet_id, true); } @@ -214,32 +217,193 @@ impl GridController { bail!("Expected Operation::FlattenDataTable in execute_flatten_data_table"); } + + pub(super) fn execute_grid_to_data_table( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::GridToDataTable { sheet_rect } = op { + let sheet_id = sheet_rect.sheet_id; + let rect = Rect::from(sheet_rect); + let sheet = self.try_sheet_result(sheet_id)?; + let sheet_pos = sheet_rect.min.to_sheet_pos(sheet_id); + + let old_values = sheet.cell_values_in_rect(&rect, false)?; + + let import = Import::new("simple.csv".into()); + let data_table = DataTable::from((import.to_owned(), old_values.to_owned(), sheet)); + let cell_value = CellValue::Import(import.to_owned()); + + let sheet = self.try_sheet_mut_result(sheet_id)?; + sheet.delete_cell_values(rect); + sheet.set_cell_value(sheet_rect.min, cell_value); + sheet + .data_tables + .insert_full(sheet_rect.min, data_table.to_owned()); + + // let the client know that the code cell has been created to apply the styles + if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() { + transaction.add_code_cell(sheet_id, sheet_rect.min); + } + + self.send_to_wasm(transaction, &sheet_rect)?; + + let forward_operations = vec![ + Operation::SetCellValues { + sheet_pos, + values: CellValues::from(CellValue::Import(import)), + }, + Operation::SetCodeRun { + sheet_pos, + code_run: Some(data_table), + index: 0, + }, + ]; + + let reverse_operations = vec![Operation::SetCellValues { + sheet_pos, + values: CellValues::from(old_values), + }]; + + self.data_table_operations( + transaction, + &sheet_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::GridToDataTable in execute_grid_to_data_table"); + } + + pub(super) fn execute_sort_data_table( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::SortDataTable { + sheet_rect, + column_index, + sort_order, + } = op + { + // let sheet_id = sheet_pos.sheet_id; + // let pos = Pos::from(sheet_pos); + // let sheet = self.try_sheet_mut_result(sheet_id)?; + // let data_table_pos = sheet.first_data_table_within(pos)?; + // let mut data_table = sheet.data_table_mut(data_table_pos)?; + + // let sort_order = match sort_order.as_str() { + // "asc" => SortOrder::Asc, + // "desc" => SortOrder::Desc, + // _ => bail!("Invalid sort order"), + // }; + + // data_table.sort(column_index, sort_order); + + // self.send_to_wasm(transaction, &sheet_rect)?; + + // let forward_operations = vec![ + // Operation::SetCellValues { + // sheet_pos, + // values: CellValues::from(CellValue::Import(import)), + // }, + // Operation::SetCodeRun { + // sheet_pos, + // code_run: Some(data_table), + // index: 0, + // }, + // ]; + + // let reverse_operations = vec![Operation::SetCellValues { + // sheet_pos, + // values: CellValues::from(old_values), + // }]; + + // self.data_table_operations( + // transaction, + // &sheet_rect, + // forward_operations, + // reverse_operations, + // ); + + return Ok(()); + }; + + bail!("Expected Operation::GridToDataTable in execute_grid_to_data_table"); + } } #[cfg(test)] mod tests { use crate::{ - controller::user_actions::import::tests::simple_csv, test_util::print_table, SheetPos, + controller::user_actions::import::tests::{assert_simple_csv, simple_csv}, + grid::SheetId, + test_util::{assert_cell_value_row, print_data_table, print_table}, + SheetPos, }; use super::*; + pub(crate) fn flatten_data_table<'a>( + gc: &'a mut GridController, + sheet_id: SheetId, + pos: Pos, + file_name: &'a str, + ) { + let sheet_pos = SheetPos::from((pos, sheet_id)); + let op = Operation::FlattenDataTable { sheet_pos }; + let mut transaction = PendingTransaction::default(); + + assert_simple_csv(&gc, sheet_id, pos, file_name); + + gc.execute_flatten_data_table(&mut transaction, op).unwrap(); + + assert_eq!(transaction.forward_operations.len(), 1); + assert_eq!(transaction.reverse_operations.len(), 2); + + gc.finalize_transaction(transaction); + + assert!(gc.sheet(sheet_id).first_data_table_within(pos).is_err()); + + assert_flattened_simple_csv(&gc, sheet_id, pos, file_name); + + print_table(&gc, sheet_id, Rect::new(0, 0, 2, 2)); + } + + #[track_caller] + pub(crate) fn assert_flattened_simple_csv<'a>( + gc: &'a GridController, + sheet_id: SheetId, + pos: Pos, + file_name: &'a str, + ) -> (&'a GridController, SheetId, Pos, &'a str) { + // there should be no data tables + assert!(gc.sheet(sheet_id).first_data_table_within(pos).is_err()); + + let first_row = vec!["city", "region", "country", "population"]; + assert_cell_value_row(&gc, sheet_id, 0, 3, 0, first_row); + + let last_row = vec!["Concord", "NH", "United States", "42605"]; + assert_cell_value_row(&gc, sheet_id, 0, 3, 10, last_row); + + (gc, sheet_id, pos, file_name) + } + #[test] fn test_execute_set_data_table_at() { - // let (sheet, data_table) = new_data_table(); let (mut gc, sheet_id, pos, _) = simple_csv(); let change_val_pos = Pos::new(1, 1); let sheet_pos = SheetPos::from((change_val_pos, sheet_id)); - // let values_array = data_table.value.clone().into_array().unwrap(); - // let ArraySize { w, h } = values_array.size(); - // let cell_values = values_array.into_cell_values_vec().into_vec(); - // let values = CellValues::from_flat_array(w.get(), h.get(), cell_values); let values = CellValue::Number(1.into()).into(); let op = Operation::SetDataTableAt { sheet_pos, values }; let mut transaction = PendingTransaction::default(); - gc.execute_set_data_table_at(&mut transaction, op); + gc.execute_set_data_table_at(&mut transaction, op).unwrap(); assert_eq!(transaction.forward_operations.len(), 1); assert_eq!(transaction.reverse_operations.len(), 1); @@ -252,19 +416,35 @@ mod tests { } #[test] - fn test_execute_flatten_table() { - let (mut gc, sheet_id, pos, _) = simple_csv(); - let sheet_pos = SheetPos::from((pos, sheet_id)); - let op = Operation::FlattenDataTable { sheet_pos }; - let mut transaction = PendingTransaction::default(); + fn test_execute_flatten_data_table() { + let (mut gc, sheet_id, pos, file_name) = simple_csv(); + + assert_simple_csv(&gc, sheet_id, pos, file_name); + + flatten_data_table(&mut gc, sheet_id, pos, file_name); + print_table(&gc, sheet_id, Rect::new(0, 0, 2, 2)); - gc.execute_flatten_data_table(&mut transaction, op); + assert_flattened_simple_csv(&gc, sheet_id, pos, file_name); + } + + #[test] + fn test_execute_grid_to_data_table() { + let (mut gc, sheet_id, pos, file_name) = simple_csv(); + flatten_data_table(&mut gc, sheet_id, pos, file_name); + assert_flattened_simple_csv(&gc, sheet_id, pos, file_name); + + let max = Pos::new(3, 10); + let sheet_rect = SheetRect::new_pos_span(pos, max, sheet_id); + let op = Operation::GridToDataTable { sheet_rect }; + let mut transaction = PendingTransaction::default(); + gc.execute_grid_to_data_table(&mut transaction, op).unwrap(); // assert_eq!(transaction.forward_operations.len(), 1); - // assert_eq!(transaction.reverse_operations.len(), 1); + // assert_eq!(transaction.reverse_operations.len(), 2); gc.finalize_transaction(transaction); + print_data_table(&gc, sheet_id, Rect::new(0, 0, 2, 2)); - print_table(&gc, sheet_id, Rect::new(0, 0, 10, 10)); + assert_simple_csv(&gc, sheet_id, pos, file_name); } } diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 30d0c6f6fa..93c4964138 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -38,6 +38,12 @@ impl GridController { Operation::FlattenDataTable { .. } => Self::handle_execution_operation_result( self.execute_flatten_data_table(transaction, op), ), + Operation::GridToDataTable { .. } => Self::handle_execution_operation_result( + self.execute_grid_to_data_table(transaction, op), + ), + Operation::SortDataTable { .. } => Self::handle_execution_operation_result( + self.execute_grid_to_data_table(transaction, op), + ), Operation::ComputeCode { .. } => self.execute_compute_code(transaction, op), Operation::SetCellFormats { .. } => self.execute_set_cell_formats(transaction, op), Operation::SetCellFormatsSelection { .. } => { diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 4f4c8b097b..cd9cf374e2 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -40,12 +40,14 @@ impl GridController { let old_data_table = if let Some(new_data_table) = &new_data_table { let (old_index, old_data_table) = sheet.data_tables.insert_full(pos, new_data_table.clone()); + // keep the orderings of the code runs consistent, particularly when undoing/redoing let index = if index > sheet.data_tables.len() - 1 { sheet.data_tables.len() - 1 } else { index }; + sheet.data_tables.move_index(old_index, index); old_data_table } else { diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index 1300dcfee0..cdcf344666 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -17,6 +17,20 @@ impl GridController { ) -> Vec { vec![Operation::GridToDataTable { sheet_rect }] } + + pub fn sort_data_table_operations( + &self, + sheet_rect: SheetRect, + column_index: u32, + sort_order: String, + _cursor: Option, + ) -> Vec { + vec![Operation::SortDataTable { + sheet_rect, + column_index, + sort_order, + }] + } } #[cfg(test)] diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index ae27402b00..380b59800a 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -51,6 +51,11 @@ pub enum Operation { GridToDataTable { sheet_rect: SheetRect, }, + SortDataTable { + sheet_rect: SheetRect, + column_index: u32, + sort_order: String, + }, ComputeCode { sheet_pos: SheetPos, }, @@ -213,6 +218,17 @@ impl fmt::Display for Operation { Operation::GridToDataTable { sheet_rect } => { write!(fmt, "GridToDataTable {{ sheet_rect: {} }}", sheet_rect) } + Operation::SortDataTable { + sheet_rect, + column_index, + sort_order, + } => { + write!( + fmt, + "SortDataTable {{ sheet_rect: {}, column_index: {}, sort_order: {} }}", + sheet_rect, column_index, sort_order + ) + } Operation::SetCellFormats { .. } => write!(fmt, "SetCellFormats {{ todo }}",), Operation::SetCellFormatsSelection { selection, formats } => { write!( diff --git a/quadratic-core/src/controller/user_actions/cells.rs b/quadratic-core/src/controller/user_actions/cells.rs index 6af47f26ff..5ad013b0d3 100644 --- a/quadratic-core/src/controller/user_actions/cells.rs +++ b/quadratic-core/src/controller/user_actions/cells.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use crate::controller::active_transactions::transaction_name::TransactionName; use crate::controller::GridController; @@ -13,9 +13,7 @@ impl GridController { value: String, cursor: Option, ) -> Result<()> { - let sheet = self - .try_sheet_mut(sheet_pos.sheet_id) - .ok_or_else(|| anyhow!("Sheet not found"))?; + let sheet = self.try_sheet_mut_result(sheet_pos.sheet_id)?; let cell_value = sheet .get_column(sheet_pos.x) @@ -72,16 +70,6 @@ impl GridController { self.start_user_transaction(ops, cursor, TransactionName::SetCells); } - pub fn set_data_table_value( - &mut self, - sheet_pos: SheetPos, - value: String, - cursor: Option, - ) { - let ops = self.set_data_table_operations_at(sheet_pos, value); - self.start_user_transaction(ops, cursor, TransactionName::SetDataTableAt); - } - /// Starts a transaction to deletes the cell values and code in a given rect and updates dependent cells. pub fn delete_cells(&mut self, selection: &Selection, cursor: Option) { let ops = self.delete_cells_operations(selection); diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index ffc81d7b26..967359553f 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -15,6 +15,16 @@ impl GridController { sheet.data_tables_within(pos) } + pub fn set_data_table_value( + &mut self, + sheet_pos: SheetPos, + value: String, + cursor: Option, + ) { + let ops = self.set_data_table_operations_at(sheet_pos, value); + self.start_user_transaction(ops, cursor, TransactionName::SetDataTableAt); + } + pub fn flatten_data_table(&mut self, sheet_pos: SheetPos, cursor: Option) { let ops = self.flatten_data_table_operations(sheet_pos, cursor.to_owned()); self.start_user_transaction(ops, cursor, TransactionName::FlattenDataTable); @@ -24,6 +34,17 @@ impl GridController { let ops = self.grid_to_data_table_operations(sheet_rect, cursor.to_owned()); self.start_user_transaction(ops, cursor, TransactionName::GridToDataTable); } + + pub fn sort_data_table( + &mut self, + sheet_rect: SheetRect, + column_index: u32, + sort_order: String, + cursor: Option, + ) { + let ops = self.grid_to_data_table_operations(sheet_rect, cursor.to_owned()); + self.start_user_transaction(ops, cursor, TransactionName::GridToDataTable); + } } #[cfg(test)] diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index b6679ea24d..7d50303cd3 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -112,50 +112,49 @@ pub(crate) mod tests { pub(crate) fn simple_csv() -> (GridController, SheetId, Pos, &'static str) { let csv_file = read_test_csv_file("simple.csv"); - let mut grid_controller = GridController::test(); - let sheet_id = grid_controller.grid.sheets()[0].id; + let mut gc = GridController::test(); + let sheet_id = gc.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; let file_name = "simple.csv"; - grid_controller - .import_csv(sheet_id, csv_file.as_slice().to_vec(), file_name, pos, None) + gc.import_csv(sheet_id, csv_file.as_slice().to_vec(), file_name, pos, None) .unwrap(); - (grid_controller, sheet_id, pos, file_name) + (gc, sheet_id, pos, file_name) } - #[test] - #[parallel] - fn imports_a_simple_csv() { - let (grid_controller, sheet_id, pos, file_name) = simple_csv(); - - print_table( - &grid_controller, - sheet_id, - Rect::new_span(pos, Pos { x: 3, y: 10 }), - ); - + #[track_caller] + pub(crate) fn assert_simple_csv<'a>( + gc: &'a GridController, + sheet_id: SheetId, + pos: Pos, + file_name: &'a str, + ) -> (&'a GridController, SheetId, Pos, &'a str) { let import = Import::new(file_name.into()); let cell_value = CellValue::Import(import); - assert_display_cell_value(&grid_controller, sheet_id, 0, 0, &cell_value.to_string()); + assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); - assert_data_table_cell_value_row( - &grid_controller, - sheet_id, - 0, - 3, - 0, - vec!["city", "region", "country", "population"], + // data table should be at `pos` + assert_eq!( + gc.sheet(sheet_id).first_data_table_within(pos).unwrap(), + pos ); - assert_data_table_cell_value_row( - &grid_controller, - sheet_id, - 0, - 3, - 10, - vec!["Concord", "NH", "United States", "42605"], - ); + let first_row = vec!["city", "region", "country", "population"]; + assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 0, first_row); + + let last_row = vec!["Concord", "NH", "United States", "42605"]; + assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 10, last_row); + + (gc, sheet_id, pos, file_name) + } + + #[test] + #[parallel] + fn imports_a_simple_csv() { + let (gc, sheet_id, pos, file_name) = simple_csv(); + + assert_simple_csv(&gc, sheet_id, pos, file_name); } #[test] diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index a374fb069c..dec9e990f3 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -4,13 +4,16 @@ //! any given CellValue::Code type (ie, if it doesn't exist then a run hasn't been //! performed yet). +use std::cmp::Ordering; + use crate::cellvalue::Import; use crate::grid::CodeRun; use crate::{ Array, ArraySize, CellValue, Pos, Rect, RunError, RunErrorMsg, SheetPos, SheetRect, Value, }; -use anyhow::anyhow; +use anyhow::{anyhow, Ok, Result}; use chrono::{DateTime, Utc}; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use super::Sheet; @@ -32,6 +35,12 @@ impl DataTableColumn { } } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum SortDirection { + Ascending, + Descending, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum DataTableKind { CodeRun(CodeRun), @@ -43,6 +52,7 @@ pub struct DataTable { pub kind: DataTableKind, pub name: String, pub columns: Option>, + pub display_buffer: Option>, pub value: Value, pub readonly: bool, pub spill_error: bool, @@ -83,6 +93,7 @@ impl DataTable { kind, name: name.into(), columns: None, + display_buffer: None, value, readonly, spill_error, @@ -101,6 +112,7 @@ impl DataTable { kind: DataTableKind, name: &str, columns: Option>, + display_buffer: Option>, value: Value, readonly: bool, spill_error: bool, @@ -109,6 +121,7 @@ impl DataTable { kind, name: name.into(), columns, + display_buffer, value, readonly, spill_error, @@ -204,6 +217,49 @@ impl DataTable { Ok(()) } + pub fn sort(&mut self, column_index: usize, direction: SortDirection) -> Result<()> { + let values = self.value.clone().into_array()?; + + let display_buffer = values + .col(column_index) + .enumerate() + .sorted_by(|a, b| match direction { + SortDirection::Ascending => a.1.cmp(b.1).unwrap_or(Ordering::Equal), + SortDirection::Descending => b.1.cmp(a.1).unwrap_or(Ordering::Equal), + }) + .map(|(i, _)| i as u64) + .collect::>(); + + self.display_buffer = Some(display_buffer); + + Ok(()) + } + + pub fn display_value_from_buffer(&self, display_buffer: &Vec) -> Result { + let value = self.value.to_owned().into_array()?; + + let values = display_buffer + .iter() + .filter_map(|index| { + value + .get_row(*index as usize) + .map(|row| row.into_iter().cloned().collect::>()) + .ok() + }) + .collect::>>(); + + let array = Array::from(values); + + Ok(array.into()) + } + + pub fn display_value(&self) -> Result { + match self.display_buffer { + Some(ref display_buffer) => self.display_value_from_buffer(display_buffer), + None => Ok(self.value.to_owned()), + } + } + /// Helper functtion to get the CodeRun from the DataTable. /// Returns `None` if the DataTableKind is not CodeRun. pub fn code_run(&self) -> Option<&CodeRun> { @@ -341,22 +397,74 @@ pub(crate) mod test { use super::*; use crate::{controller::GridController, grid::SheetId, Array}; use serial_test::parallel; + use tabled::{ + builder::Builder, + settings::{Color, Modify, Style}, + }; - pub fn new_data_table() -> (Sheet, DataTable) { - let sheet = GridController::test().grid().sheets()[0].clone(); - let file_name = "test.csv"; - let values = vec![ + pub fn test_csv_values() -> Vec> { + vec![ vec!["city", "region", "country", "population"], vec!["Southborough", "MA", "United States", "1000"], vec!["Denver", "CO", "United States", "10000"], vec!["Seattle", "WA", "United States", "100"], - ]; + ] + } + + pub fn new_data_table() -> (Sheet, DataTable) { + let sheet = GridController::test().grid().sheets()[0].clone(); + let file_name = "test.csv"; + let values = test_csv_values(); let import = Import::new(file_name.into()); - let data_table = DataTable::from((import.clone(), values.clone().into(), &sheet)); + let array = Array::from_str_vec(values, true).unwrap(); + let data_table = DataTable::from((import.clone(), array, &sheet)); (sheet, data_table) } + /// Util to print a data table when testing + #[track_caller] + pub fn print_data_table(data_table: &DataTable, title: Option<&str>, max: Option) { + let mut builder = Builder::default(); + let array = data_table.display_value().unwrap().into_array().unwrap(); + let max = max.unwrap_or(array.height() as usize); + let title = title.unwrap_or("Data Table"); + + if let Some(columns) = data_table.columns.as_ref() { + let columns = columns.iter().map(|c| c.name.clone()).collect::>(); + builder.set_header(columns); + } + + for row in array.rows().take(max) { + let row = row.iter().map(|s| s.to_string()).collect::>(); + builder.push_record(row); + } + + let mut table = builder.build(); + table.with(Style::modern()); + + // bold the headers if they exist + if let Some(columns) = data_table.columns.as_ref() { + columns.iter().enumerate().for_each(|(index, _)| { + table.with(Modify::new((0, index)).with(Color::BOLD)); + }); + } + + println!("\nData Table: {title}\n{table}"); + } + + /// Assert a data table row matches the expected values + #[track_caller] + pub fn assert_data_table_row(data_table: &DataTable, row_index: usize, expected: Vec<&str>) { + let values = data_table.display_value().unwrap().into_array().unwrap(); + + values.get_row(row_index).unwrap().iter().enumerate().for_each(|(index, value)| { + let value = value.to_string(); + let expected_value = expected[index]; + assert_eq!(&value, expected_value, "Expected row {row_index} to be {expected_value} at col {index}, but got {value}"); + }); + } + #[test] #[parallel] fn test_import_data_table_and_headers() { @@ -416,6 +524,31 @@ pub(crate) mod test { assert_eq!(data_table.columns.as_ref().unwrap()[0].display, false); } + #[test] + #[parallel] + fn test_data_table_sort() { + let (_, mut data_table) = new_data_table(); + data_table.apply_header_from_first_row(); + + let mut values = test_csv_values(); + values.remove(0); // remove header row + print_data_table(&data_table, Some("Original Data Table"), None); + + // sort by population city ascending + data_table.sort(0, SortDirection::Ascending).unwrap(); + print_data_table(&data_table, Some("Sorted by City"), None); + assert_data_table_row(&data_table, 0, values[1].clone()); + assert_data_table_row(&data_table, 1, values[2].clone()); + assert_data_table_row(&data_table, 2, values[0].clone()); + + // sort by population descending + data_table.sort(3, SortDirection::Descending).unwrap(); + print_data_table(&data_table, Some("Sorted by Population Descending"), None); + assert_data_table_row(&data_table, 0, values[1].clone()); + assert_data_table_row(&data_table, 1, values[0].clone()); + assert_data_table_row(&data_table, 2, values[2].clone()); + } + #[test] #[parallel] fn test_output_size() { diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 961570d037..7b400319dd 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -176,6 +176,7 @@ pub(crate) fn import_data_table_builder( }) .collect() }), + display_buffer: data_table.display_buffer, }; new_data_tables.insert(Pos { x: pos.x, y: pos.y }, data_table); @@ -350,6 +351,7 @@ pub(crate) fn export_data_table_runs( kind, name: data_table.name, columns, + display_buffer: data_table.display_buffer, readonly: data_table.readonly, last_modified: Some(data_table.last_modified), spill_error: data_table.spill_error, diff --git a/quadratic-core/src/grid/file/v1_6/file.rs b/quadratic-core/src/grid/file/v1_6/file.rs index 0d053ca877..2dc6b013a6 100644 --- a/quadratic-core/src/grid/file/v1_6/file.rs +++ b/quadratic-core/src/grid/file/v1_6/file.rs @@ -210,6 +210,7 @@ fn upgrade_code_runs( kind: v1_7::DataTableKindSchema::CodeRun(new_code_run), name: format!("Table {}", i), columns: None, + display_buffer: None, value, readonly: true, spill_error: code_run.spill_error, diff --git a/quadratic-core/src/grid/file/v1_7/schema.rs b/quadratic-core/src/grid/file/v1_7/schema.rs index 2fa300b7a6..2d679a175f 100644 --- a/quadratic-core/src/grid/file/v1_7/schema.rs +++ b/quadratic-core/src/grid/file/v1_7/schema.rs @@ -155,6 +155,7 @@ pub struct DataTableSchema { pub kind: DataTableKindSchema, pub name: String, pub columns: Option>, + pub display_buffer: Option>, pub value: OutputValueSchema, pub readonly: bool, pub spill_error: bool, diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index 0d1af0033f..bcb30feb0e 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -51,6 +51,11 @@ impl Sheet { self.data_tables.get(&pos) } + /// Returns a mutable DatatTable at a Pos + pub fn data_table_mut(&mut self, pos: Pos) -> Option<&mut DataTable> { + self.data_tables.get_mut(&pos) + } + pub fn delete_data_table(&mut self, pos: Pos) -> Result { self.data_tables .swap_remove(&pos) diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index baa8fd03db..cf6431f398 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -211,6 +211,7 @@ pub fn assert_cell_format_fill_color( } // Util to print a simple grid to assist in TDD +#[track_caller] pub fn print_table(grid_controller: &GridController, sheet_id: SheetId, rect: Rect) { let Some(sheet) = grid_controller.try_sheet(sheet_id) else { println!("Sheet not found"); @@ -220,6 +221,7 @@ pub fn print_table(grid_controller: &GridController, sheet_id: SheetId, rect: Re } // Util to print a simple grid to assist in TDD +#[track_caller] pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rect: Rect) { let Some(sheet) = grid_controller.try_sheet(sheet_id) else { println!("Sheet not found"); @@ -229,6 +231,7 @@ pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rec } /// Util to print the entire sheet +#[track_caller] pub fn print_sheet(sheet: &Sheet) { let bounds = sheet.bounds(true); if let GridBounds::NonEmpty(rect) = bounds { @@ -239,6 +242,7 @@ pub fn print_sheet(sheet: &Sheet) { } /// Util to print a simple grid to assist in TDD +#[track_caller] pub fn print_table_sheet(sheet: &Sheet, rect: Rect, disply_cell_values: bool) { let mut vals = vec![]; let mut builder = Builder::default(); @@ -271,7 +275,7 @@ pub fn print_table_sheet(sheet: &Sheet, rect: Rect, disply_cell_values: bool) { true => sheet.cell_value(pos), false => sheet .data_table(rect.min) - .unwrap() + .expect(&format!("Data table not found at {:?}", rect.min)) .cell_value_at(x as u32, y as u32), }; diff --git a/quadratic-core/src/values/array.rs b/quadratic-core/src/values/array.rs index 1462aa9f34..7f530d2c9f 100644 --- a/quadratic-core/src/values/array.rs +++ b/quadratic-core/src/values/array.rs @@ -1,4 +1,9 @@ -use std::{fmt, num::NonZeroU32}; +use std::{ + fmt, + iter::{Skip, StepBy}, + num::NonZeroU32, + slice::Iter, +}; use anyhow::{bail, Result}; use bigdecimal::BigDecimal; @@ -83,6 +88,7 @@ impl TryFrom for Vec { } } +// TODO(ddimaria): this function makes a copy of the data, consider consuming the vec impl From>> for Array { fn from(v: Vec>) -> Self { let w = v[0].len(); @@ -98,6 +104,7 @@ impl From>> for Array { } } +// TODO(ddimaria): this function makes a copy of the data, consider consuming the vec impl From>> for Array { fn from(v: Vec>) -> Self { let w = v[0].len(); @@ -207,6 +214,13 @@ impl Array { pub fn rows(&self) -> std::slice::Chunks<'_, CellValue> { self.values.chunks(self.width() as usize) } + /// Returns an iterator over a single col of the array. + pub fn col(&self, index: usize) -> StepBy>> { + self.values + .iter() + .skip(index) + .step_by(self.width() as usize) + } /// Remove the first row of the array and return it. pub fn shift(&mut self) -> Result> { let width = (self.width() as usize).min(self.values.len()); @@ -247,6 +261,13 @@ impl Array { let i = self.size().flatten_index(x, y)?; Ok(&self.values[i]) } + pub fn get_row(&self, index: usize) -> Result<&[CellValue], RunErrorMsg> { + let width = self.width() as usize; + let start = index * width; + let end = start + width; + + Ok(&self.values[start..end]) + } /// Sets the value at a given 0-indexed position in an array. Returns an /// error if `x` or `y` is out of range. pub fn set(&mut self, x: u32, y: u32, value: CellValue) -> Result<(), RunErrorMsg> { @@ -315,6 +336,23 @@ impl Array { } } + // convert from a Vec> to an Array, auto-picking the type if selected + pub fn from_str_vec(array: Vec>, auto_pick_type: bool) -> anyhow::Result { + let w = array[0].len(); + let h = array.len(); + let size = ArraySize::new_or_err(w as u32, h as u32)?; + let values = array + .into_iter() + .flatten() + .map(|s| match auto_pick_type { + true => CellValue::parse_from_str(s), + false => CellValue::from(s), + }) + .collect(); + + Ok(Array { size, values }) + } + pub fn from_string_list( start: Pos, sheet: &mut Sheet, diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index 62a38b2d1f..a44fa264f3 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -1,3 +1,5 @@ +use selection::Selection; + use super::*; #[wasm_bindgen] @@ -17,17 +19,32 @@ impl GridController { Ok(()) } - /// Flattens a Data Table + /// Converts a selection on the grid to a Data Table #[wasm_bindgen(js_name = "gridToDataTable")] pub fn js_grid_to_data_table( &mut self, - sheet_id: String, - rect: String, + selection: String, cursor: Option, ) -> Result<(), JsValue> { - let rect = serde_json::from_str::(&rect).map_err(|e| e.to_string())?; - let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; - self.grid_to_data_table(rect.to_sheet_rect(sheet_id), cursor); + let selection = Selection::from_str(&selection).map_err(|_| "Invalid selection")?; + let sheet_rect = selection.rects.unwrap()[0].to_sheet_rect(selection.sheet_id); + self.grid_to_data_table(sheet_rect, cursor); + + Ok(()) + } + + /// Flattens a Data Table + #[wasm_bindgen(js_name = "sortDataTable")] + pub fn js_sort_data_table( + &mut self, + selection: String, + column_index: u32, + sort_order: String, + cursor: Option, + ) -> Result<(), JsValue> { + let selection = Selection::from_str(&selection).map_err(|_| "Invalid selection")?; + let sheet_rect = selection.rects.unwrap()[0].to_sheet_rect(selection.sheet_id); + self.sort_data_table(sheet_rect, column_index, sort_order, cursor); Ok(()) } diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs new file mode 100644 index 0000000000..a0dea21ae2 --- /dev/null +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -0,0 +1,2 @@ +const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From 7edc95df5999b96032068427e3d4b6e6c6a51fd7 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 11 Oct 2024 14:55:24 -0600 Subject: [PATCH 024/373] Upgrade to v1_8, prepare for qa merge --- quadratic-core/src/grid/file/mod.rs | 41 ++- quadratic-core/src/grid/file/serialize/mod.rs | 2 +- quadratic-core/src/grid/file/sheet_schema.rs | 14 +- quadratic-core/src/grid/file/v1_6/file.rs | 3 +- quadratic-core/src/grid/file/v1_7/file.rs | 302 +++++++++++++++++ quadratic-core/src/grid/file/v1_7/mod.rs | 2 + .../src/grid/file/v1_7/run_error_schema.rs | 303 ++++++++++++++++++ quadratic-core/src/grid/file/v1_7/schema.rs | 145 +++++---- quadratic-core/src/grid/file/v1_8/mod.rs | 1 + quadratic-core/src/grid/file/v1_8/schema.rs | 182 +++++++++++ 10 files changed, 910 insertions(+), 85 deletions(-) create mode 100644 quadratic-core/src/grid/file/v1_7/file.rs create mode 100644 quadratic-core/src/grid/file/v1_7/run_error_schema.rs create mode 100644 quadratic-core/src/grid/file/v1_8/mod.rs create mode 100644 quadratic-core/src/grid/file/v1_8/schema.rs diff --git a/quadratic-core/src/grid/file/mod.rs b/quadratic-core/src/grid/file/mod.rs index 99d83645de..f65938b1c7 100644 --- a/quadratic-core/src/grid/file/mod.rs +++ b/quadratic-core/src/grid/file/mod.rs @@ -8,7 +8,7 @@ use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::str; -use v1_7::schema::GridSchema as current; +use v1_8::schema::GridSchema as current; pub mod serialize; pub mod sheet_schema; @@ -17,8 +17,9 @@ mod v1_4; mod v1_5; mod v1_6; mod v1_7; +mod v1_8; -pub static CURRENT_VERSION: &str = "1.7"; +pub static CURRENT_VERSION: &str = "1.8"; pub static SERIALIZATION_FORMAT: SerializationFormat = SerializationFormat::Json; pub static COMPRESSION_FORMAT: CompressionFormat = CompressionFormat::Zlib; pub static HEADER_SERIALIZATION_FORMAT: SerializationFormat = SerializationFormat::Bincode; @@ -31,6 +32,11 @@ pub struct FileVersion { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "version")] enum GridFile { + #[serde(rename = "1.8")] + V1_8 { + #[serde(flatten)] + grid: v1_8::schema::GridSchema, + }, #[serde(rename = "1.7")] V1_7 { #[serde(flatten)] @@ -59,16 +65,19 @@ enum GridFile { } impl GridFile { - fn into_latest(self) -> Result { + fn into_latest(self) -> Result { match self { - GridFile::V1_7 { grid } => Ok(grid), - GridFile::V1_6 { grid } => v1_6::file::upgrade(grid), - GridFile::V1_5 { grid } => v1_6::file::upgrade(v1_5::file::upgrade(grid)?), - GridFile::V1_4 { grid } => { - v1_6::file::upgrade(v1_5::file::upgrade(v1_4::file::upgrade(grid)?)?) + GridFile::V1_8 { grid } => Ok(grid), + GridFile::V1_7 { grid } => v1_7::file::upgrade(grid), + GridFile::V1_6 { grid } => v1_7::file::upgrade(v1_6::file::upgrade(grid)?), + GridFile::V1_5 { grid } => { + v1_7::file::upgrade(v1_6::file::upgrade(v1_5::file::upgrade(grid)?)?) } - GridFile::V1_3 { grid } => v1_6::file::upgrade(v1_5::file::upgrade( - v1_4::file::upgrade(v1_3::file::upgrade(grid)?)?, + GridFile::V1_4 { grid } => v1_7::file::upgrade(v1_6::file::upgrade( + v1_5::file::upgrade(v1_4::file::upgrade(grid)?)?, + )?), + GridFile::V1_3 { grid } => v1_7::file::upgrade(v1_6::file::upgrade( + v1_5::file::upgrade(v1_4::file::upgrade(v1_3::file::upgrade(grid)?)?)?, )?), } } @@ -99,10 +108,20 @@ fn import_binary(file_contents: Vec) -> Result { data, )?; drop(file_contents); - let schema = v1_6::file::upgrade(schema)?; + let schema = v1_7::file::upgrade(v1_6::file::upgrade(schema)?)?; Ok(serialize::import(schema)?) } "1.7" => { + let schema = decompress_and_deserialize::( + &SERIALIZATION_FORMAT, + &COMPRESSION_FORMAT, + data, + )?; + drop(file_contents); + let schema = v1_7::file::upgrade(schema)?; + Ok(serialize::import(schema)?) + } + "1.8" => { let schema = decompress_and_deserialize::( &SERIALIZATION_FORMAT, &COMPRESSION_FORMAT, diff --git a/quadratic-core/src/grid/file/serialize/mod.rs b/quadratic-core/src/grid/file/serialize/mod.rs index e99c21a15f..98fb9e945a 100644 --- a/quadratic-core/src/grid/file/serialize/mod.rs +++ b/quadratic-core/src/grid/file/serialize/mod.rs @@ -1,7 +1,7 @@ use anyhow::Result; use sheets::{export_sheet, import_sheet}; -pub use crate::grid::file::v1_7::schema::{self as current}; +pub use crate::grid::file::v1_8::schema::{self as current}; use crate::grid::Grid; use super::CURRENT_VERSION; diff --git a/quadratic-core/src/grid/file/sheet_schema.rs b/quadratic-core/src/grid/file/sheet_schema.rs index 2f15284e5d..0ca61337e1 100644 --- a/quadratic-core/src/grid/file/sheet_schema.rs +++ b/quadratic-core/src/grid/file/sheet_schema.rs @@ -1,5 +1,7 @@ +use super::serialize::sheets::import_sheet; use super::v1_6; use super::v1_7; +use super::v1_8; use crate::grid::Sheet; use anyhow::Result; use serde::{Deserialize, Serialize}; @@ -8,6 +10,7 @@ use serde::{Deserialize, Serialize}; #[allow(clippy::large_enum_variant)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum SheetSchema { + V1_8(v1_8::schema::SheetSchema), V1_7(v1_7::schema::SheetSchema), V1_6(v1_6::schema::Sheet), } @@ -16,10 +19,11 @@ impl SheetSchema { /// Imports a Sheet from the schema. pub fn into_latest(self) -> Result { match self { - SheetSchema::V1_7(sheet) => super::serialize::sheets::import_sheet(sheet), - SheetSchema::V1_6(sheet) => { - super::serialize::sheets::import_sheet(v1_6::file::upgrade_sheet(sheet)?) - } + SheetSchema::V1_8(sheet) => import_sheet(sheet), + SheetSchema::V1_7(sheet) => import_sheet(v1_7::file::upgrade_sheet(sheet)?), + SheetSchema::V1_6(sheet) => import_sheet(v1_7::file::upgrade_sheet( + v1_6::file::upgrade_sheet(sheet)?, + )?), } } } @@ -27,7 +31,7 @@ impl SheetSchema { /// Exports a Sheet to the latest schema version. pub fn export_sheet(sheet: Sheet) -> SheetSchema { let schema = super::serialize::sheets::export_sheet(sheet); - SheetSchema::V1_7(schema) + SheetSchema::V1_8(schema) } #[cfg(test)] diff --git a/quadratic-core/src/grid/file/v1_6/file.rs b/quadratic-core/src/grid/file/v1_6/file.rs index 2dc6b013a6..3842e08af7 100644 --- a/quadratic-core/src/grid/file/v1_6/file.rs +++ b/quadratic-core/src/grid/file/v1_6/file.rs @@ -7,6 +7,7 @@ use crate::{ file::{ serialize::borders::export_borders, v1_7::schema::{self as v1_7}, + v1_8::schema::{self as v1_8}, }, sheet::borders::{BorderStyle, Borders, CellBorderLine}, }, @@ -77,7 +78,7 @@ fn upgrade_borders(borders: current::Borders) -> Result { fn upgrade_code_runs( sheet: current::Sheet, -) -> Result> { +) -> Result> { sheet .code_runs .into_iter() diff --git a/quadratic-core/src/grid/file/v1_7/file.rs b/quadratic-core/src/grid/file/v1_7/file.rs new file mode 100644 index 0000000000..f9aa4c2d2c --- /dev/null +++ b/quadratic-core/src/grid/file/v1_7/file.rs @@ -0,0 +1,302 @@ +use anyhow::Result; + +use super::schema::{self as current}; +use crate::{ + color::Rgba, + grid::{ + file::{ + serialize::borders::export_borders, + v1_8::schema::{self as v1_8}, + }, + sheet::borders::{BorderStyle, Borders, CellBorderLine}, + }, +}; + +// index for old borders enum +// enum CellSide { +// Left = 0, +// Top = 1, +// Right = 2, +// Bottom = 3, +// } + +fn upgrade_borders(borders: current::Borders) -> Result { + fn convert_border_style(border_style: current::CellBorder) -> Result { + let mut color = Rgba::color_from_str(&border_style.color)?; + + // the alpha was set incorrectly to 1; should be 255 + color.alpha = 255; + + let line = match border_style.line.as_str() { + "line1" => CellBorderLine::Line1, + "line2" => CellBorderLine::Line2, + "line3" => CellBorderLine::Line3, + "dotted" => CellBorderLine::Dotted, + "dashed" => CellBorderLine::Dashed, + "double" => CellBorderLine::Double, + _ => return Err(anyhow::anyhow!("Invalid border line style")), + }; + Ok(BorderStyle { color, line }) + } + + let mut borders_new = Borders::default(); + for (col_id, sheet_borders) in borders { + if sheet_borders.is_empty() { + continue; + } + let col: i64 = col_id + .parse::() + .expect("Failed to parse col_id as i64"); + for (row, mut row_borders) in sheet_borders { + if let Some(left_old) = row_borders[0].take() { + if let Ok(style) = convert_border_style(left_old) { + borders_new.set(col, row, None, None, Some(style), None); + } + } + if let Some(right_old) = row_borders[2].take() { + if let Ok(style) = convert_border_style(right_old) { + borders_new.set(col, row, None, None, None, Some(style)); + } + } + if let Some(top_old) = row_borders[1].take() { + if let Ok(style) = convert_border_style(top_old) { + borders_new.set(col, row, Some(style), None, None, None); + } + } + if let Some(bottom_old) = row_borders[3].take() { + if let Ok(style) = convert_border_style(bottom_old) { + borders_new.set(col, row, None, Some(style), None, None); + } + } + } + } + + let borders = export_borders(borders_new); + Ok(borders) +} + +fn upgrade_code_runs( + sheet: current::Sheet, +) -> Result> { + sheet + .code_runs + .into_iter() + .enumerate() + .map(|(i, (pos, code_run))| { + let error = if let current::CodeRunResult::Err(error) = &code_run.result { + let new_error_msg = match error.msg.to_owned() { + current::RunErrorMsg::PythonError(msg) => { + v1_8::RunErrorMsgSchema::PythonError(msg) + } + current::RunErrorMsg::Unexpected(msg) => { + v1_8::RunErrorMsgSchema::Unexpected(msg) + } + current::RunErrorMsg::Spill => v1_8::RunErrorMsgSchema::Spill, + current::RunErrorMsg::Unimplemented(msg) => { + v1_8::RunErrorMsgSchema::Unimplemented(msg) + } + current::RunErrorMsg::UnknownError => v1_8::RunErrorMsgSchema::UnknownError, + current::RunErrorMsg::InternalError(msg) => { + v1_8::RunErrorMsgSchema::InternalError(msg) + } + current::RunErrorMsg::Unterminated(msg) => { + v1_8::RunErrorMsgSchema::Unterminated(msg) + } + current::RunErrorMsg::Expected { expected, got } => { + v1_8::RunErrorMsgSchema::Expected { expected, got } + } + current::RunErrorMsg::TooManyArguments { + func_name, + max_arg_count, + } => v1_8::RunErrorMsgSchema::TooManyArguments { + func_name, + max_arg_count, + }, + current::RunErrorMsg::MissingRequiredArgument { + func_name, + arg_name, + } => v1_8::RunErrorMsgSchema::MissingRequiredArgument { + func_name, + arg_name, + }, + current::RunErrorMsg::BadFunctionName => { + v1_8::RunErrorMsgSchema::BadFunctionName + } + current::RunErrorMsg::BadCellReference => { + v1_8::RunErrorMsgSchema::BadCellReference + } + current::RunErrorMsg::BadNumber => v1_8::RunErrorMsgSchema::BadNumber, + current::RunErrorMsg::BadOp { + op, + ty1, + ty2, + use_duration_instead, + } => v1_8::RunErrorMsgSchema::BadOp { + op, + ty1, + ty2, + use_duration_instead, + }, + current::RunErrorMsg::NaN => v1_8::RunErrorMsgSchema::NaN, + current::RunErrorMsg::ExactArraySizeMismatch { expected, got } => { + v1_8::RunErrorMsgSchema::ExactArraySizeMismatch { expected, got } + } + current::RunErrorMsg::ExactArrayAxisMismatch { + axis, + expected, + got, + } => v1_8::RunErrorMsgSchema::ExactArrayAxisMismatch { + axis, + expected, + got, + }, + current::RunErrorMsg::ArrayAxisMismatch { + axis, + expected, + got, + } => v1_8::RunErrorMsgSchema::ArrayAxisMismatch { + axis, + expected, + got, + }, + current::RunErrorMsg::EmptyArray => v1_8::RunErrorMsgSchema::EmptyArray, + current::RunErrorMsg::NonRectangularArray => { + v1_8::RunErrorMsgSchema::NonRectangularArray + } + current::RunErrorMsg::NonLinearArray => v1_8::RunErrorMsgSchema::NonLinearArray, + current::RunErrorMsg::ArrayTooBig => v1_8::RunErrorMsgSchema::ArrayTooBig, + current::RunErrorMsg::CircularReference => { + v1_8::RunErrorMsgSchema::CircularReference + } + current::RunErrorMsg::Overflow => v1_8::RunErrorMsgSchema::Overflow, + current::RunErrorMsg::DivideByZero => v1_8::RunErrorMsgSchema::DivideByZero, + current::RunErrorMsg::NegativeExponent => { + v1_8::RunErrorMsgSchema::NegativeExponent + } + current::RunErrorMsg::NotANumber => v1_8::RunErrorMsgSchema::NotANumber, + current::RunErrorMsg::Infinity => v1_8::RunErrorMsgSchema::Infinity, + current::RunErrorMsg::IndexOutOfBounds => { + v1_8::RunErrorMsgSchema::IndexOutOfBounds + } + current::RunErrorMsg::NoMatch => v1_8::RunErrorMsgSchema::NoMatch, + current::RunErrorMsg::InvalidArgument => { + v1_8::RunErrorMsgSchema::InvalidArgument + } + }; + let new_error = v1_8::RunErrorSchema { + span: None, + msg: new_error_msg, + }; + Some(new_error) + } else { + None + }; + let new_code_run = v1_8::CodeRunSchema { + formatted_code_string: code_run.formatted_code_string, + std_out: code_run.std_out, + std_err: code_run.std_err, + cells_accessed: code_run.cells_accessed, + error, + return_type: code_run.return_type, + line_number: code_run.line_number, + output_type: code_run.output_type, + }; + let value = if let current::CodeRunResult::Ok(value) = &code_run.result { + value.to_owned() + } else { + v1_8::OutputValueSchema::Single(v1_8::CellValueSchema::Blank) + }; + let new_data_table = v1_8::DataTableSchema { + kind: v1_8::DataTableKindSchema::CodeRun(new_code_run), + name: format!("Table {}", i), + columns: None, + display_buffer: None, + value, + readonly: true, + spill_error: code_run.spill_error, + last_modified: code_run.last_modified, + }; + Ok((v1_8::PosSchema::from(pos), new_data_table)) + }) + .collect::>>() +} + +pub fn upgrade_sheet(sheet: current::Sheet) -> Result { + let data_tables = upgrade_code_runs(sheet.clone())?; + let borders = upgrade_borders(sheet.borders.clone())?; + + Ok(v1_8::SheetSchema { + id: sheet.id, + name: sheet.name, + color: sheet.color, + order: sheet.order, + offsets: sheet.offsets, + columns: sheet.columns, + data_tables, + formats_all: sheet.formats_all, + formats_columns: sheet.formats_columns, + formats_rows: sheet.formats_rows, + rows_resize: sheet.rows_resize, + validations: sheet.validations, + borders, + }) +} + +pub fn upgrade(grid: current::GridSchema) -> Result { + let new_grid = v1_8::GridSchema { + version: Some("1.8".to_string()), + sheets: grid + .sheets + .into_iter() + .map(upgrade_sheet) + .collect::>()?, + }; + Ok(new_grid) +} + +#[cfg(test)] +mod tests { + use serial_test::parallel; + + use super::*; + + use crate::{ + controller::GridController, + grid::file::{export, import}, + }; + + const V1_5_FILE: &[u8] = + include_bytes!("../../../../../quadratic-rust-shared/data/grid/v1_5_simple.grid"); + + const V1_6_BORDERS_FILE: &[u8] = include_bytes!("../../../../test-files/borders_1_6.grid"); + + #[test] + #[parallel] + fn import_and_export_a_v1_5_file() { + let imported = import(V1_5_FILE.to_vec()).unwrap(); + let exported = export(imported.clone()).unwrap(); + let imported_copy = import(exported).unwrap(); + assert_eq!(imported_copy, imported); + } + + #[test] + #[parallel] + fn import_and_export_a_v1_6_borders_file() { + let imported = import(V1_6_BORDERS_FILE.to_vec()).unwrap(); + let exported = export(imported.clone()).unwrap(); + let imported_copy = import(exported).unwrap(); + assert_eq!(imported_copy, imported); + + let gc = GridController::from_grid(imported, 0); + let sheet_id = gc.sheet_ids()[0]; + let sheet = gc.sheet(sheet_id); + + let border_0_0 = sheet.borders.get(0, 0); + assert_eq!(border_0_0.top.unwrap().line, CellBorderLine::Line1); + assert_eq!(border_0_0.top.unwrap().color, Rgba::new(0, 0, 0, 255)); + assert_eq!(border_0_0.left.unwrap().line, CellBorderLine::Line1); + assert_eq!(border_0_0.left.unwrap().color, Rgba::new(0, 0, 0, 255)); + assert_eq!(border_0_0.bottom, None); + assert_eq!(border_0_0.right, None); + } +} diff --git a/quadratic-core/src/grid/file/v1_7/mod.rs b/quadratic-core/src/grid/file/v1_7/mod.rs index 1ce7e17666..7dd5af0626 100644 --- a/quadratic-core/src/grid/file/v1_7/mod.rs +++ b/quadratic-core/src/grid/file/v1_7/mod.rs @@ -1 +1,3 @@ +pub mod file; +pub mod run_error_schema; pub mod schema; diff --git a/quadratic-core/src/grid/file/v1_7/run_error_schema.rs b/quadratic-core/src/grid/file/v1_7/run_error_schema.rs new file mode 100644 index 0000000000..63989c1862 --- /dev/null +++ b/quadratic-core/src/grid/file/v1_7/run_error_schema.rs @@ -0,0 +1,303 @@ +//! Error for file schema. Needs to be kept updated with src/error.rs. + +use super::schema::{OutputSizeSchema, SpanSchema}; +use serde::{Deserialize, Serialize}; +use std::{borrow::Cow, num::NonZeroU32}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RunErrorSchema { + pub span: Option, + pub msg: RunErrorMsgSchema, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum AxisSchema { + X = 0, + Y = 1, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum RunErrorMsgSchema { + CodeRunError(Cow<'static, str>), + + Spill, + + // Miscellaneous errors + Unimplemented(Cow<'static, str>), + UnknownError, + InternalError(Cow<'static, str>), + + // Compile errors + Unterminated(Cow<'static, str>), + Expected { + expected: Cow<'static, str>, + got: Option>, + }, + Unexpected(Cow<'static, str>), + TooManyArguments { + func_name: Cow<'static, str>, + max_arg_count: usize, + }, + MissingRequiredArgument { + func_name: Cow<'static, str>, + arg_name: Cow<'static, str>, + }, + BadFunctionName, + BadCellReference, + BadNumber, + BadOp { + op: Cow<'static, str>, + ty1: Cow<'static, str>, + ty2: Option>, + use_duration_instead: bool, + }, + NaN, + + // Array size errors + ExactArraySizeMismatch { + expected: OutputSizeSchema, + got: OutputSizeSchema, + }, + ExactArrayAxisMismatch { + axis: AxisSchema, + expected: u32, + got: u32, + }, + ArrayAxisMismatch { + axis: AxisSchema, + expected: u32, + got: u32, + }, + EmptyArray, + NonRectangularArray, + NonLinearArray, + ArrayTooBig, + + // Runtime errors + CircularReference, + Overflow, + DivideByZero, + NegativeExponent, + NotANumber, + Infinity, + IndexOutOfBounds, + NoMatch, + InvalidArgument, +} + +// todo: There's probably a better way to do the From/Into between the types. + +impl RunErrorSchema { + pub fn from_grid_run_error(error: crate::RunError) -> Self { + Self { + span: error.span.map(|span| SpanSchema { + start: span.start, + end: span.end, + }), + msg: match error.msg.clone() { + crate::RunErrorMsg::CodeRunError(str) => RunErrorMsgSchema::CodeRunError(str), + crate::RunErrorMsg::Spill => RunErrorMsgSchema::Spill, + crate::RunErrorMsg::Unimplemented(str) => RunErrorMsgSchema::Unimplemented(str), + crate::RunErrorMsg::UnknownError => RunErrorMsgSchema::UnknownError, + crate::RunErrorMsg::InternalError(str) => RunErrorMsgSchema::InternalError(str), + + // Compile errors + crate::RunErrorMsg::Unterminated(str) => RunErrorMsgSchema::Unterminated(str), + crate::RunErrorMsg::Expected { expected, got } => { + RunErrorMsgSchema::Expected { expected, got } + } + crate::RunErrorMsg::Unexpected(str) => RunErrorMsgSchema::Unexpected(str), + crate::RunErrorMsg::TooManyArguments { + func_name, + max_arg_count, + } => RunErrorMsgSchema::TooManyArguments { + func_name, + max_arg_count, + }, + crate::RunErrorMsg::MissingRequiredArgument { + func_name, + arg_name, + } => RunErrorMsgSchema::MissingRequiredArgument { + func_name, + arg_name, + }, + crate::RunErrorMsg::BadFunctionName => RunErrorMsgSchema::BadFunctionName, + crate::RunErrorMsg::BadCellReference => RunErrorMsgSchema::BadCellReference, + crate::RunErrorMsg::BadNumber => RunErrorMsgSchema::BadNumber, + crate::RunErrorMsg::BadOp { + op, + ty1, + ty2, + use_duration_instead, + } => RunErrorMsgSchema::BadOp { + op, + ty1, + ty2, + use_duration_instead, + }, + crate::RunErrorMsg::NaN => RunErrorMsgSchema::NaN, + + // Array size errors + crate::RunErrorMsg::ExactArraySizeMismatch { expected, got } => { + RunErrorMsgSchema::ExactArraySizeMismatch { + expected: OutputSizeSchema { + w: expected.w.get() as i64, + h: expected.h.get() as i64, + }, + got: OutputSizeSchema { + w: got.w.get() as i64, + h: got.h.get() as i64, + }, + } + } + crate::RunErrorMsg::ExactArrayAxisMismatch { + axis, + expected, + got, + } => RunErrorMsgSchema::ExactArrayAxisMismatch { + axis: match axis { + crate::Axis::X => AxisSchema::X, + crate::Axis::Y => AxisSchema::Y, + }, + expected, + got, + }, + crate::RunErrorMsg::ArrayAxisMismatch { + axis, + expected, + got, + } => RunErrorMsgSchema::ArrayAxisMismatch { + axis: match axis { + crate::Axis::X => AxisSchema::X, + crate::Axis::Y => AxisSchema::Y, + }, + expected, + got, + }, + crate::RunErrorMsg::EmptyArray => RunErrorMsgSchema::EmptyArray, + crate::RunErrorMsg::NonRectangularArray => RunErrorMsgSchema::NonRectangularArray, + crate::RunErrorMsg::NonLinearArray => RunErrorMsgSchema::NonLinearArray, + crate::RunErrorMsg::ArrayTooBig => RunErrorMsgSchema::ArrayTooBig, + + crate::RunErrorMsg::CircularReference => RunErrorMsgSchema::CircularReference, + crate::RunErrorMsg::Overflow => RunErrorMsgSchema::Overflow, + crate::RunErrorMsg::DivideByZero => RunErrorMsgSchema::DivideByZero, + crate::RunErrorMsg::NegativeExponent => RunErrorMsgSchema::NegativeExponent, + crate::RunErrorMsg::NotANumber => RunErrorMsgSchema::NotANumber, + crate::RunErrorMsg::Infinity => RunErrorMsgSchema::Infinity, + crate::RunErrorMsg::IndexOutOfBounds => RunErrorMsgSchema::IndexOutOfBounds, + crate::RunErrorMsg::NoMatch => RunErrorMsgSchema::NoMatch, + crate::RunErrorMsg::InvalidArgument => RunErrorMsgSchema::InvalidArgument, + }, + } + } +} + +impl From for crate::RunError { + fn from(error: RunErrorSchema) -> crate::RunError { + crate::RunError { + span: error.span.map(|span| crate::Span { + start: span.start, + end: span.end, + }), + msg: match error.msg { + RunErrorMsgSchema::CodeRunError(str) => crate::RunErrorMsg::CodeRunError(str), + RunErrorMsgSchema::Spill => crate::RunErrorMsg::Spill, + RunErrorMsgSchema::Unimplemented(str) => crate::RunErrorMsg::Unimplemented(str), + RunErrorMsgSchema::UnknownError => crate::RunErrorMsg::UnknownError, + RunErrorMsgSchema::InternalError(str) => crate::RunErrorMsg::InternalError(str), + + // Compile errors + RunErrorMsgSchema::Unterminated(str) => crate::RunErrorMsg::Unterminated(str), + RunErrorMsgSchema::Expected { expected, got } => { + crate::RunErrorMsg::Expected { expected, got } + } + RunErrorMsgSchema::Unexpected(str) => crate::RunErrorMsg::Unexpected(str), + RunErrorMsgSchema::TooManyArguments { + func_name, + max_arg_count, + } => crate::RunErrorMsg::TooManyArguments { + func_name, + max_arg_count, + }, + RunErrorMsgSchema::MissingRequiredArgument { + func_name, + arg_name, + } => crate::RunErrorMsg::MissingRequiredArgument { + func_name, + arg_name, + }, + RunErrorMsgSchema::BadFunctionName => crate::RunErrorMsg::BadFunctionName, + RunErrorMsgSchema::BadCellReference => crate::RunErrorMsg::BadCellReference, + RunErrorMsgSchema::BadNumber => crate::RunErrorMsg::BadNumber, + RunErrorMsgSchema::BadOp { + op, + ty1, + ty2, + use_duration_instead, + } => crate::RunErrorMsg::BadOp { + op, + ty1, + ty2, + use_duration_instead, + }, + RunErrorMsgSchema::NaN => crate::RunErrorMsg::NaN, + + // Array size errors + RunErrorMsgSchema::ExactArraySizeMismatch { expected, got } => { + crate::RunErrorMsg::ExactArraySizeMismatch { + expected: crate::ArraySize { + w: NonZeroU32::new(expected.w as u32) + .unwrap_or(NonZeroU32::new(1).unwrap()), + h: NonZeroU32::new(expected.h as u32) + .unwrap_or(NonZeroU32::new(1).unwrap()), + }, + got: crate::ArraySize { + w: NonZeroU32::new(got.w as u32).unwrap_or(NonZeroU32::new(1).unwrap()), + h: NonZeroU32::new(got.h as u32).unwrap_or(NonZeroU32::new(1).unwrap()), + }, + } + } + RunErrorMsgSchema::ExactArrayAxisMismatch { + axis, + expected, + got, + } => crate::RunErrorMsg::ExactArrayAxisMismatch { + axis: match axis { + AxisSchema::X => crate::Axis::X, + AxisSchema::Y => crate::Axis::Y, + }, + expected, + got, + }, + RunErrorMsgSchema::ArrayAxisMismatch { + axis, + expected, + got, + } => crate::RunErrorMsg::ArrayAxisMismatch { + axis: match axis { + AxisSchema::X => crate::Axis::X, + AxisSchema::Y => crate::Axis::Y, + }, + expected, + got, + }, + RunErrorMsgSchema::EmptyArray => crate::RunErrorMsg::EmptyArray, + RunErrorMsgSchema::NonRectangularArray => crate::RunErrorMsg::NonRectangularArray, + RunErrorMsgSchema::NonLinearArray => crate::RunErrorMsg::NonLinearArray, + RunErrorMsgSchema::ArrayTooBig => crate::RunErrorMsg::ArrayTooBig, + + // Runtime errors + RunErrorMsgSchema::CircularReference => crate::RunErrorMsg::CircularReference, + RunErrorMsgSchema::Overflow => crate::RunErrorMsg::Overflow, + RunErrorMsgSchema::DivideByZero => crate::RunErrorMsg::DivideByZero, + RunErrorMsgSchema::NegativeExponent => crate::RunErrorMsg::NegativeExponent, + RunErrorMsgSchema::NotANumber => crate::RunErrorMsg::NotANumber, + RunErrorMsgSchema::Infinity => crate::RunErrorMsg::Infinity, + RunErrorMsgSchema::IndexOutOfBounds => crate::RunErrorMsg::IndexOutOfBounds, + RunErrorMsgSchema::NoMatch => crate::RunErrorMsg::NoMatch, + RunErrorMsgSchema::InvalidArgument => crate::RunErrorMsg::InvalidArgument, + }, + } + } +} diff --git a/quadratic-core/src/grid/file/v1_7/schema.rs b/quadratic-core/src/grid/file/v1_7/schema.rs index 2d679a175f..65218e8f4e 100644 --- a/quadratic-core/src/grid/file/v1_7/schema.rs +++ b/quadratic-core/src/grid/file/v1_7/schema.rs @@ -2,27 +2,29 @@ use std::collections::HashMap; use crate::grid::file::v1_6::schema as v1_6; use crate::grid::file::v1_6::schema_validation as v1_6_validation; -use chrono::{DateTime, Utc}; +use chrono::DateTime; +use chrono::NaiveDate; +use chrono::NaiveDateTime; +use chrono::NaiveTime; +use chrono::Utc; use serde::{Deserialize, Serialize}; +pub use super::run_error_schema::AxisSchema; +pub use super::run_error_schema::RunErrorMsgSchema; +pub use super::run_error_schema::RunErrorSchema; + pub type IdSchema = v1_6::Id; pub type PosSchema = v1_6::Pos; pub type RectSchema = v1_6::Rect; pub type SheetRectSchema = v1_6::SheetRect; pub type OffsetsSchema = v1_6::Offsets; -pub type RunErrorSchema = v1_6::RunError; pub type FormatSchema = v1_6::Format; pub type ValidationsSchema = v1_6_validation::Validations; pub type ResizeSchema = v1_6::Resize; -pub type CodeRunResultSchema = v1_6::CodeRunResult; -pub type OutputValueSchema = v1_6::OutputValue; -pub type OutputArraySchema = v1_6::OutputArray; pub type OutputSizeSchema = v1_6::OutputSize; pub type OutputValueValueSchema = v1_6::OutputValueValue; -pub type ColumnSchema = v1_6::Column; pub type NumericFormatKindSchema = v1_6::NumericFormatKind; pub type NumericFormatSchema = v1_6::NumericFormat; -pub type CellValueSchema = v1_6::CellValue; pub type CodeCellLanguageSchema = v1_6::CodeCellLanguage; pub type ConnectionKindSchema = v1_6::ConnectionKind; pub type CodeCellSchema = v1_6::CodeCell; @@ -32,10 +34,7 @@ pub type CellWrapSchema = v1_6::CellWrap; pub type CellBorderSchema = v1_6::CellBorder; pub type ColumnRepeatSchema = v1_6::ColumnRepeat; pub type RenderSizeSchema = v1_6::RenderSize; -pub type RunErrorMsgSchema = v1_6::RunErrorMsg; -pub type AxisSchema = v1_6::Axis; pub type SpanSchema = v1_6::Span; -pub type ImportSchema = v1_6::Import; pub type SelectionSchema = v1_6_validation::Selection; @@ -61,6 +60,74 @@ pub struct GridSchema { pub version: Option, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CodeRunSchema { + pub formatted_code_string: Option, + pub std_out: Option, + pub std_err: Option, + pub cells_accessed: Vec, + pub result: CodeRunResultSchema, + pub return_type: Option, + pub line_number: Option, + pub output_type: Option, + pub spill_error: bool, + pub last_modified: Option>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum CodeRunResultSchema { + Ok(OutputValueSchema), + Err(RunErrorSchema), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum OutputValueSchema { + Single(CellValueSchema), + Array(OutputArraySchema), +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct OutputArraySchema { + pub size: OutputSizeSchema, + pub values: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum CellValueSchema { + Blank, + Text(String), + Number(String), + Html(String), + Code(CodeCellSchema), + Logical(bool), + Instant(String), + Date(NaiveDate), + Time(NaiveTime), + DateTime(NaiveDateTime), + Duration(String), + Error(RunErrorSchema), + Image(String), +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ColumnSchema { + pub values: HashMap, + pub align: HashMap>, + pub vertical_align: HashMap>, + pub wrap: HashMap>, + pub numeric_format: HashMap>, + pub numeric_decimals: HashMap>, + pub numeric_commas: HashMap>, + pub bold: HashMap>, + pub italic: HashMap>, + pub underline: HashMap>, + pub strike_through: HashMap>, + pub text_color: HashMap>, + pub fill_color: HashMap>, + pub render_size: HashMap>, + pub date_time: HashMap>, +} + #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct RgbaSchema { pub red: u8, @@ -116,7 +183,7 @@ pub struct SheetSchema { pub order: String, pub offsets: OffsetsSchema, pub columns: Vec<(i64, ColumnSchema)>, - pub data_tables: Vec<(PosSchema, DataTableSchema)>, + pub code_runs: Vec<(PosSchema, CodeRunSchema)>, pub formats_all: Option, pub formats_columns: Vec<(i64, (FormatSchema, i64))>, pub formats_rows: Vec<(i64, (FormatSchema, i64))>, @@ -124,59 +191,3 @@ pub struct SheetSchema { pub validations: ValidationsSchema, pub borders: BordersSchema, } - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CodeRunSchema { - pub formatted_code_string: Option, - pub std_out: Option, - pub std_err: Option, - pub cells_accessed: Vec, - pub error: Option, - pub return_type: Option, - pub line_number: Option, - pub output_type: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct DataTableColumnSchema { - pub name: String, - pub display: bool, - pub value_index: u32, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum DataTableKindSchema { - CodeRun(CodeRunSchema), - Import(ImportSchema), -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct DataTableSchema { - pub kind: DataTableKindSchema, - pub name: String, - pub columns: Option>, - pub display_buffer: Option>, - pub value: OutputValueSchema, - pub readonly: bool, - pub spill_error: bool, - pub last_modified: Option>, -} - -impl From for AxisSchema { - fn from(val: i8) -> Self { - match val { - 0 => AxisSchema::X, - 1 => AxisSchema::Y, - _ => panic!("Invalid Axis value: {}", val), - } - } -} - -impl From for i8 { - fn from(val: AxisSchema) -> Self { - match val { - AxisSchema::X => 0, - AxisSchema::Y => 1, - } - } -} diff --git a/quadratic-core/src/grid/file/v1_8/mod.rs b/quadratic-core/src/grid/file/v1_8/mod.rs new file mode 100644 index 0000000000..1ce7e17666 --- /dev/null +++ b/quadratic-core/src/grid/file/v1_8/mod.rs @@ -0,0 +1 @@ +pub mod schema; diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs new file mode 100644 index 0000000000..2d679a175f --- /dev/null +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -0,0 +1,182 @@ +use std::collections::HashMap; + +use crate::grid::file::v1_6::schema as v1_6; +use crate::grid::file::v1_6::schema_validation as v1_6_validation; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +pub type IdSchema = v1_6::Id; +pub type PosSchema = v1_6::Pos; +pub type RectSchema = v1_6::Rect; +pub type SheetRectSchema = v1_6::SheetRect; +pub type OffsetsSchema = v1_6::Offsets; +pub type RunErrorSchema = v1_6::RunError; +pub type FormatSchema = v1_6::Format; +pub type ValidationsSchema = v1_6_validation::Validations; +pub type ResizeSchema = v1_6::Resize; +pub type CodeRunResultSchema = v1_6::CodeRunResult; +pub type OutputValueSchema = v1_6::OutputValue; +pub type OutputArraySchema = v1_6::OutputArray; +pub type OutputSizeSchema = v1_6::OutputSize; +pub type OutputValueValueSchema = v1_6::OutputValueValue; +pub type ColumnSchema = v1_6::Column; +pub type NumericFormatKindSchema = v1_6::NumericFormatKind; +pub type NumericFormatSchema = v1_6::NumericFormat; +pub type CellValueSchema = v1_6::CellValue; +pub type CodeCellLanguageSchema = v1_6::CodeCellLanguage; +pub type ConnectionKindSchema = v1_6::ConnectionKind; +pub type CodeCellSchema = v1_6::CodeCell; +pub type CellAlignSchema = v1_6::CellAlign; +pub type CellVerticalAlignSchema = v1_6::CellVerticalAlign; +pub type CellWrapSchema = v1_6::CellWrap; +pub type CellBorderSchema = v1_6::CellBorder; +pub type ColumnRepeatSchema = v1_6::ColumnRepeat; +pub type RenderSizeSchema = v1_6::RenderSize; +pub type RunErrorMsgSchema = v1_6::RunErrorMsg; +pub type AxisSchema = v1_6::Axis; +pub type SpanSchema = v1_6::Span; +pub type ImportSchema = v1_6::Import; + +pub type SelectionSchema = v1_6_validation::Selection; + +pub type ValidationSchema = v1_6_validation::Validation; +pub type ValidationStyleSchema = v1_6_validation::ValidationStyle; +pub type ValidationMessageSchema = v1_6_validation::ValidationMessage; +pub type ValidationErrorSchema = v1_6_validation::ValidationError; +pub type ValidationRuleSchema = v1_6_validation::ValidationRule; +pub type ValidationDateTimeSchema = v1_6_validation::ValidationDateTime; +pub type ValidationNumberSchema = v1_6_validation::ValidationNumber; +pub type ValidationTextSchema = v1_6_validation::ValidationText; +pub type ValidationLogicalSchema = v1_6_validation::ValidationLogical; +pub type ValidationListSchema = v1_6_validation::ValidationList; +pub type ValidationListSourceSchema = v1_6_validation::ValidationListSource; +pub type TextMatchSchema = v1_6_validation::TextMatch; +pub type TextCaseSchema = v1_6_validation::TextCase; +pub type DateTimeRangeSchema = v1_6_validation::DateTimeRange; +pub type NumberRangeSchema = v1_6_validation::NumberRange; + +#[derive(Default, Debug, PartialEq, Serialize, Deserialize, Clone)] +pub struct GridSchema { + pub sheets: Vec, + pub version: Option, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RgbaSchema { + pub red: u8, + pub green: u8, + pub blue: u8, + pub alpha: u8, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum CellBorderLineSchema { + #[default] + Line1, + Line2, + Line3, + Dotted, + Dashed, + Double, + Clear, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct BorderStyleTimestampSchema { + pub color: RgbaSchema, + pub line: CellBorderLineSchema, + pub timestamp: u32, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct BorderStyleCellSchema { + pub top: Option, + pub bottom: Option, + pub left: Option, + pub right: Option, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct BordersSchema { + pub all: BorderStyleCellSchema, + pub columns: HashMap, + pub rows: HashMap, + + pub left: HashMap>>, + pub right: HashMap>>, + pub top: HashMap>>, + pub bottom: HashMap>>, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SheetSchema { + pub id: IdSchema, + pub name: String, + pub color: Option, + pub order: String, + pub offsets: OffsetsSchema, + pub columns: Vec<(i64, ColumnSchema)>, + pub data_tables: Vec<(PosSchema, DataTableSchema)>, + pub formats_all: Option, + pub formats_columns: Vec<(i64, (FormatSchema, i64))>, + pub formats_rows: Vec<(i64, (FormatSchema, i64))>, + pub rows_resize: Vec<(i64, ResizeSchema)>, + pub validations: ValidationsSchema, + pub borders: BordersSchema, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CodeRunSchema { + pub formatted_code_string: Option, + pub std_out: Option, + pub std_err: Option, + pub cells_accessed: Vec, + pub error: Option, + pub return_type: Option, + pub line_number: Option, + pub output_type: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DataTableColumnSchema { + pub name: String, + pub display: bool, + pub value_index: u32, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum DataTableKindSchema { + CodeRun(CodeRunSchema), + Import(ImportSchema), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DataTableSchema { + pub kind: DataTableKindSchema, + pub name: String, + pub columns: Option>, + pub display_buffer: Option>, + pub value: OutputValueSchema, + pub readonly: bool, + pub spill_error: bool, + pub last_modified: Option>, +} + +impl From for AxisSchema { + fn from(val: i8) -> Self { + match val { + 0 => AxisSchema::X, + 1 => AxisSchema::Y, + _ => panic!("Invalid Axis value: {}", val), + } + } +} + +impl From for i8 { + fn from(val: AxisSchema) -> Self { + match val { + AxisSchema::X => 0, + AxisSchema::Y => 1, + } + } +} From 7f259f052969e05e4fdd4eb79c06be036f079d90 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 14 Oct 2024 08:35:19 -0600 Subject: [PATCH 025/373] Merge conflict remains --- quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx index 6b9b314b97..fb303ba90e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx @@ -3,10 +3,6 @@ import { Action } from '@/app/actions/actions'; import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; import { gridHeadingAtom } from '@/app/atoms/gridHeadingAtom'; -<<<<<<< HEAD -import { events } from '@/app/events/events'; -======= ->>>>>>> origin/qa import { sheets } from '@/app/grid/controller/Sheets'; import { focusGrid } from '@/app/helpers/focusGrid'; import { keyboardShortcutEnumToDisplay } from '@/app/helpers/keyboardShortcutsDisplay'; From 634ead2b04e5d75028fd6af6d42c658f70044b20 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 14 Oct 2024 09:58:14 -0600 Subject: [PATCH 026/373] Complete implementing sort for rust operations + tests --- .../execute_operation/execute_data_table.rs | 130 +++++++++++------- .../execution/execute_operation/mod.rs | 2 +- quadratic-core/src/grid/data_table.rs | 34 ++++- quadratic-core/src/grid/sheet/data_table.rs | 6 +- quadratic-core/src/pos.rs | 8 ++ quadratic-core/src/test_util.rs | 14 +- 6 files changed, 131 insertions(+), 63 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 1cb3742e69..d391439e7d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -5,7 +5,7 @@ use crate::{ active_transactions::pending_transaction::PendingTransaction, operations::operation::Operation, GridController, }, - grid::DataTable, + grid::{DataTable, SortDirection}, ArraySize, CellValue, Pos, Rect, SheetRect, }; @@ -283,56 +283,45 @@ impl GridController { transaction: &mut PendingTransaction, op: Operation, ) -> Result<()> { - // if let Operation::SortDataTable { - // sheet_rect, - // column_index, - // sort_order, - // } = op - // { - // // let sheet_id = sheet_pos.sheet_id; - // // let pos = Pos::from(sheet_pos); - // // let sheet = self.try_sheet_mut_result(sheet_id)?; - // // let data_table_pos = sheet.first_data_table_within(pos)?; - // // let mut data_table = sheet.data_table_mut(data_table_pos)?; - - // // let sort_order = match sort_order.as_str() { - // // "asc" => SortOrder::Asc, - // // "desc" => SortOrder::Desc, - // // _ => bail!("Invalid sort order"), - // // }; - - // // data_table.sort(column_index, sort_order); - - // // self.send_to_wasm(transaction, &sheet_rect)?; - - // // let forward_operations = vec![ - // // Operation::SetCellValues { - // // sheet_pos, - // // values: CellValues::from(CellValue::Import(import)), - // // }, - // // Operation::SetCodeRun { - // // sheet_pos, - // // code_run: Some(data_table), - // // index: 0, - // // }, - // // ]; - - // // let reverse_operations = vec![Operation::SetCellValues { - // // sheet_pos, - // // values: CellValues::from(old_values), - // // }]; - - // // self.data_table_operations( - // // transaction, - // // &sheet_rect, - // // forward_operations, - // // reverse_operations, - // // ); - - // return Ok(()); - // }; + if let Operation::SortDataTable { + sheet_rect, + column_index, + sort_order, + } = op.to_owned() + { + let sheet_id = sheet_rect.sheet_id; + // let rect = Rect::from(sheet_rect); + let sheet = self.try_sheet_mut_result(sheet_id)?; + // let sheet_pos = sheet_rect.min.to_sheet_pos(sheet_id); + let data_table_pos = sheet.first_data_table_within(sheet_rect.min)?; + let data_table = sheet.data_table_mut(data_table_pos)?; + + let sort_order_enum = match sort_order.as_str() { + "asc" => SortDirection::Ascending, + "desc" => SortDirection::Descending, + _ => bail!("Invalid sort order"), + }; - bail!("Expected Operation::GridToDataTable in execute_grid_to_data_table"); + data_table.sort(column_index as usize, sort_order_enum)?; + + self.send_to_wasm(transaction, &sheet_rect)?; + + // TODO(ddimaria): remove this clone + let forward_operations = vec![op.clone()]; + + let reverse_operations = vec![op.clone()]; + + self.data_table_operations( + transaction, + &sheet_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::SortDataTable in execute_sort_data_table"); } } @@ -341,7 +330,9 @@ mod tests { use crate::{ controller::user_actions::import::tests::{assert_simple_csv, simple_csv}, grid::SheetId, - test_util::{assert_cell_value_row, print_data_table, print_table}, + test_util::{ + assert_cell_value_row, assert_data_table_cell_value_row, print_data_table, print_table, + }, SheetPos, }; @@ -446,4 +437,41 @@ mod tests { assert_simple_csv(&gc, sheet_id, pos, file_name); } + + #[test] + fn test_execute_sort_data_table() { + let (mut gc, sheet_id, pos, _) = simple_csv(); + let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); + data_table.apply_header_from_first_row(); + + print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + + let max = Pos::new(3, 10); + let sheet_rect = SheetRect::new_pos_span(pos, max, sheet_id); + let op = Operation::SortDataTable { + sheet_rect, + column_index: 0, + sort_order: "asc".into(), + }; + let mut transaction = PendingTransaction::default(); + gc.execute_sort_data_table(&mut transaction, op).unwrap(); + + // assert_eq!(transaction.forward_operations.len(), 1); + // assert_eq!(transaction.reverse_operations.len(), 2); + + gc.finalize_transaction(transaction); + print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + + let first_row = vec!["Concord", "NH", "United States", "42605"]; + assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 0, first_row); + + let second_row = vec!["Marlborough", "MA", "United States", "38334"]; + assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 1, second_row); + + let third_row = vec!["Northbridge", "MA", "United States", "14061"]; + assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 2, third_row); + + let last_row = vec!["Westborough", "MA", "United States", "29313"]; + assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 9, last_row); + } } diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 93c4964138..98f326d16d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -42,7 +42,7 @@ impl GridController { self.execute_grid_to_data_table(transaction, op), ), Operation::SortDataTable { .. } => Self::handle_execution_operation_result( - self.execute_grid_to_data_table(transaction, op), + self.execute_sort_data_table(transaction, op), ), Operation::ComputeCode { .. } => self.execute_compute_code(transaction, op), Operation::SetCellFormats { .. } => self.execute_set_cell_formats(transaction, op), diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 745f09deca..ba4cb96966 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -251,6 +251,19 @@ impl DataTable { Ok(array.into()) } + pub fn display_value_from_buffer_at( + &self, + display_buffer: &Vec, + pos: Pos, + ) -> Result<&CellValue> { + let y = display_buffer + .get(pos.y as usize) + .ok_or_else(|| anyhow!("Y {} out of bounds: {}", pos.y, display_buffer.len()))?; + let cell_value = self.value.get(pos.x as u32, *y as u32)?; + + Ok(cell_value) + } + pub fn display_value(&self) -> Result { match self.display_buffer { Some(ref display_buffer) => self.display_value_from_buffer(display_buffer), @@ -258,6 +271,13 @@ impl DataTable { } } + pub fn display_value_at(&self, pos: Pos) -> Result<&CellValue> { + match self.display_buffer { + Some(ref display_buffer) => self.display_value_from_buffer_at(display_buffer, pos), + None => Ok(self.value.get(pos.x as u32, pos.y as u32)?), + } + } + /// Helper functtion to get the CodeRun from the DataTable. /// Returns `None` if the DataTableKind is not CodeRun. pub fn code_run(&self) -> Option<&CodeRun> { @@ -299,7 +319,7 @@ impl DataTable { if self.spill_error { None } else { - self.value.get(x, y).ok() + self.display_value_at((x, y).into()).ok() } } @@ -422,7 +442,11 @@ pub(crate) mod test { /// Util to print a data table when testing #[track_caller] - pub fn print_data_table(data_table: &DataTable, title: Option<&str>, max: Option) { + pub fn pretty_print_data_table( + data_table: &DataTable, + title: Option<&str>, + max: Option, + ) { let mut builder = Builder::default(); let array = data_table.display_value().unwrap().into_array().unwrap(); let max = max.unwrap_or(array.height() as usize); @@ -530,18 +554,18 @@ pub(crate) mod test { let mut values = test_csv_values(); values.remove(0); // remove header row - print_data_table(&data_table, Some("Original Data Table"), None); + pretty_print_data_table(&data_table, Some("Original Data Table"), None); // sort by population city ascending data_table.sort(0, SortDirection::Ascending).unwrap(); - print_data_table(&data_table, Some("Sorted by City"), None); + pretty_print_data_table(&data_table, Some("Sorted by City"), None); assert_data_table_row(&data_table, 0, values[1].clone()); assert_data_table_row(&data_table, 1, values[2].clone()); assert_data_table_row(&data_table, 2, values[0].clone()); // sort by population descending data_table.sort(3, SortDirection::Descending).unwrap(); - print_data_table(&data_table, Some("Sorted by Population Descending"), None); + pretty_print_data_table(&data_table, Some("Sorted by Population Descending"), None); assert_data_table_row(&data_table, 0, values[1].clone()); assert_data_table_row(&data_table, 1, values[0].clone()); assert_data_table_row(&data_table, 2, values[2].clone()); diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index bcb30feb0e..3e8708d12c 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -52,8 +52,10 @@ impl Sheet { } /// Returns a mutable DatatTable at a Pos - pub fn data_table_mut(&mut self, pos: Pos) -> Option<&mut DataTable> { - self.data_tables.get_mut(&pos) + pub fn data_table_mut(&mut self, pos: Pos) -> Result<&mut DataTable> { + self.data_tables + .get_mut(&pos) + .ok_or_else(|| anyhow!("Data table not found at {:?}", pos)) } pub fn delete_data_table(&mut self, pos: Pos) -> Result { diff --git a/quadratic-core/src/pos.rs b/quadratic-core/src/pos.rs index 81847aff02..ddc836d9a2 100644 --- a/quadratic-core/src/pos.rs +++ b/quadratic-core/src/pos.rs @@ -86,6 +86,14 @@ impl From<(i32, i32)> for Pos { } } } +impl From<(u32, u32)> for Pos { + fn from(pos: (u32, u32)) -> Self { + Pos { + x: pos.0 as i64, + y: pos.1 as i64, + } + } +} impl From for Pos { fn from(sheet_pos: SheetPos) -> Self { Pos { diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index cf6431f398..b023781e52 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -1,7 +1,10 @@ use crate::{ controller::GridController, formulas::replace_internal_cell_references, - grid::{Bold, CodeCellLanguage, FillColor, GridBounds, Sheet, SheetId}, + grid::{ + test::pretty_print_data_table, Bold, CodeCellLanguage, FillColor, GridBounds, Sheet, + SheetId, + }, CellValue, Pos, Rect, }; use std::collections::HashMap; @@ -223,11 +226,14 @@ pub fn print_table(grid_controller: &GridController, sheet_id: SheetId, rect: Re // Util to print a simple grid to assist in TDD #[track_caller] pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rect: Rect) { - let Some(sheet) = grid_controller.try_sheet(sheet_id) else { + if let Some(sheet) = grid_controller.try_sheet(sheet_id) { + let data_table = sheet.data_table(rect.min).unwrap(); + let max = rect.max.y - rect.min.y; + pretty_print_data_table(data_table, None, Some(max as usize)); + } else { println!("Sheet not found"); return; - }; - print_table_sheet(sheet, rect, false); + } } /// Util to print the entire sheet From be353f1497667bc9849af2836912b24aa60c82fa Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 14 Oct 2024 18:16:08 -0600 Subject: [PATCH 027/373] Implement sort on the client, store sort state --- quadratic-client/src/app/actions/actions.ts | 1 + .../src/app/actions/dataTableSpec.ts | 11 ++- .../src/app/grid/sheet/SheetCursor.ts | 4 - .../app/gridGL/HTMLGrid/GridContextMenu.tsx | 6 +- .../quadraticCore/coreClientMessages.ts | 13 ++- .../quadraticCore/quadraticCore.ts | 12 +++ .../web-workers/quadraticCore/worker/core.ts | 5 ++ .../quadraticCore/worker/coreClient.ts | 4 + .../execute_operation/execute_data_table.rs | 16 ++-- .../src/controller/operations/data_table.rs | 4 +- .../src/controller/operations/operation.rs | 8 +- .../src/controller/user_actions/data_table.rs | 10 +-- quadratic-core/src/grid/data_table.rs | 18 +++- .../src/grid/file/serialize/data_table.rs | 28 +++++- .../src/grid/file/serialize/sheets.rs | 4 +- quadratic-core/src/grid/file/v1_7/file.rs | 20 +---- quadratic-core/src/grid/file/v1_7/mod.rs | 2 + quadratic-core/src/grid/file/v1_7/schema.rs | 1 + quadratic-core/src/grid/file/v1_8/schema.rs | 89 +++++++++++-------- quadratic-core/src/grid/mod.rs | 2 +- quadratic-core/src/test_util.rs | 12 +-- .../wasm_bindings/controller/data_table.rs | 9 +- 22 files changed, 176 insertions(+), 103 deletions(-) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 0392ac365f..9744e2e3e2 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -141,4 +141,5 @@ export enum Action { DeleteColumn = 'delete_column', FlattenDataTable = 'flatten_data_table', GridToDataTable = 'grid_to_data_table', + SortDataTable = 'sort_data_table', } diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 1bc36608c6..3ad2af607c 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -5,7 +5,7 @@ import { sheets } from '../grid/controller/Sheets'; import { ActionSpecRecord } from './actionsSpec'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -type DataTableSpec = Pick; +type DataTableSpec = Pick; export type DataTableActionArgs = { [Action.FlattenDataTable]: { name: string }; @@ -33,4 +33,13 @@ export const dataTableSpec: DataTableSpec = { quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); }, }, + [Action.SortDataTable]: { + label: 'Sort Data Table', + Icon: PersonAddIcon, + isAvailable: () => isDataTable(), + run: async () => { + const { x, y } = sheets.sheet.cursor.cursorPosition; + quadraticCore.sortDataTable(sheets.sheet.id, x, y, 0, 'asc', sheets.getCursorPosition()); + }, + }, }; diff --git a/quadratic-client/src/app/grid/sheet/SheetCursor.ts b/quadratic-client/src/app/grid/sheet/SheetCursor.ts index 8eb66e22cb..3b1f5d95f9 100644 --- a/quadratic-client/src/app/grid/sheet/SheetCursor.ts +++ b/quadratic-client/src/app/grid/sheet/SheetCursor.ts @@ -294,8 +294,4 @@ export class SheetCursor { onlySingleSelection(): boolean { return !this.multiCursor?.length && !this.columnRow; } - - hasDataTable(oneCell?: boolean): boolean { - return true; - } } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx index fb303ba90e..afd4e46364 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx @@ -34,7 +34,6 @@ export const GridContextMenu = () => { const ref = useRef(null); const isColumnRowAvailable = sheets.sheet.cursor.hasOneColumnRowSelection(true); - const isDataTable = sheets.sheet.cursor.hasDataTable(true); return (
{ )} + + + + +
); diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index 405493e14c..5d0c1d7d18 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -1030,6 +1030,16 @@ export interface ClientCoreGridToDataTable { cursor: string; } +export interface ClientCoreSortDataTable { + type: 'clientCoreSortDataTable'; + sheetId: string; + x: number; + y: number; + column_index: number; + sort_order: string; + cursor: string; +} + export type ClientCoreMessage = | ClientCoreLoad | ClientCoreGetCodeCell @@ -1111,7 +1121,8 @@ export type ClientCoreMessage = | ClientCoreInsertColumn | ClientCoreInsertRow | ClientCoreFlattenDataTable - | ClientCoreGridToDataTable; + | ClientCoreGridToDataTable + | ClientCoreSortDataTable; export type CoreClientMessage = | CoreClientGetCodeCell diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index 1d66b0e5a8..e037baae92 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -1188,6 +1188,18 @@ class QuadraticCore { cursor, }); } + + sortDataTable(sheetId: string, x: number, y: number, column_index: number, sort_order: string, cursor: string) { + this.send({ + type: 'clientCoreSortDataTable', + sheetId, + x, + y, + column_index, + sort_order, + cursor, + }); + } //#endregion } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 59316816fa..51de947ae6 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1124,6 +1124,11 @@ class Core { if (!this.gridController) throw new Error('Expected gridController to be defined'); this.gridController.gridToDataTable(JSON.stringify(selection, bigIntReplacer), cursor); } + + sortDataTable(sheetId: string, x: number, y: number, column_index: number, sort_order: string, cursor: string) { + if (!this.gridController) throw new Error('Expected gridController to be defined'); + this.gridController.sortDataTable(sheetId, posToPos(x, y), column_index, sort_order, cursor); + } } export const core = new Core(); diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index d8ad1014d5..5a2c5b6625 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -589,6 +589,10 @@ class CoreClient { core.gridToDataTable(e.data.selection, e.data.cursor); return; + case 'clientCoreSortDataTable': + core.sortDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.column_index, e.data.sort_order, e.data.cursor); + return; + default: if (e.data.id !== undefined) { // handle responses from requests to quadratic-core diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index d391439e7d..61ddc19a20 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -166,7 +166,8 @@ impl GridController { // Pull out the data table via a swap, removing it from the sheet let data_table = sheet.delete_data_table(data_table_pos)?; - let values = data_table.value.to_owned().into_array()?; + let old_values = data_table.value.to_owned().into_array()?; + let values = data_table.display_value()?.into_array()?; let ArraySize { w, h } = values.size(); let sheet_pos = data_table_pos.to_sheet_pos(sheet_id); @@ -176,7 +177,7 @@ impl GridController { }; let sheet_rect = SheetRect::new_pos_span(data_table_pos, max, sheet_id); - let old_values = sheet.set_cell_values(sheet_rect.into(), &values); + let _ = sheet.set_cell_values(sheet_rect.into(), &values); let old_cell_values = CellValues::from(old_values); let cell_values = CellValues::from(values); @@ -284,16 +285,17 @@ impl GridController { op: Operation, ) -> Result<()> { if let Operation::SortDataTable { - sheet_rect, + sheet_pos, column_index, sort_order, } = op.to_owned() { - let sheet_id = sheet_rect.sheet_id; + let sheet_id = sheet_pos.sheet_id; // let rect = Rect::from(sheet_rect); let sheet = self.try_sheet_mut_result(sheet_id)?; // let sheet_pos = sheet_rect.min.to_sheet_pos(sheet_id); - let data_table_pos = sheet.first_data_table_within(sheet_rect.min)?; + let sheet_rect = SheetRect::single_sheet_pos(sheet_pos); + let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; let data_table = sheet.data_table_mut(data_table_pos)?; let sort_order_enum = match sort_order.as_str() { @@ -447,9 +449,9 @@ mod tests { print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); let max = Pos::new(3, 10); - let sheet_rect = SheetRect::new_pos_span(pos, max, sheet_id); + let sheet_pos = SheetPos::from((pos, sheet_id)); let op = Operation::SortDataTable { - sheet_rect, + sheet_pos, column_index: 0, sort_order: "asc".into(), }; diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index cdcf344666..3ba1aa1d03 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -20,13 +20,13 @@ impl GridController { pub fn sort_data_table_operations( &self, - sheet_rect: SheetRect, + sheet_pos: SheetPos, column_index: u32, sort_order: String, _cursor: Option, ) -> Vec { vec![Operation::SortDataTable { - sheet_rect, + sheet_pos, column_index, sort_order, }] diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 380b59800a..a48e89c9bc 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -52,7 +52,7 @@ pub enum Operation { sheet_rect: SheetRect, }, SortDataTable { - sheet_rect: SheetRect, + sheet_pos: SheetPos, column_index: u32, sort_order: String, }, @@ -219,14 +219,14 @@ impl fmt::Display for Operation { write!(fmt, "GridToDataTable {{ sheet_rect: {} }}", sheet_rect) } Operation::SortDataTable { - sheet_rect, + sheet_pos, column_index, sort_order, } => { write!( fmt, - "SortDataTable {{ sheet_rect: {}, column_index: {}, sort_order: {} }}", - sheet_rect, column_index, sort_order + "SortDataTable {{ sheet_pos: {}, column_index: {}, sort_order: {} }}", + sheet_pos, column_index, sort_order ) } Operation::SetCellFormats { .. } => write!(fmt, "SetCellFormats {{ todo }}",), diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index 71f5ffec76..ff72663b40 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -37,17 +37,13 @@ impl GridController { pub fn sort_data_table( &mut self, - sheet_rect: SheetRect, + sheet_pos: SheetPos, column_index: u32, sort_order: String, cursor: Option, ) { - let ops = self.sort_data_table_operations( - sheet_rect, - column_index, - sort_order, - cursor.to_owned(), - ); + let ops = + self.sort_data_table_operations(sheet_pos, column_index, sort_order, cursor.to_owned()); self.start_user_transaction(ops, cursor, TransactionName::GridToDataTable); } } diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index ba4cb96966..cab5ea6751 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -23,6 +23,12 @@ pub struct DataTableColumn { pub value_index: u32, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum DataTableKind { + CodeRun(CodeRun), + Import(Import), +} + impl DataTableColumn { pub fn new(name: String, display: bool, value_index: u32) -> Self { DataTableColumn { @@ -40,9 +46,9 @@ pub enum SortDirection { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub enum DataTableKind { - CodeRun(CodeRun), - Import(Import), +pub struct DataTableSortOrder { + pub column_index: usize, + pub direction: SortDirection, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -50,6 +56,7 @@ pub struct DataTable { pub kind: DataTableKind, pub name: String, pub columns: Option>, + pub sort: Option>, pub display_buffer: Option>, pub value: Value, pub readonly: bool, @@ -91,6 +98,7 @@ impl DataTable { kind, name: name.into(), columns: None, + sort: None, display_buffer: None, value, readonly, @@ -110,6 +118,7 @@ impl DataTable { kind: DataTableKind, name: &str, columns: Option>, + sort: Option>, display_buffer: Option>, value: Value, readonly: bool, @@ -119,6 +128,7 @@ impl DataTable { kind, name: name.into(), columns, + sort, display_buffer, value, readonly, @@ -409,7 +419,7 @@ impl DataTable { } #[cfg(test)] -pub(crate) mod test { +pub mod test { use std::collections::HashSet; use super::*; diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 3a60e81512..dca4d3aa1f 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -4,7 +4,7 @@ use indexmap::IndexMap; use itertools::Itertools; use crate::{ - grid::{CodeRun, DataTable, DataTableColumn, DataTableKind}, + grid::{CodeRun, DataTable, DataTableColumn, DataTableKind, DataTableSortOrder, SortDirection}, ArraySize, Axis, Pos, RunError, RunErrorMsg, Value, }; @@ -176,6 +176,17 @@ pub(crate) fn import_data_table_builder( }) .collect() }), + sort: data_table.sort.map(|sort| { + sort.into_iter() + .map(|sort| DataTableSortOrder { + column_index: sort.column_index, + direction: match sort.direction { + current::SortDirectionSchema::Ascending => SortDirection::Ascending, + current::SortDirectionSchema::Descending => SortDirection::Descending, + }, + }) + .collect() + }), display_buffer: data_table.display_buffer, }; @@ -301,7 +312,7 @@ pub(crate) fn export_code_run(code_run: CodeRun) -> current::CodeRunSchema { } } -pub(crate) fn export_data_table_runs( +pub(crate) fn export_data_tables( data_tables: IndexMap, ) -> Vec<(current::PosSchema, current::DataTableSchema)> { data_tables @@ -341,6 +352,18 @@ pub(crate) fn export_data_table_runs( .collect() }); + let sort = data_table.sort.map(|sort| { + sort.into_iter() + .map(|item| current::DataTableSortOrderSchema { + column_index: item.column_index, + direction: match item.direction { + SortDirection::Ascending => current::SortDirectionSchema::Ascending, + SortDirection::Descending => current::SortDirectionSchema::Descending, + }, + }) + .collect() + }); + let kind = match data_table.kind { DataTableKind::CodeRun(code_run) => { let code_run = export_code_run(code_run); @@ -357,6 +380,7 @@ pub(crate) fn export_data_table_runs( kind, name: data_table.name, columns, + sort, display_buffer: data_table.display_buffer, readonly: data_table.readonly, last_modified: Some(data_table.last_modified), diff --git a/quadratic-core/src/grid/file/serialize/sheets.rs b/quadratic-core/src/grid/file/serialize/sheets.rs index 25315fa852..ca2c4a5cc4 100644 --- a/quadratic-core/src/grid/file/serialize/sheets.rs +++ b/quadratic-core/src/grid/file/serialize/sheets.rs @@ -11,7 +11,7 @@ use super::{ borders::{export_borders, import_borders}, column::{export_column_builder, import_column_builder}, current, - data_table::{export_data_table_runs, import_data_table_builder}, + data_table::{export_data_tables, import_data_table_builder}, format::{ export_format, export_formats, export_rows_size, import_format, import_formats, import_rows_size, @@ -60,7 +60,7 @@ pub(crate) fn export_sheet(sheet: Sheet) -> current::SheetSchema { validations: export_validations(sheet.validations), rows_resize: export_rows_size(sheet.rows_resize), borders: export_borders(sheet.borders), - data_tables: export_data_table_runs(sheet.data_tables), + data_tables: export_data_tables(sheet.data_tables), columns: export_column_builder(sheet.columns), } } diff --git a/quadratic-core/src/grid/file/v1_7/file.rs b/quadratic-core/src/grid/file/v1_7/file.rs index cdd849a891..b4a0e0ff26 100644 --- a/quadratic-core/src/grid/file/v1_7/file.rs +++ b/quadratic-core/src/grid/file/v1_7/file.rs @@ -5,25 +5,6 @@ use crate::grid::file::{ v1_8::schema::{self as v1_8}, }; -fn convert_cell_value(cell_value: v1_7::CellValueSchema) -> v1_8::CellValueSchema { - match cell_value { - v1_7::CellValueSchema::Blank => v1_8::CellValueSchema::Blank, - v1_7::CellValueSchema::Text(str) => v1_8::CellValueSchema::Text(str), - v1_7::CellValueSchema::Number(str) => v1_8::CellValueSchema::Number(str), - v1_7::CellValueSchema::Html(str) => v1_8::CellValueSchema::Html(str), - v1_7::CellValueSchema::Code(code_cell) => v1_8::CellValueSchema::Code(code_cell), - v1_7::CellValueSchema::Logical(bool) => v1_8::CellValueSchema::Logical(bool), - v1_7::CellValueSchema::Instant(str) => v1_8::CellValueSchema::Instant(str), - v1_7::CellValueSchema::Date(date) => v1_8::CellValueSchema::Date(date), - v1_7::CellValueSchema::Time(time) => v1_8::CellValueSchema::Time(time), - v1_7::CellValueSchema::DateTime(datetime) => v1_8::CellValueSchema::DateTime(datetime), - v1_7::CellValueSchema::Duration(str) => v1_8::CellValueSchema::Duration(str), - v1_7::CellValueSchema::Error(run_error) => v1_8::CellValueSchema::Error(run_error), - v1_7::CellValueSchema::Image(str) => v1_8::CellValueSchema::Image(str), - v1_7::CellValueSchema::Import(str) => v1_8::CellValueSchema::Import(str), - } -} - fn upgrade_code_runs( code_runs: Vec<(v1_7::PosSchema, v1_7::CodeRunSchema)>, ) -> Result> { @@ -68,6 +49,7 @@ fn upgrade_code_runs( kind: v1_8::DataTableKindSchema::CodeRun(new_code_run), name: format!("Table {}", i), columns: None, + sort: None, display_buffer: None, value, readonly: true, diff --git a/quadratic-core/src/grid/file/v1_7/mod.rs b/quadratic-core/src/grid/file/v1_7/mod.rs index 7dd5af0626..d2a284ee25 100644 --- a/quadratic-core/src/grid/file/v1_7/mod.rs +++ b/quadratic-core/src/grid/file/v1_7/mod.rs @@ -1,3 +1,5 @@ pub mod file; pub mod run_error_schema; pub mod schema; + +pub use schema::*; diff --git a/quadratic-core/src/grid/file/v1_7/schema.rs b/quadratic-core/src/grid/file/v1_7/schema.rs index a5a401d9b7..cb4ecbecf8 100644 --- a/quadratic-core/src/grid/file/v1_7/schema.rs +++ b/quadratic-core/src/grid/file/v1_7/schema.rs @@ -27,6 +27,7 @@ pub type NumericFormatKindSchema = v1_6::NumericFormatKind; pub type NumericFormatSchema = v1_6::NumericFormat; pub type ConnectionKindSchema = v1_6::ConnectionKind; pub type CodeCellSchema = v1_6::CodeCell; +pub type CodeCellLanguageSchema = v1_6::CodeCellLanguage; pub type CellAlignSchema = v1_6::CellAlign; pub type CellVerticalAlignSchema = v1_6::CellVerticalAlign; pub type CellWrapSchema = v1_6::CellWrap; diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index 2eb158fcde..2545b72360 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -1,36 +1,34 @@ -use crate::grid::file::v1_6::schema as v1_6; -use crate::grid::file::v1_6::schema_validation as v1_6_validation; use crate::grid::file::v1_7; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -pub type IdSchema = v1_6::Id; -pub type PosSchema = v1_6::Pos; -pub type RectSchema = v1_6::Rect; -pub type SheetRectSchema = v1_6::SheetRect; -pub type OffsetsSchema = v1_6::Offsets; +pub type IdSchema = v1_7::IdSchema; +pub type PosSchema = v1_7::PosSchema; +pub type RectSchema = v1_7::RectSchema; +pub type SheetRectSchema = v1_7::SheetRectSchema; +pub type OffsetsSchema = v1_7::OffsetsSchema; pub type RunErrorSchema = v1_7::run_error_schema::RunErrorSchema; -pub type FormatSchema = v1_6::Format; -pub type ValidationsSchema = v1_6_validation::Validations; -pub type ResizeSchema = v1_6::Resize; -pub type CodeRunResultSchema = v1_6::CodeRunResult; +pub type FormatSchema = v1_7::FormatSchema; +pub type ValidationsSchema = v1_7::ValidationsSchema; +pub type ResizeSchema = v1_7::ResizeSchema; +pub type CodeRunResultSchema = v1_7::CodeRunResultSchema; pub type OutputValueSchema = v1_7::schema::OutputValueSchema; pub type OutputArraySchema = v1_7::schema::OutputArraySchema; pub type OutputSizeSchema = v1_7::schema::OutputSizeSchema; -pub type OutputValueValueSchema = v1_6::OutputValueValue; +pub type OutputValueValueSchema = v1_7::OutputValueValueSchema; pub type ColumnSchema = v1_7::schema::ColumnSchema; -pub type NumericFormatKindSchema = v1_6::NumericFormatKind; -pub type NumericFormatSchema = v1_6::NumericFormat; +pub type NumericFormatKindSchema = v1_7::NumericFormatKindSchema; +pub type NumericFormatSchema = v1_7::NumericFormatSchema; pub type CellValueSchema = v1_7::schema::CellValueSchema; -pub type CodeCellLanguage = v1_6::CodeCellLanguage; -pub type ConnectionKindSchema = v1_6::ConnectionKind; -pub type CodeCellSchema = v1_6::CodeCell; -pub type CellAlignSchema = v1_6::CellAlign; -pub type CellVerticalAlignSchema = v1_6::CellVerticalAlign; -pub type CellWrapSchema = v1_6::CellWrap; -pub type CellBorderSchema = v1_6::CellBorder; -pub type ColumnRepeatSchema = v1_6::ColumnRepeat; -pub type RenderSizeSchema = v1_6::RenderSize; +pub type CodeCellLanguage = v1_7::CodeCellLanguageSchema; +pub type ConnectionKindSchema = v1_7::ConnectionKindSchema; +pub type CodeCellSchema = v1_7::CodeCellSchema; +pub type CellAlignSchema = v1_7::CellAlignSchema; +pub type CellVerticalAlignSchema = v1_7::CellVerticalAlignSchema; +pub type CellWrapSchema = v1_7::CellWrapSchema; +pub type CellBorderSchema = v1_7::CellBorderSchema; +pub type ColumnRepeatSchema = v1_7::ColumnRepeatSchema; +pub type RenderSizeSchema = v1_7::RenderSizeSchema; pub type RunErrorMsgSchema = v1_7::run_error_schema::RunErrorMsgSchema; pub type AxisSchema = v1_7::schema::AxisSchema; pub type SpanSchema = v1_7::schema::SpanSchema; @@ -42,23 +40,23 @@ pub type CellBorderLineSchema = v1_7::schema::CellBorderLineSchema; pub type RgbaSchema = v1_7::schema::RgbaSchema; pub type BorderStyleCell = v1_7::schema::BorderStyleCellSchema; -pub type SelectionSchema = v1_6_validation::Selection; +pub type SelectionSchema = v1_7::SelectionSchema; -pub type ValidationSchema = v1_6_validation::Validation; -pub type ValidationStyleSchema = v1_6_validation::ValidationStyle; -pub type ValidationMessageSchema = v1_6_validation::ValidationMessage; -pub type ValidationErrorSchema = v1_6_validation::ValidationError; -pub type ValidationRuleSchema = v1_6_validation::ValidationRule; -pub type ValidationDateTimeSchema = v1_6_validation::ValidationDateTime; -pub type ValidationNumberSchema = v1_6_validation::ValidationNumber; -pub type ValidationTextSchema = v1_6_validation::ValidationText; -pub type ValidationLogicalSchema = v1_6_validation::ValidationLogical; -pub type ValidationListSchema = v1_6_validation::ValidationList; -pub type ValidationListSourceSchema = v1_6_validation::ValidationListSource; -pub type TextMatchSchema = v1_6_validation::TextMatch; -pub type TextCaseSchema = v1_6_validation::TextCase; -pub type DateTimeRangeSchema = v1_6_validation::DateTimeRange; -pub type NumberRangeSchema = v1_6_validation::NumberRange; +pub type ValidationSchema = v1_7::ValidationSchema; +pub type ValidationStyleSchema = v1_7::ValidationStyleSchema; +pub type ValidationMessageSchema = v1_7::ValidationMessageSchema; +pub type ValidationErrorSchema = v1_7::ValidationErrorSchema; +pub type ValidationRuleSchema = v1_7::ValidationRuleSchema; +pub type ValidationDateTimeSchema = v1_7::ValidationDateTimeSchema; +pub type ValidationNumberSchema = v1_7::ValidationNumberSchema; +pub type ValidationTextSchema = v1_7::ValidationTextSchema; +pub type ValidationLogicalSchema = v1_7::ValidationLogicalSchema; +pub type ValidationListSchema = v1_7::ValidationListSchema; +pub type ValidationListSourceSchema = v1_7::ValidationListSourceSchema; +pub type TextMatchSchema = v1_7::TextMatchSchema; +pub type TextCaseSchema = v1_7::TextCaseSchema; +pub type DateTimeRangeSchema = v1_7::DateTimeRangeSchema; +pub type NumberRangeSchema = v1_7::NumberRangeSchema; #[derive(Default, Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct GridSchema { @@ -108,11 +106,24 @@ pub enum DataTableKindSchema { Import(ImportSchema), } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum SortDirectionSchema { + Ascending, + Descending, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DataTableSortOrderSchema { + pub column_index: usize, + pub direction: SortDirectionSchema, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DataTableSchema { pub kind: DataTableKindSchema, pub name: String, pub columns: Option>, + pub sort: Option>, pub display_buffer: Option>, pub value: OutputValueSchema, pub readonly: bool, diff --git a/quadratic-core/src/grid/mod.rs b/quadratic-core/src/grid/mod.rs index 4972625d06..b7b49bc290 100644 --- a/quadratic-core/src/grid/mod.rs +++ b/quadratic-core/src/grid/mod.rs @@ -27,7 +27,7 @@ mod borders; mod bounds; mod code_run; mod column; -mod data_table; +pub mod data_table; pub mod file; pub mod formats; pub mod formatting; diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index b023781e52..bbe0bedd70 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -1,10 +1,7 @@ use crate::{ controller::GridController, formulas::replace_internal_cell_references, - grid::{ - test::pretty_print_data_table, Bold, CodeCellLanguage, FillColor, GridBounds, Sheet, - SheetId, - }, + grid::{Bold, CodeCellLanguage, FillColor, GridBounds, Sheet, SheetId}, CellValue, Pos, Rect, }; use std::collections::HashMap; @@ -225,11 +222,16 @@ pub fn print_table(grid_controller: &GridController, sheet_id: SheetId, rect: Re // Util to print a simple grid to assist in TDD #[track_caller] +#[cfg(test)] pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rect: Rect) { if let Some(sheet) = grid_controller.try_sheet(sheet_id) { let data_table = sheet.data_table(rect.min).unwrap(); let max = rect.max.y - rect.min.y; - pretty_print_data_table(data_table, None, Some(max as usize)); + crate::grid::data_table::test::pretty_print_data_table( + data_table, + None, + Some(max as usize), + ); } else { println!("Sheet not found"); return; diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index a44fa264f3..15ea6df4a9 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -37,14 +37,15 @@ impl GridController { #[wasm_bindgen(js_name = "sortDataTable")] pub fn js_sort_data_table( &mut self, - selection: String, + sheet_id: String, + pos: String, column_index: u32, sort_order: String, cursor: Option, ) -> Result<(), JsValue> { - let selection = Selection::from_str(&selection).map_err(|_| "Invalid selection")?; - let sheet_rect = selection.rects.unwrap()[0].to_sheet_rect(selection.sheet_id); - self.sort_data_table(sheet_rect, column_index, sort_order, cursor); + let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; + let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; + self.sort_data_table(pos.to_sheet_pos(sheet_id), column_index, sort_order, cursor); Ok(()) } From 2cc7dbc8326953086e432ec6e79ee9ae629dd18e Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 15 Oct 2024 08:30:07 -0700 Subject: [PATCH 028/373] table name --- quadratic-client/src/app/events/events.ts | 1 + .../app/gridGL/HTMLGrid/HTMLGridContainer.tsx | 2 + .../HTMLGrid/tablesOverlay/TablesOverlay.tsx | 104 ++++++++++++++++++ .../src/app/gridGL/cells/CellsArray.ts | 33 ++++++ .../src/app/gridGL/helpers/intersects.ts | 3 +- .../app/gridGL/interaction/pointer/Pointer.ts | 2 +- .../interaction/pointer/pointerCursor.ts | 39 ++++++- .../src/app/quadratic-core-types/index.d.ts | 2 +- quadratic-core/src/grid/js_types.rs | 4 +- quadratic-core/src/grid/sheet/rendering.rs | 3 + quadratic-rust-shared/src/auto_gen_path.rs | 4 +- 11 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 95971e56de..3640111b20 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -35,6 +35,7 @@ interface EventTypes { search: (found?: SheetPosTS[], current?: number) => void; hoverCell: (cell?: JsRenderCodeCell | EditingCell | ErrorValidation) => void; hoverTooltip: (rect?: Rectangle, text?: string, subtext?: string) => void; + hoverTable: (table?: JsRenderCodeCell) => void; zoom: (scale: number) => void; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index 3d85f235eb..af6b14aac1 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -15,6 +15,7 @@ import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Following } from '@/app/ui/components/Following'; import { ReactNode, useCallback, useEffect, useState } from 'react'; import { SuggestionDropDown } from './SuggestionDropdown'; +import { TableOverlay } from './tablesOverlay/TablesOverlay'; interface Props { parent?: HTMLDivElement; @@ -130,6 +131,7 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { pointerEvents: 'none', }} > + diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx new file mode 100644 index 0000000000..59a1640639 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx @@ -0,0 +1,104 @@ +//! This draws the table heading and provides its context menu. +//! +//! There are two overlays: the first is the active table that the sheet cursor +//! is in. The second is the table that the mouse is hovering over. + +import { useEffect, useMemo, useState } from 'react'; +import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { events } from '@/app/events/events'; +import { Rectangle } from 'pixi.js'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { pixiApp } from '../../pixiApp/PixiApp'; + +export const TableOverlay = () => { + const [table, setTable] = useState(undefined); + const [rect, setRect] = useState(undefined); + const [name, setName] = useState(undefined); + useEffect(() => { + const check = () => { + const cursor = sheets.sheet.cursor.cursorPosition; + let checkTable = pixiApp.cellsSheets.current?.cellsArray.getTableCursor(cursor); + setTable(checkTable); + if (checkTable) { + const sheet = sheets.sheet; + const rect = sheet.getScreenRectangle(checkTable.x, checkTable.y, checkTable.w, checkTable.h); + setRect(rect); + setName(checkTable.name); + } else { + setRect(undefined); + setName(undefined); + } + }; + events.on('cursorPosition', check); + return () => { + events.off('cursorPosition', check); + }; + }, []); + + const [hoverTable, setHoverTable] = useState(undefined); + const [hoverRect, setHoverRect] = useState(undefined); + const [hoverName, setHoverName] = useState(undefined); + useEffect(() => { + const set = (checkTable?: JsRenderCodeCell) => { + setHoverTable(checkTable); + if (checkTable) { + const sheet = sheets.sheet; + const rect = sheet.getScreenRectangle(checkTable.x, checkTable.y, checkTable.w, checkTable.h); + setHoverRect(rect); + setHoverName(checkTable.name); + } else { + setHoverRect(undefined); + setHoverName(undefined); + } + }; + events.on('hoverTable', set); + return () => { + events.off('hoverTable', set); + }; + }, [table]); + + const tableRender = useMemo(() => { + if (table && rect && name !== undefined) { + return ( +
+
{name}
+
+ ); + } + }, [name, rect, table]); + + const hoverTableRender = useMemo(() => { + if (hoverTable && hoverRect && hoverName !== undefined) { + return ( +
+
{hoverName}
+
+ ); + } + }, [hoverName, hoverRect, hoverTable]); + + console.log(table, hoverTable); + + if (!tableRender && !hoverTableRender) return null; + + return ( + <> + {tableRender} + {hoverTable !== table && hoverTableRender} + + ); +}; diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts index 79c18b3532..8c501ec5d7 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsArray.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsArray.ts @@ -21,6 +21,7 @@ const SPILL_FILL_ALPHA = 0.025; export class CellsArray extends Container { private cellsSheet: CellsSheet; private codeCells: Map; + private tables: Map; private particles: ParticleContainer; // only used for the spill error indicators (lines are drawn using sprites in particles for performance) @@ -34,6 +35,7 @@ export class CellsArray extends Container { this.cellsSheet = cellsSheet; this.lines = []; this.codeCells = new Map(); + this.tables = new Map(); events.on('renderCodeCells', this.renderCodeCells); events.on('sheetOffsets', this.sheetOffsets); events.on('updateCodeCell', this.updateCodeCell); @@ -195,6 +197,15 @@ export class CellsArray extends Container { } else { this.drawBox(start, end, tint); } + + // save the entire table for hover checks + if (!codeCell.spill_error) { + const endTable = this.sheet.getCellOffsets(Number(codeCell.x) + codeCell.w, Number(codeCell.y) + codeCell.h); + this.tables.set( + this.key(codeCell.x, codeCell.y), + new Rectangle(start.x, start.y, endTable.x - start.x, endTable.y - start.y) + ); + } } private drawBox(start: Rectangle, end: Rectangle, tint: number) { @@ -287,4 +298,26 @@ export class CellsArray extends Container { return rect.contains(x, y); }); } + + getCodeCellWorld(point: Point): JsRenderCodeCell | undefined { + for (const [index, tableRect] of this.tables.entries()) { + if (tableRect.contains(point.x, point.y)) { + return this.codeCells.get(index); + } + } + } + + getTableCursor(point: Coordinate): JsRenderCodeCell | undefined { + for (const codeCell of this.codeCells.values()) { + if ( + !codeCell.spill_error && + codeCell.x <= point.x && + codeCell.x + codeCell.w > point.x && + codeCell.y <= point.y && + codeCell.y + codeCell.h > point.y + ) { + return codeCell; + } + } + } } diff --git a/quadratic-client/src/app/gridGL/helpers/intersects.ts b/quadratic-client/src/app/gridGL/helpers/intersects.ts index 30d5ff1c4a..5eb37ed2d7 100644 --- a/quadratic-client/src/app/gridGL/helpers/intersects.ts +++ b/quadratic-client/src/app/gridGL/helpers/intersects.ts @@ -2,6 +2,7 @@ import { RectangleLike } from '@/app/grid/sheet/SheetCursor'; import { Rect } from '@/app/quadratic-core-types'; import { rectToRectangle } from '@/app/web-workers/quadraticCore/worker/rustConversions'; import { Circle, Point, Rectangle } from 'pixi.js'; +import { Coordinate } from '../types/size'; function left(rectangle: RectangleLike): number { return Math.min(rectangle.x, rectangle.x + rectangle.width); @@ -19,7 +20,7 @@ function bottom(rectangle: RectangleLike): number { return Math.max(rectangle.y, rectangle.y + rectangle.height); } -function rectanglePoint(rectangle: RectangleLike, point: Point): boolean { +function rectanglePoint(rectangle: RectangleLike, point: Point | Coordinate): boolean { return ( point.x >= left(rectangle) && point.x <= right(rectangle) && diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts index 9dd69883c9..a994355760 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts @@ -124,7 +124,7 @@ export class Pointer { this.pointerHeading.pointerMove(world) || this.pointerAutoComplete.pointerMove(world) || this.pointerDown.pointerMove(world, event) || - this.pointerCursor.pointerMove(world) || + this.pointerCursor.pointerMove(world, event) || this.pointerLink.pointerMove(world, event); this.updateCursor(); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts b/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts index 575504c784..e36484ac07 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts @@ -6,11 +6,13 @@ import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { Point } from 'pixi.js'; +import { intersects } from '../../helpers/intersects'; export class PointerCursor { private lastInfo?: JsRenderCodeCell | EditingCell | ErrorValidation; + private lastTable?: JsRenderCodeCell; - private checkHoverCell(world: Point) { + private checkHoverCell(world: Point, event: PointerEvent) { if (!pixiApp.cellsSheets.current) throw new Error('Expected cellsSheets.current to be defined in PointerCursor'); const cell = sheets.sheet.getColumnRow(world.x, world.y); const editingCell = multiplayer.cellIsBeingEdited(cell.x, cell.y, sheets.sheet.id); @@ -21,34 +23,59 @@ export class PointerCursor { return; } + let foundCodeCell = false; const codeCell = pixiApp.cellsSheets.current.cellsMarkers.intersectsCodeInfo(world); if (codeCell) { if (this.lastInfo?.x !== codeCell.x || this.lastInfo?.y !== codeCell.y) { events.emit('hoverCell', codeCell); this.lastInfo = codeCell; } - return; + foundCodeCell = true; + } + + let foundTable = false; + const table = pixiApp.cellsSheets.current.cellsArray.getCodeCellWorld(world); + if (table) { + if (this.lastTable?.x !== table.x || this.lastTable?.y !== table.y) { + events.emit('hoverTable', table); + this.lastTable = table; + } + foundTable = true; + } else if (this.lastTable) { + const tablesHeading = document.querySelector('.tables-overlay'); + if (tablesHeading) { + const rect = tablesHeading.getBoundingClientRect(); + if (intersects.rectanglePoint(rect, { x: event.clientX, y: event.clientY })) { + foundTable = true; + } + } } + let foundValidation = false; const validation = pixiApp.cellsSheets.current.cellsLabels.intersectsErrorMarkerValidation(world); if (validation) { if (this.lastInfo?.x !== validation.x || this.lastInfo?.y !== validation.y) { events.emit('hoverCell', validation); this.lastInfo = validation; } - return; + foundValidation = true; } - if (this.lastInfo) { + if (!foundCodeCell && !foundValidation && this.lastInfo) { events.emit('hoverCell'); this.lastInfo = undefined; } + + if (!foundTable && this.lastTable) { + events.emit('hoverTable'); + this.lastTable = undefined; + } } - pointerMove(world: Point): void { + pointerMove(world: Point, event: PointerEvent): void { const cursor = pixiApp.pointer.pointerHeading.cursor ?? pixiApp.pointer.pointerAutoComplete.cursor; pixiApp.canvas.style.cursor = cursor ?? 'unset'; multiplayer.sendMouseMove(world.x, world.y); - this.checkHoverCell(world); + this.checkHoverCell(world, event); } } diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index df17a15288..4e577f7808 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -36,7 +36,7 @@ export interface JsOffset { column: number | null, row: number | null, size: num export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, } +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index 211768b410..a65dad0f67 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -183,8 +183,7 @@ pub struct JsCodeCell { pub cells_accessed: Option>, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub struct JsRenderCodeCell { pub x: i32, pub y: i32, @@ -193,6 +192,7 @@ pub struct JsRenderCodeCell { pub language: CodeCellLanguage, pub state: JsRenderCodeCellState, pub spill_error: Option>, + pub name: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 4d632821ae..893f4313b7 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -416,6 +416,7 @@ impl Sheet { }, state, spill_error, + name: data_table.name.clone(), }) } @@ -458,6 +459,7 @@ impl Sheet { language: code_cell_value.language, state, spill_error, + name: data_table.name.clone(), }) } _ => None, // this should not happen. A CodeRun should always have a CellValue::Code. @@ -1079,6 +1081,7 @@ mod tests { language: CodeCellLanguage::Python, state: crate::grid::js_types::JsRenderCodeCellState::Success, spill_error: None, + name: "Table 1".to_string(), }) ); } diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index a0dea21ae2..8df3d2b4d5 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From 6a29ae758fcc133981b71a50635e773b6f72491c Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 15 Oct 2024 09:24:31 -0700 Subject: [PATCH 029/373] add viewport events from negative --- quadratic-client/src/app/events/events.ts | 6 ++++++ .../app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx | 8 ++++++++ quadratic-client/src/app/gridGL/pixiApp/Update.ts | 4 ++++ quadratic-client/src/app/gridGL/pixiApp/Viewport.ts | 4 ++++ 4 files changed, 22 insertions(+) diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 3640111b20..270ffda6ff 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -128,6 +128,12 @@ interface EventTypes { gridContextMenu: (world: Point, row: number | null, column: number | null) => void; suggestionDropdownKeyboard: (key: 'ArrowDown' | 'ArrowUp' | 'Enter' | 'Escape' | 'Tab') => void; + + // use this to set a drawing element to dirty + viewportChanged: () => void; + + // use this only if you need to immediately get the viewport's value (ie, from React) + viewportChangedReady: () => void; } export const events = new EventEmitter(); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx index 59a1640639..95bf88114a 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx @@ -35,6 +35,14 @@ export const TableOverlay = () => { }; }, []); + // const [viewableRect, setViewableRect] = useState(undefined); + // useEffect(() => { + // const checkViewport = () => { + + // } + // events.on('') + // }, [rect]); + const [hoverTable, setHoverTable] = useState(undefined); const [hoverRect, setHoverRect] = useState(undefined); const [hoverName, setHoverName] = useState(undefined); diff --git a/quadratic-client/src/app/gridGL/pixiApp/Update.ts b/quadratic-client/src/app/gridGL/pixiApp/Update.ts index c500ac9437..49bf238346 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Update.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Update.ts @@ -79,6 +79,10 @@ export class Update { if (dirty) { pixiApp.viewportChanged(); this.sendRenderViewport(); + + // signals to react that the viewport has changed (so it can update any + // related positioning) + events.emit('viewportChangedReady'); } } diff --git a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts index 9b99518cef..ca652c051b 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Viewport.ts @@ -3,6 +3,7 @@ import { Drag, Viewport as PixiViewport } from 'pixi-viewport'; import { Point, Rectangle } from 'pixi.js'; import { isMobile } from 'react-device-detect'; import { HORIZONTAL_SCROLL_KEY, Wheel, ZOOM_KEY } from '../pixiOverride/Wheel'; +import { events } from '@/app/events/events'; const MULTIPLAYER_VIEWPORT_EASE_TIME = 100; const MINIMUM_VIEWPORT_SCALE = 0.01; @@ -45,6 +46,9 @@ export class Viewport extends PixiViewport { // hack to ensure pointermove works outside of canvas this.off('pointerout'); + + this.on('moved', () => events.emit('viewportChanged')); + this.on('zoomed', () => events.emit('viewportChanged')); } loadViewport() { From 4c192c9536dd549d096ed8f6c4a9a57f69879208 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 15 Oct 2024 10:45:31 -0700 Subject: [PATCH 030/373] table name is properly scaling --- .../HTMLGrid/tablesOverlay/TablesOverlay.tsx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx index 95bf88114a..87755e4f4e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx @@ -3,7 +3,7 @@ //! There are two overlays: the first is the active table that the sheet cursor //! is in. The second is the table that the mouse is hovering over. -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { events } from '@/app/events/events'; import { Rectangle } from 'pixi.js'; @@ -11,6 +11,9 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { pixiApp } from '../../pixiApp/PixiApp'; export const TableOverlay = () => { + const tableRef = useRef(null); + const hoverTableRef = useRef(null); + const [table, setTable] = useState(undefined); const [rect, setRect] = useState(undefined); const [name, setName] = useState(undefined); @@ -69,11 +72,13 @@ export const TableOverlay = () => { if (table && rect && name !== undefined) { return (
{name}
@@ -86,11 +91,13 @@ export const TableOverlay = () => { if (hoverTable && hoverRect && hoverName !== undefined) { return (
{hoverName}
@@ -99,7 +106,20 @@ export const TableOverlay = () => { } }, [hoverName, hoverRect, hoverTable]); - console.log(table, hoverTable); + useEffect(() => { + const updateViewport = () => { + if (table && tableRef.current) { + tableRef.current.style.transform = `translateY(-100%) scale(${1 / pixiApp.viewport.scale.x})`; + } + if (hoverTable && hoverTableRef.current) { + hoverTableRef.current.style.transform = `translateY(-100%) scale(${1 / pixiApp.viewport.scale.x})`; + } + }; + events.on('viewportChanged', updateViewport); + return () => { + events.off('viewportChanged', updateViewport); + }; + }, [table, tableRef, hoverTable, hoverTableRef]); if (!tableRender && !hoverTableRender) return null; From bda91ce2aec4a890cfbc7a0df2851b0158471b3a Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 15 Oct 2024 13:57:45 -0600 Subject: [PATCH 031/373] Clean up sorting, implement header toggle in rust and the client --- quadratic-client/src/app/actions/actions.ts | 6 +- .../src/app/actions/dataTableSpec.ts | 41 ++++++- .../app/gridGL/HTMLGrid/GridContextMenu.tsx | 5 +- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../quadraticCore/coreClientMessages.ts | 16 ++- .../quadraticCore/quadraticCore.ts | 17 ++- .../web-workers/quadraticCore/worker/core.ts | 5 + .../quadraticCore/worker/coreClient.ts | 6 +- .../active_transactions/transaction_name.rs | 3 + .../execution/control_transaction.rs | 4 +- .../execute_operation/execute_data_table.rs | 44 ++++++- .../execution/execute_operation/mod.rs | 5 + .../src/controller/operations/data_table.rs | 12 ++ .../src/controller/operations/import.rs | 3 +- .../src/controller/operations/operation.rs | 14 +++ .../src/controller/user_actions/data_table.rs | 14 +++ quadratic-core/src/grid/data_table.rs | 110 +++++++++++------- .../src/grid/file/serialize/data_table.rs | 2 + quadratic-core/src/grid/file/v1_7/file.rs | 1 + quadratic-core/src/grid/file/v1_8/schema.rs | 1 + quadratic-core/src/grid/sheet/data_table.rs | 2 +- .../wasm_bindings/controller/data_table.rs | 22 +++- 22 files changed, 274 insertions(+), 61 deletions(-) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 9744e2e3e2..887dff89c5 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -139,7 +139,11 @@ export enum Action { InsertRowBelow = 'insert_row_below', DeleteRow = 'delete_row', DeleteColumn = 'delete_column', + FlattenDataTable = 'flatten_data_table', GridToDataTable = 'grid_to_data_table', - SortDataTable = 'sort_data_table', + SortDataTableFirstColAsc = 'sort_data_table_first_col_asc', + SortDataTableFirstColDesc = 'sort_data_table_first_col_desc', + AddFirstRowAsHeaderDataTable = 'add_first_row_as_header_data_table', + RemoveFirstRowAsHeaderDataTable = 'remove_first_row_as_header_data_table', } diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 3ad2af607c..aea6640937 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -5,7 +5,15 @@ import { sheets } from '../grid/controller/Sheets'; import { ActionSpecRecord } from './actionsSpec'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -type DataTableSpec = Pick; +type DataTableSpec = Pick< + ActionSpecRecord, + | Action.FlattenDataTable + | Action.GridToDataTable + | Action.SortDataTableFirstColAsc + | Action.SortDataTableFirstColDesc + | Action.AddFirstRowAsHeaderDataTable + | Action.RemoveFirstRowAsHeaderDataTable +>; export type DataTableActionArgs = { [Action.FlattenDataTable]: { name: string }; @@ -33,8 +41,8 @@ export const dataTableSpec: DataTableSpec = { quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); }, }, - [Action.SortDataTable]: { - label: 'Sort Data Table', + [Action.SortDataTableFirstColAsc]: { + label: 'Sort Data Table - First Column Ascending', Icon: PersonAddIcon, isAvailable: () => isDataTable(), run: async () => { @@ -42,4 +50,31 @@ export const dataTableSpec: DataTableSpec = { quadraticCore.sortDataTable(sheets.sheet.id, x, y, 0, 'asc', sheets.getCursorPosition()); }, }, + [Action.SortDataTableFirstColDesc]: { + label: 'Sort Data Table - First Column Descending', + Icon: PersonAddIcon, + isAvailable: () => isDataTable(), + run: async () => { + const { x, y } = sheets.sheet.cursor.cursorPosition; + quadraticCore.sortDataTable(sheets.sheet.id, x, y, 0, 'desc', sheets.getCursorPosition()); + }, + }, + [Action.AddFirstRowAsHeaderDataTable]: { + label: 'Add First Row as Header', + Icon: PersonAddIcon, + isAvailable: () => isDataTable(), + run: async () => { + const { x, y } = sheets.sheet.cursor.cursorPosition; + quadraticCore.dataTableFirstRowAsHeader(sheets.sheet.id, x, y, true, sheets.getCursorPosition()); + }, + }, + [Action.RemoveFirstRowAsHeaderDataTable]: { + label: 'Remove First Row as Header', + Icon: PersonAddIcon, + isAvailable: () => isDataTable(), + run: async () => { + const { x, y } = sheets.sheet.cursor.cursorPosition; + quadraticCore.dataTableFirstRowAsHeader(sheets.sheet.id, x, y, false, sheets.getCursorPosition()); + }, + }, }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx index afd4e46364..3ef77cd784 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx @@ -82,7 +82,10 @@ export const GridContextMenu = () => { - + + + +
); diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index df17a15288..82873f3379 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -66,7 +66,7 @@ export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; export type TextMatch = { "Exactly": TextCase } | { "Contains": TextCase } | { "NotContains": TextCase } | { "TextLength": { min: number | null, max: number | null, } }; -export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "GridToDataTable" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; +export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "GridToDataTable" | "DataTableFirstRowAsHeader" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; export interface TransientResize { row: bigint | null, column: bigint | null, old_size: number, new_size: number, } export interface Validation { id: string, selection: Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } export interface ValidationDateTime { ignore_blank: boolean, require_date: boolean, require_time: boolean, prohibit_date: boolean, prohibit_time: boolean, ranges: Array, } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index 5d0c1d7d18..e3a887a263 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -1035,8 +1035,17 @@ export interface ClientCoreSortDataTable { sheetId: string; x: number; y: number; - column_index: number; - sort_order: string; + columnIndex: number; + sortOrder: string; + cursor: string; +} + +export interface ClientCoreDataTableFirstRowAsHeader { + type: 'clientCoreDataTableFirstRowAsHeader'; + sheetId: string; + x: number; + y: number; + firstRowAsHeader: boolean; cursor: string; } @@ -1122,7 +1131,8 @@ export type ClientCoreMessage = | ClientCoreInsertRow | ClientCoreFlattenDataTable | ClientCoreGridToDataTable - | ClientCoreSortDataTable; + | ClientCoreSortDataTable + | ClientCoreDataTableFirstRowAsHeader; export type CoreClientMessage = | CoreClientGetCodeCell diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index e037baae92..f886021c4a 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -1189,14 +1189,25 @@ class QuadraticCore { }); } - sortDataTable(sheetId: string, x: number, y: number, column_index: number, sort_order: string, cursor: string) { + sortDataTable(sheetId: string, x: number, y: number, columnIndex: number, sortOrder: string, cursor: string) { this.send({ type: 'clientCoreSortDataTable', sheetId, x, y, - column_index, - sort_order, + columnIndex, + sortOrder, + cursor, + }); + } + + dataTableFirstRowAsHeader(sheetId: string, x: number, y: number, firstRowAsHeader: boolean, cursor: string) { + this.send({ + type: 'clientCoreDataTableFirstRowAsHeader', + sheetId, + x, + y, + firstRowAsHeader, cursor, }); } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 51de947ae6..c41ce8545a 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1129,6 +1129,11 @@ class Core { if (!this.gridController) throw new Error('Expected gridController to be defined'); this.gridController.sortDataTable(sheetId, posToPos(x, y), column_index, sort_order, cursor); } + + dataTableFirstRowAsHeader(sheetId: string, x: number, y: number, firstRowAsHeader: boolean, cursor: string) { + if (!this.gridController) throw new Error('Expected gridController to be defined'); + this.gridController.dataTablefirstRowAsHeader(sheetId, posToPos(x, y), firstRowAsHeader, cursor); + } } export const core = new Core(); diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index 5a2c5b6625..1d43233c15 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -590,7 +590,11 @@ class CoreClient { return; case 'clientCoreSortDataTable': - core.sortDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.column_index, e.data.sort_order, e.data.cursor); + core.sortDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.columnIndex, e.data.sortOrder, e.data.cursor); + return; + + case 'clientCoreDataTableFirstRowAsHeader': + core.dataTableFirstRowAsHeader(e.data.sheetId, e.data.x, e.data.y, e.data.firstRowAsHeader, e.data.cursor); return; default: diff --git a/quadratic-core/src/controller/active_transactions/transaction_name.rs b/quadratic-core/src/controller/active_transactions/transaction_name.rs index 31cfc15ab1..41c422d5b8 100644 --- a/quadratic-core/src/controller/active_transactions/transaction_name.rs +++ b/quadratic-core/src/controller/active_transactions/transaction_name.rs @@ -13,11 +13,14 @@ pub enum TransactionName { SetDataTableAt, CutClipboard, PasteClipboard, + SetCode, RunCode, FlattenDataTable, GridToDataTable, + DataTableFirstRowAsHeader, Import, + SetSheetMetadata, SheetAdd, SheetDelete, diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index 7dc64e0187..79960156d5 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -298,9 +298,11 @@ impl GridController { } else { Value::Array(array.into()) }; + + let sheet = self.try_sheet_result(current_sheet_pos.sheet_id)?; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1", + &sheet.next_data_table_name(), value, false, false, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 61ddc19a20..820a0c1b13 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -291,9 +291,7 @@ impl GridController { } = op.to_owned() { let sheet_id = sheet_pos.sheet_id; - // let rect = Rect::from(sheet_rect); let sheet = self.try_sheet_mut_result(sheet_id)?; - // let sheet_pos = sheet_rect.min.to_sheet_pos(sheet_id); let sheet_rect = SheetRect::single_sheet_pos(sheet_pos); let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; let data_table = sheet.data_table_mut(data_table_pos)?; @@ -311,6 +309,7 @@ impl GridController { // TODO(ddimaria): remove this clone let forward_operations = vec![op.clone()]; + // TODO(ddimaria): this is a placeholder, actually implement let reverse_operations = vec![op.clone()]; self.data_table_operations( @@ -325,6 +324,45 @@ impl GridController { bail!("Expected Operation::SortDataTable in execute_sort_data_table"); } + + pub(super) fn execute_data_table_first_row_as_header( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::DataTableFirstRowAsHeader { + sheet_pos, + first_row_is_header, + } = op.to_owned() + { + let sheet_id = sheet_pos.sheet_id; + let sheet = self.try_sheet_mut_result(sheet_id)?; + let sheet_rect = SheetRect::single_sheet_pos(sheet_pos); + let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; + let data_table = sheet.data_table_mut(data_table_pos)?; + + data_table.toggle_first_row_as_header(first_row_is_header); + + self.send_to_wasm(transaction, &sheet_rect)?; + + // TODO(ddimaria): remove this clone + let forward_operations = vec![op.clone()]; + + // TODO(ddimaria): this is a placeholder, actually implement + let reverse_operations = vec![op.clone()]; + + self.data_table_operations( + transaction, + &sheet_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::DataTableFirstRowAsHeader in execute_data_table_first_row_as_header"); + } } #[cfg(test)] @@ -444,7 +482,7 @@ mod tests { fn test_execute_sort_data_table() { let (mut gc, sheet_id, pos, _) = simple_csv(); let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); - data_table.apply_header_from_first_row(); + data_table.apply_first_row_as_header(); print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 98f326d16d..50d683ce33 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -44,6 +44,11 @@ impl GridController { Operation::SortDataTable { .. } => Self::handle_execution_operation_result( self.execute_sort_data_table(transaction, op), ), + Operation::DataTableFirstRowAsHeader { .. } => { + Self::handle_execution_operation_result( + self.execute_data_table_first_row_as_header(transaction, op), + ) + } Operation::ComputeCode { .. } => self.execute_compute_code(transaction, op), Operation::SetCellFormats { .. } => self.execute_set_cell_formats(transaction, op), Operation::SetCellFormatsSelection { .. } => { diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index 3ba1aa1d03..6901b398c3 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -31,6 +31,18 @@ impl GridController { sort_order, }] } + + pub fn data_table_first_row_as_header_operations( + &self, + sheet_pos: SheetPos, + first_row_is_header: bool, + _cursor: Option, + ) -> Vec { + vec![Operation::DataTableFirstRowAsHeader { + sheet_pos, + first_row_is_header, + }] + } } #[cfg(test)] diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 910f36e5a9..d8cfa85df3 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -377,7 +377,8 @@ impl GridController { .try_sheet(sheet_id) .ok_or_else(|| anyhow!("Sheet {sheet_id} not found"))?; let sheet_pos = SheetPos::from((insert_at, sheet_id)); - let data_table = DataTable::from((import.to_owned(), cell_values, sheet)); + let mut data_table = DataTable::from((import.to_owned(), cell_values, sheet)); + data_table.has_header = true; // this operation must be before the SetCodeRun operations ops.push(Operation::SetCellValues { diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index a48e89c9bc..0cf495cfe9 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -56,6 +56,10 @@ pub enum Operation { column_index: u32, sort_order: String, }, + DataTableFirstRowAsHeader { + sheet_pos: SheetPos, + first_row_is_header: bool, + }, ComputeCode { sheet_pos: SheetPos, }, @@ -229,6 +233,16 @@ impl fmt::Display for Operation { sheet_pos, column_index, sort_order ) } + Operation::DataTableFirstRowAsHeader { + sheet_pos, + first_row_is_header, + } => { + write!( + fmt, + "DataTableFirstRowAsHeader {{ sheet_pos: {}, first_row_is_header {} }}", + sheet_pos, first_row_is_header + ) + } Operation::SetCellFormats { .. } => write!(fmt, "SetCellFormats {{ todo }}",), Operation::SetCellFormatsSelection { selection, formats } => { write!( diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index ff72663b40..c446da6e51 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -46,6 +46,20 @@ impl GridController { self.sort_data_table_operations(sheet_pos, column_index, sort_order, cursor.to_owned()); self.start_user_transaction(ops, cursor, TransactionName::GridToDataTable); } + + pub fn data_table_first_row_as_header( + &mut self, + sheet_pos: SheetPos, + first_row_is_header: bool, + cursor: Option, + ) { + let ops = self.data_table_first_row_as_header_operations( + sheet_pos, + first_row_is_header, + cursor.to_owned(), + ); + self.start_user_transaction(ops, cursor, TransactionName::DataTableFirstRowAsHeader); + } } #[cfg(test)] diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index cab5ea6751..7d727d369a 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -13,6 +13,10 @@ use anyhow::{anyhow, Ok, Result}; use chrono::{DateTime, Utc}; use itertools::Itertools; use serde::{Deserialize, Serialize}; +use tabled::{ + builder::Builder, + settings::{Color, Modify, Style}, +}; use super::Sheet; @@ -55,6 +59,7 @@ pub struct DataTableSortOrder { pub struct DataTable { pub kind: DataTableKind, pub name: String, + pub has_header: bool, pub columns: Option>, pub sort: Option>, pub display_buffer: Option>, @@ -87,16 +92,17 @@ impl DataTable { name: &str, value: Value, spill_error: bool, - header: bool, + has_header: bool, ) -> Self { let readonly = match kind { DataTableKind::CodeRun(_) => true, DataTableKind::Import(_) => false, }; - let mut data_table = DataTable { + let data_table = DataTable { kind, name: name.into(), + has_header, columns: None, sort: None, display_buffer: None, @@ -106,9 +112,9 @@ impl DataTable { last_modified: Utc::now(), }; - if header { - data_table.apply_header_from_first_row(); - } + // if has_header { + // data_table.apply_header_from_first_row(); + // } data_table } @@ -117,6 +123,7 @@ impl DataTable { pub fn new_raw( kind: DataTableKind, name: &str, + has_header: bool, columns: Option>, sort: Option>, display_buffer: Option>, @@ -127,6 +134,7 @@ impl DataTable { DataTable { kind, name: name.into(), + has_header, columns, sort, display_buffer, @@ -144,10 +152,10 @@ impl DataTable { } /// Takes the first row of the array and sets it as the column headings. - /// The source array is shifted up one in place. - pub fn apply_header_from_first_row(&mut self) { + pub fn apply_first_row_as_header(&mut self) { self.columns = match self.value { - Value::Array(ref mut array) => array.shift().ok().map(|array| { + // Value::Array(ref mut array) => array.shift().ok().map(|array| { + Value::Array(ref mut array) => array.get_row(0).ok().map(|array| { array .iter() .enumerate() @@ -158,6 +166,15 @@ impl DataTable { }; } + pub fn toggle_first_row_as_header(&mut self, first_row_as_header: bool) { + self.has_header = first_row_as_header; + + match first_row_as_header { + true => self.apply_first_row_as_header(), + false => self.columns = None, + } + } + /// Apply default column headings to the DataTable. /// For example, the column headings will be "Column 1", "Column 2", etc. pub fn apply_default_header(&mut self) { @@ -227,17 +244,23 @@ impl DataTable { pub fn sort(&mut self, column_index: usize, direction: SortDirection) -> Result<()> { let values = self.value.clone().into_array()?; + let increment = |i| if self.has_header { i + 1 } else { i }; - let display_buffer = values + let mut display_buffer = values .col(column_index) + .skip(increment(0)) .enumerate() .sorted_by(|a, b| match direction { SortDirection::Ascending => a.1.total_cmp(b.1), SortDirection::Descending => b.1.total_cmp(a.1), }) - .map(|(i, _)| i as u64) + .map(|(i, _)| increment(i) as u64) .collect::>(); + if self.has_header { + display_buffer.insert(0, 0); + } + self.display_buffer = Some(display_buffer); Ok(()) @@ -416,6 +439,39 @@ impl DataTable { Rect::from_pos_and_size(pos, self.output_size()) } } + + pub fn pretty_print_data_table( + data_table: &DataTable, + title: Option<&str>, + max: Option, + ) -> String { + let mut builder = Builder::default(); + let array = data_table.display_value().unwrap().into_array().unwrap(); + let max = max.unwrap_or(array.height() as usize); + let title = title.unwrap_or("Data Table"); + + if let Some(columns) = data_table.columns.as_ref() { + let columns = columns.iter().map(|c| c.name.clone()).collect::>(); + builder.set_header(columns); + } + + for row in array.rows().take(max) { + let row = row.iter().map(|s| s.to_string()).collect::>(); + builder.push_record(row); + } + + let mut table = builder.build(); + table.with(Style::modern()); + + // bold the headers if they exist + if let Some(columns) = data_table.columns.as_ref() { + columns.iter().enumerate().for_each(|(index, _)| { + table.with(Modify::new((0, index)).with(Color::BOLD)); + }); + } + + format!("\nData Table: {title}\n{table}") + } } #[cfg(test)] @@ -425,10 +481,6 @@ pub mod test { use super::*; use crate::{controller::GridController, grid::SheetId, Array}; use serial_test::parallel; - use tabled::{ - builder::Builder, - settings::{Color, Modify, Style}, - }; pub fn test_csv_values() -> Vec> { vec![ @@ -457,32 +509,8 @@ pub mod test { title: Option<&str>, max: Option, ) { - let mut builder = Builder::default(); - let array = data_table.display_value().unwrap().into_array().unwrap(); - let max = max.unwrap_or(array.height() as usize); - let title = title.unwrap_or("Data Table"); - - if let Some(columns) = data_table.columns.as_ref() { - let columns = columns.iter().map(|c| c.name.clone()).collect::>(); - builder.set_header(columns); - } - - for row in array.rows().take(max) { - let row = row.iter().map(|s| s.to_string()).collect::>(); - builder.push_record(row); - } - - let mut table = builder.build(); - table.with(Style::modern()); - - // bold the headers if they exist - if let Some(columns) = data_table.columns.as_ref() { - columns.iter().enumerate().for_each(|(index, _)| { - table.with(Modify::new((0, index)).with(Color::BOLD)); - }); - } - - println!("\nData Table: {title}\n{table}"); + let data_table = super::DataTable::pretty_print_data_table(data_table, title, max); + println!("{}", data_table); } /// Assert a data table row matches the expected values @@ -560,7 +588,7 @@ pub mod test { #[parallel] fn test_data_table_sort() { let (_, mut data_table) = new_data_table(); - data_table.apply_header_from_first_row(); + data_table.apply_first_row_as_header(); let mut values = test_csv_values(); values.remove(0); // remove header row diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index dca4d3aa1f..881dce2740 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -164,6 +164,7 @@ pub(crate) fn import_data_table_builder( } }, name: data_table.name, + has_header: data_table.has_header, readonly: data_table.readonly, last_modified: data_table.last_modified.unwrap_or(Utc::now()), // this is required but fall back to now if failed spill_error: data_table.spill_error, @@ -379,6 +380,7 @@ pub(crate) fn export_data_tables( let data_table = current::DataTableSchema { kind, name: data_table.name, + has_header: data_table.has_header, columns, sort, display_buffer: data_table.display_buffer, diff --git a/quadratic-core/src/grid/file/v1_7/file.rs b/quadratic-core/src/grid/file/v1_7/file.rs index b4a0e0ff26..a549ad29d4 100644 --- a/quadratic-core/src/grid/file/v1_7/file.rs +++ b/quadratic-core/src/grid/file/v1_7/file.rs @@ -48,6 +48,7 @@ fn upgrade_code_runs( let new_data_table = v1_8::DataTableSchema { kind: v1_8::DataTableKindSchema::CodeRun(new_code_run), name: format!("Table {}", i), + has_header: false, columns: None, sort: None, display_buffer: None, diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index 2545b72360..c16e1d7e5d 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -122,6 +122,7 @@ pub struct DataTableSortOrderSchema { pub struct DataTableSchema { pub kind: DataTableKindSchema, pub name: String, + pub has_header: bool, pub columns: Option>, pub sort: Option>, pub display_buffer: Option>, diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index 3e8708d12c..f74c63a5c2 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -21,7 +21,7 @@ impl Sheet { self.set_data_table(pos, Some(data_table)) } - /// Sets or deletes a code run. + /// Sets or deletes a data table. /// /// Returns the old value if it was set. pub fn set_data_table(&mut self, pos: Pos, data_table: Option) -> Option { diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index 15ea6df4a9..007192cb67 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -33,7 +33,7 @@ impl GridController { Ok(()) } - /// Flattens a Data Table + /// Sort a Data Table #[wasm_bindgen(js_name = "sortDataTable")] pub fn js_sort_data_table( &mut self, @@ -49,4 +49,24 @@ impl GridController { Ok(()) } + + /// Toggle applin the first row as head + #[wasm_bindgen(js_name = "dataTablefirstRowAsHeader")] + pub fn js_data_table_first_row_as_header( + &mut self, + sheet_id: String, + pos: String, + first_row_is_header: bool, + cursor: Option, + ) -> Result<(), JsValue> { + let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; + let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; + self.data_table_first_row_as_header( + pos.to_sheet_pos(sheet_id), + first_row_is_header, + cursor, + ); + + Ok(()) + } } From eee6f6eb9f00f8a8592945a79b07c8a683ad232e Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 15 Oct 2024 14:05:58 -0700 Subject: [PATCH 032/373] headings stays on top --- .vscode/settings.json | 7 ---- .../HTMLGrid/tablesOverlay/TablesOverlay.tsx | 34 +++++++++++++------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 86d98756ed..8532f157ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -55,13 +55,6 @@ "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, - "rust-analyzer.linkedProjects": [ - "./Cargo.toml", - "./quadratic-files/Cargo.toml", - "./quadratic-core/Cargo.toml", - "./quadratic-multiplayer/Cargo.toml", - "./quadratic-rust-client/Cargo.toml" - ], "rust-analyzer.checkOnSave": true, "rust-analyzer.cargo.unsetTest": true, // "rust-analyzer.checkOnSave.command": "clippy", diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx index 87755e4f4e..86fc92f583 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx @@ -1,13 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ //! This draws the table heading and provides its context menu. //! //! There are two overlays: the first is the active table that the sheet cursor //! is in. The second is the table that the mouse is hovering over. -import { useEffect, useMemo, useRef, useState } from 'react'; -import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { events } from '@/app/events/events'; -import { Rectangle } from 'pixi.js'; import { sheets } from '@/app/grid/controller/Sheets'; +import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { Rectangle } from 'pixi.js'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { pixiApp } from '../../pixiApp/PixiApp'; export const TableOverlay = () => { @@ -38,13 +39,26 @@ export const TableOverlay = () => { }; }, []); - // const [viewableRect, setViewableRect] = useState(undefined); - // useEffect(() => { - // const checkViewport = () => { - - // } - // events.on('') - // }, [rect]); + useEffect(() => { + let tableTop = rect ? rect.y : 0; + const checkViewport = () => { + if (!rect || !tableRef.current) { + return; + } + const viewport = pixiApp.viewport; + const headingHeight = pixiApp.headings.headingSize.height / pixiApp.viewport.scale.y; + if (rect.y < viewport.top + headingHeight) { + tableTop = rect.y + (viewport.top + headingHeight - rect.y); + tableRef.current.style.top = `${tableTop}px`; + } else { + tableRef.current.style.top = `${rect.y}px`; + } + }; + events.on('viewportChanged', checkViewport); + return () => { + events.off('viewportChanged', checkViewport); + }; + }, [rect]); const [hoverTable, setHoverTable] = useState(undefined); const [hoverRect, setHoverRect] = useState(undefined); From 50ae5bfb9560dc2eb17a6eed59754ea2c566b550 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 15 Oct 2024 17:20:14 -0600 Subject: [PATCH 033/373] Fix all the broken things --- .vscode/settings.json | 7 --- quadratic-api/src/data/current_blank.grid | Bin 193 -> 261 bytes .../execute_operation/execute_data_table.rs | 9 ++-- .../src/controller/operations/import.rs | 38 +++++++++----- .../src/controller/user_actions/import.rs | 2 +- quadratic-core/src/grid/data_table.rs | 48 +++++++++--------- .../src/grid/file/serialize/data_table.rs | 2 + quadratic-core/src/test_util.rs | 1 + .../data/grid/current_blank.grid | Bin 193 -> 261 bytes quadratic-rust-shared/src/auto_gen_path.rs | 4 +- 10 files changed, 58 insertions(+), 53 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 86d98756ed..8532f157ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -55,13 +55,6 @@ "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, - "rust-analyzer.linkedProjects": [ - "./Cargo.toml", - "./quadratic-files/Cargo.toml", - "./quadratic-core/Cargo.toml", - "./quadratic-multiplayer/Cargo.toml", - "./quadratic-rust-client/Cargo.toml" - ], "rust-analyzer.checkOnSave": true, "rust-analyzer.cargo.unsetTest": true, // "rust-analyzer.checkOnSave.command": "clippy", diff --git a/quadratic-api/src/data/current_blank.grid b/quadratic-api/src/data/current_blank.grid index 739e23d05cbad953003f4b2a325c7a189104e66f..3e81b823b6a06e05d63fcc50d19d361d0e863f97 100644 GIT binary patch literal 261 zcmV+g0s8&}000000000nE;uT90aZ}jj)Nc&{gur;>&8pBYyFLVG{&GnOM=b@uuV$* z_YNpc`a;f`%Q=VfU~fV=3vK5?acK7!YT5ALsXXU(QHlK2oT~iNR6MWik`?FX!X^8G zErW&40$TVBB3~tBFrh{R?Xqf}0U9od04u12Qrg|A>$9%!L#EuG6Q+n46e~ zXzsz`f66eTb2ulag>(rUb-f*gMBW)ak&uXPOK4w+wS7FmC?7x)_uID7P42>23QY$t LzZ=+Ij46)=J_UP& literal 193 zcmV;y06zZ%000000000nE;cH70X>dU3WG2ZM6XizTZ>u^>NWZqf<%*0A!Z>lEmXXF zS0(*fc6Nt1vq5-iy$0y{2D5_xnqVsVkW5uMV?~+Ql6868vNkU})v%^6THTGXXW- return Err(error(format!("line {}: {}", y + 1, e))), Ok(record) => { @@ -597,22 +606,23 @@ mod test { .unwrap(); let sheet = gc.sheet(sheet_id); + let data_table = sheet.data_table(pos).unwrap(); // date assert_eq!( - sheet.data_table(pos).unwrap().cell_value_at(0, 1), + data_table.cell_value_at(0, 1), Some(CellValue::Date( NaiveDate::parse_from_str("2024-12-21", "%Y-%m-%d").unwrap() )) ); assert_eq!( - sheet.data_table(pos).unwrap().cell_value_at(0, 2), + data_table.cell_value_at(0, 2), Some(CellValue::Date( NaiveDate::parse_from_str("2024-12-22", "%Y-%m-%d").unwrap() )) ); assert_eq!( - sheet.data_table(pos).unwrap().cell_value_at(0, 3), + data_table.cell_value_at(0, 3), Some(CellValue::Date( NaiveDate::parse_from_str("2024-12-23", "%Y-%m-%d").unwrap() )) @@ -620,19 +630,19 @@ mod test { // time assert_eq!( - sheet.cell_value((1, 1).into()), + data_table.cell_value_at(1, 1), Some(CellValue::Time( NaiveTime::parse_from_str("13:23:00", "%H:%M:%S").unwrap() )) ); assert_eq!( - sheet.cell_value((1, 2).into()), + data_table.cell_value_at(1, 2), Some(CellValue::Time( NaiveTime::parse_from_str("14:45:00", "%H:%M:%S").unwrap() )) ); assert_eq!( - sheet.cell_value((1, 3).into()), + data_table.cell_value_at(1, 3), Some(CellValue::Time( NaiveTime::parse_from_str("16:30:00", "%H:%M:%S").unwrap() )) @@ -640,7 +650,7 @@ mod test { // date time assert_eq!( - sheet.cell_value((2, 1).into()), + data_table.cell_value_at(2, 1), Some(CellValue::DateTime( NaiveDate::from_ymd_opt(2024, 12, 21) .unwrap() @@ -649,7 +659,7 @@ mod test { )) ); assert_eq!( - sheet.cell_value((2, 2).into()), + data_table.cell_value_at(2, 2), Some(CellValue::DateTime( NaiveDate::from_ymd_opt(2024, 12, 22) .unwrap() @@ -658,7 +668,7 @@ mod test { )) ); assert_eq!( - sheet.cell_value((2, 3).into()), + data_table.cell_value_at(2, 3), Some(CellValue::DateTime( NaiveDate::from_ymd_opt(2024, 12, 23) .unwrap() diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index 7d50303cd3..7861bca7e6 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -461,7 +461,7 @@ pub(crate) mod tests { #[test] #[parallel] - fn should_import_with_title_header() { + fn should_import_with_title_header_only() { let file_name = "title_row.csv"; let csv_file = read_test_csv_file(file_name); let mut gc = GridController::test(); diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 7d727d369a..2c3ffa810d 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -153,6 +153,8 @@ impl DataTable { /// Takes the first row of the array and sets it as the column headings. pub fn apply_first_row_as_header(&mut self) { + self.has_header = true; + self.columns = match self.value { // Value::Array(ref mut array) => array.shift().ok().map(|array| { Value::Array(ref mut array) => array.get_row(0).ok().map(|array| { @@ -450,24 +452,27 @@ impl DataTable { let max = max.unwrap_or(array.height() as usize); let title = title.unwrap_or("Data Table"); - if let Some(columns) = data_table.columns.as_ref() { - let columns = columns.iter().map(|c| c.name.clone()).collect::>(); - builder.set_header(columns); - } - - for row in array.rows().take(max) { + for (index, row) in array.rows().take(max).enumerate() { let row = row.iter().map(|s| s.to_string()).collect::>(); - builder.push_record(row); + + if index == 0 && data_table.has_header { + builder.set_header(row); + } else { + builder.push_record(row); + } } let mut table = builder.build(); table.with(Style::modern()); // bold the headers if they exist - if let Some(columns) = data_table.columns.as_ref() { - columns.iter().enumerate().for_each(|(index, _)| { - table.with(Modify::new((0, index)).with(Color::BOLD)); - }); + if data_table.has_header { + [0..table.count_columns()] + .iter() + .enumerate() + .for_each(|(index, _)| { + table.with(Modify::new((0, index)).with(Color::BOLD)); + }); } format!("\nData Table: {title}\n{table}") @@ -556,10 +561,7 @@ pub mod test { let mut data_table = DataTable::new(kind.clone(), "Table 1", value, false, true) .with_last_modified(data_table.last_modified); - // array height should be 3 since we lift the first row as column headings - let expected_array_size = ArraySize::new(4, 3).unwrap(); - assert_eq!(data_table.output_size(), expected_array_size); - + data_table.apply_first_row_as_header(); let expected_columns = vec![ DataTableColumn::new("city".into(), true, 0), DataTableColumn::new("region".into(), true, 1), @@ -568,8 +570,7 @@ pub mod test { ]; assert_eq!(data_table.columns, Some(expected_columns)); - let mut expected_values = values.clone(); - expected_values.shift().unwrap(); + let expected_values = values.clone(); assert_eq!( data_table.value.clone().into_array().unwrap(), expected_values @@ -590,23 +591,22 @@ pub mod test { let (_, mut data_table) = new_data_table(); data_table.apply_first_row_as_header(); - let mut values = test_csv_values(); - values.remove(0); // remove header row + let values = test_csv_values(); pretty_print_data_table(&data_table, Some("Original Data Table"), None); // sort by population city ascending data_table.sort(0, SortDirection::Ascending).unwrap(); pretty_print_data_table(&data_table, Some("Sorted by City"), None); - assert_data_table_row(&data_table, 0, values[1].clone()); assert_data_table_row(&data_table, 1, values[2].clone()); - assert_data_table_row(&data_table, 2, values[0].clone()); + assert_data_table_row(&data_table, 2, values[3].clone()); + assert_data_table_row(&data_table, 3, values[1].clone()); // sort by population descending data_table.sort(3, SortDirection::Descending).unwrap(); pretty_print_data_table(&data_table, Some("Sorted by Population Descending"), None); - assert_data_table_row(&data_table, 0, values[1].clone()); - assert_data_table_row(&data_table, 1, values[0].clone()); - assert_data_table_row(&data_table, 2, values[2].clone()); + assert_data_table_row(&data_table, 1, values[2].clone()); + assert_data_table_row(&data_table, 2, values[1].clone()); + assert_data_table_row(&data_table, 3, values[3].clone()); } #[test] diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 881dce2740..95a3adb67d 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -136,6 +136,7 @@ pub(crate) fn import_data_table_builder( let mut new_data_tables = IndexMap::new(); for (pos, data_table) in data_tables.into_iter() { + dbgjs!(format!("data_table: {:?}", &data_table)); let value = match data_table.value { current::OutputValueSchema::Single(value) => { Value::Single(import_cell_value(value.to_owned())) @@ -319,6 +320,7 @@ pub(crate) fn export_data_tables( data_tables .into_iter() .map(|(pos, data_table)| { + dbgjs!(format!("data_table: {:?}", &data_table)); let value = match data_table.value { Value::Single(cell_value) => { current::OutputValueSchema::Single(export_cell_value(cell_value)) diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index bbe0bedd70..6a10040a47 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -226,6 +226,7 @@ pub fn print_table(grid_controller: &GridController, sheet_id: SheetId, rect: Re pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rect: Rect) { if let Some(sheet) = grid_controller.try_sheet(sheet_id) { let data_table = sheet.data_table(rect.min).unwrap(); + println!("Data table: {:?}", data_table); let max = rect.max.y - rect.min.y; crate::grid::data_table::test::pretty_print_data_table( data_table, diff --git a/quadratic-rust-shared/data/grid/current_blank.grid b/quadratic-rust-shared/data/grid/current_blank.grid index 739e23d05cbad953003f4b2a325c7a189104e66f..3e81b823b6a06e05d63fcc50d19d361d0e863f97 100644 GIT binary patch literal 261 zcmV+g0s8&}000000000nE;uT90aZ}jj)Nc&{gur;>&8pBYyFLVG{&GnOM=b@uuV$* z_YNpc`a;f`%Q=VfU~fV=3vK5?acK7!YT5ALsXXU(QHlK2oT~iNR6MWik`?FX!X^8G zErW&40$TVBB3~tBFrh{R?Xqf}0U9od04u12Qrg|A>$9%!L#EuG6Q+n46e~ zXzsz`f66eTb2ulag>(rUb-f*gMBW)ak&uXPOK4w+wS7FmC?7x)_uID7P42>23QY$t LzZ=+Ij46)=J_UP& literal 193 zcmV;y06zZ%000000000nE;cH70X>dU3WG2ZM6XizTZ>u^>NWZqf<%*0A!Z>lEmXXF zS0(*fc6Nt1vq5-iy$0y{2D5_xnqVsVkW5uMV?~+Ql6868vNkU})v%^6THTGXXW- Date: Wed, 16 Oct 2024 05:31:00 -0700 Subject: [PATCH 034/373] started work on Tables pixi UI --- .../src/app/gridGL/cells/CellsSheet.ts | 9 +- .../src/app/gridGL/cells/tables/Tables.ts | 91 +++++++++++++++++++ .../src/app/quadratic-core-types/index.d.ts | 5 +- quadratic-client/src/app/theme/colors.ts | 6 +- .../worker/cellsLabel/CellLabel.ts | 2 + quadratic-core/src/bin/export_types.rs | 4 +- quadratic-core/src/grid/data_table.rs | 17 +++- quadratic-core/src/grid/js_types.rs | 9 +- quadratic-core/src/grid/sheet/rendering.rs | 52 +++++++++-- quadratic-rust-shared/src/auto_gen_path.rs | 4 +- 10 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/cells/tables/Tables.ts diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts index d839c00aa7..de90988122 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts @@ -1,10 +1,11 @@ import { events } from '@/app/events/events'; +import { Tables } from '@/app/gridGL/cells/tables/Tables'; import { JsValidationWarning } from '@/app/quadratic-core-types'; import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; import { Container, Rectangle, Sprite } from 'pixi.js'; import { pixiApp } from '../pixiApp/PixiApp'; -import { CellsArray } from './CellsArray'; import { Borders } from './borders/Borders'; +import { CellsArray } from './CellsArray'; import { CellsFills } from './CellsFills'; import { CellsImage } from './cellsImages/CellsImage'; import { CellsImages } from './cellsImages/CellsImages'; @@ -33,6 +34,8 @@ export class CellsSheet extends Container { cellsMarkers: CellsMarkers; cellsLabels: CellsLabels; + tables: Tables; + sheetId: string; constructor(sheetId: string) { @@ -44,7 +47,11 @@ export class CellsSheet extends Container { this.addChild(new CellsSearch(sheetId)); this.cellsLabels = this.addChild(new CellsLabels(this)); + this.tables = this.addChild(new Tables(this)); + + // todo: this should go away... this.cellsArray = this.addChild(new CellsArray(this)); + this.borders = this.addChild(new Borders(this)); this.cellsMarkers = this.addChild(new CellsMarkers()); this.cellsImages = new CellsImages(this); diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts new file mode 100644 index 0000000000..51a1851fc6 --- /dev/null +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -0,0 +1,91 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { Sheet } from '@/app/grid/sheet/Sheet'; +import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; +import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { colors } from '@/app/theme/colors'; +import { FONT_SIZE, OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; +import { BitmapText, Container, Graphics, Rectangle } from 'pixi.js'; + +interface Column { + heading: Container; + bounds: Rectangle; +} + +interface TableHeading { + container: Container; + headingBounds: Rectangle; + originalHeadingBounds: Rectangle; + columns: Column[]; + codeCell: JsRenderCodeCell; +} + +export class Tables extends Container { + private cellsSheet: CellsSheet; + + constructor(cellsSheet: CellsSheet) { + super(); + this.cellsSheet = cellsSheet; + events.on('renderCodeCells', this.renderCodeCells); + + // todo: update code cells? + } + + get sheet(): Sheet { + const sheet = sheets.getById(this.cellsSheet.sheetId); + if (!sheet) { + throw new Error('Sheet not found in Tables'); + } + return sheet; + } + + cull() {} + + private renderHeadings = (codeCell: JsRenderCodeCell): TableHeading => { + const container = this.addChild(new Container()); + console.log(codeCell.x); + const headingBounds = this.sheet.getScreenRectangle(codeCell.x, codeCell.y, codeCell.column_names.length - 1, 0); + const originalHeadingBounds = headingBounds.clone(); + container.position.set(headingBounds.x, headingBounds.y); + + // draw heading background + const background = container.addChild(new Graphics()); + background.beginFill(colors.tableHeadingBackground); + background.drawShape(new Rectangle(0, 0, headingBounds.width, headingBounds.height)); + background.endFill(); + + // draw individual headings + let x = 0; + const columns: Column[] = codeCell.column_names.map((column, index) => { + const width = this.sheet.offsets.getColumnWidth(codeCell.x + index); + const bounds = new Rectangle(x, headingBounds.y, width, headingBounds.height); + const heading = container.addChild(new Container()); + heading.position.set(x + OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); + heading.addChild( + new BitmapText(column.name, { + fontName: 'OpenSans-Bold', + fontSize: FONT_SIZE, + tint: colors.tableHeadingForeground, + }) + ); + x += width; + return { heading, bounds }; + }); + + return { + container, + headingBounds, + originalHeadingBounds, + columns, + codeCell, + }; + }; + + private renderCodeCells = (sheetId: string, codeCells: JsRenderCodeCell[]) => { + if (sheetId === this.cellsSheet.sheetId) { + this.removeChildren(); + codeCells.forEach((codeCell) => this.renderHeadings(codeCell)); + } + }; +} diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 8821e1cdc2..a5a6a8d216 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -17,6 +17,7 @@ export type CellWrap = "overflow" | "wrap" | "clip"; export type CodeCellLanguage = "Python" | "Formula" | { "Connection": { kind: ConnectionKind, id: string, } } | "Javascript" | "Import"; export interface ColumnRow { column: number, row: number, } export type ConnectionKind = "POSTGRES" | "MYSQL" | "MSSQL" | "SNOWFLAKE"; +export interface DataTableColumn { name: string, display: boolean, value_index: number, } export type DateTimeRange = { "DateRange": [bigint | null, bigint | null] } | { "DateEqual": Array } | { "DateNotEqual": Array } | { "TimeRange": [number | null, number | null] } | { "TimeEqual": Array } | { "TimeNotEqual": Array }; export interface Duration { months: number, seconds: number, } export interface Format { align: CellAlign | null, vertical_align: CellVerticalAlign | null, wrap: CellWrap | null, numeric_format: NumericFormat | null, numeric_decimals: number | null, numeric_commas: boolean | null, bold: boolean | null, italic: boolean | null, text_color: string | null, fill_color: string | null, render_size: RenderSize | null, date_time: string | null, underline: boolean | null, strike_through: boolean | null, } @@ -35,8 +36,8 @@ export interface JsNumber { decimals: number | null, commas: boolean | null, for export interface JsOffset { column: number | null, row: number | null, size: number, } export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } -export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, } +export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableHeading"; +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index 73810f90cc..71408d3b08 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -11,9 +11,9 @@ export const colors = { cellColorUserFormula: 0x8c1a6a, cellColorUserJavascript: 0xca8a04, cellColorUserAI: 0x1a8c5d, + cellColorError: 0xf25f5c, - // todo: these are new and should be reviewed cellColorWarning: 0xffb74d, cellColorInfo: 0x4fc3f7, @@ -25,6 +25,10 @@ export const colors = { gridBackground: 0xffffff, + // table headings + tableHeadingForeground: 0, + tableHeadingBackground: 0xe3eafc, + independence: 0x5d576b, headerBackgroundColor: 0xffffff, headerSelectedBackgroundColor: 0x2463eb, diff --git a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts index e1f947c048..90a2aa7c29 100644 --- a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts +++ b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts @@ -165,6 +165,8 @@ export class CellLabel { this.tint = convertColorStringToTint(cell.textColor); } else if (this.link) { this.tint = convertColorStringToTint(colors.link); + } else if (cell.special === 'TableHeading') { + this.tint = colors.tableHeadingForeground; } else { this.tint = 0; } diff --git a/quadratic-core/src/bin/export_types.rs b/quadratic-core/src/bin/export_types.rs index 6900a4f538..d40a07e836 100644 --- a/quadratic-core/src/bin/export_types.rs +++ b/quadratic-core/src/bin/export_types.rs @@ -28,7 +28,8 @@ use grid::sheet::validations::validation_rules::validation_text::{ }; use grid::sheet::validations::validation_rules::ValidationRule; use grid::{ - CellAlign, CellVerticalAlign, CellWrap, GridBounds, NumericFormat, NumericFormatKind, SheetId, + CellAlign, CellVerticalAlign, CellWrap, DataTableColumn, GridBounds, NumericFormat, + NumericFormatKind, SheetId, }; use quadratic_core::color::Rgba; use quadratic_core::controller::active_transactions::transaction_name::TransactionName; @@ -82,6 +83,7 @@ fn main() { CodeCellLanguage, ColumnRow, ConnectionKind, + DataTableColumn, DateTimeRange, Duration, Format, diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 2c3ffa810d..a0713ed53f 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -17,10 +17,11 @@ use tabled::{ builder::Builder, settings::{Color, Modify, Style}, }; +use ts_rs::TS; use super::Sheet; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub struct DataTableColumn { pub name: String, pub display: bool, @@ -477,6 +478,20 @@ impl DataTable { format!("\nData Table: {title}\n{table}") } + + /// Prepares the columns to be sent to the client. If no columns are set, it + /// will create default columns. + pub fn send_columns(&self) -> Vec { + match self.columns.as_ref() { + Some(columns) => columns.clone(), + None => { + let size = self.output_size(); + (0..size.w.get()) + .map(|i| DataTableColumn::new(format!("Column {}", i + 1), true, i)) + .collect::>() + } + } + } } #[cfg(test)] diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index a65dad0f67..a2a20ee2f3 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -7,11 +7,10 @@ use uuid::Uuid; use super::formats::format::Format; use super::formatting::{CellAlign, CellVerticalAlign, CellWrap}; use super::sheet::validations::validation::ValidationStyle; -use super::{CodeCellLanguage, NumericFormat}; +use super::{CodeCellLanguage, DataTableColumn, NumericFormat}; use crate::{Pos, SheetRect}; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)] pub enum JsRenderCellSpecial { Chart, SpillError, @@ -19,9 +18,10 @@ pub enum JsRenderCellSpecial { Logical, Checkbox, List, + TableHeading, } -#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ts_rs::TS)] +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)] pub struct JsNumber { pub decimals: Option, pub commas: Option, @@ -193,6 +193,7 @@ pub struct JsRenderCodeCell { pub state: JsRenderCodeCellState, pub spill_error: Option>, pub name: String, + pub column_names: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 893f4313b7..f666c660b8 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -27,6 +27,7 @@ impl Sheet { column: Option<&Column>, value: &CellValue, language: Option, + special: Option, ) -> JsRenderCell { if let CellValue::Html(_) = value { return JsRenderCell { @@ -68,12 +69,16 @@ impl Sheet { } else { None }; - let special = self.validations.render_special_pos(Pos { x, y }).or({ - if matches!(value, CellValue::Logical(_)) { - Some(JsRenderCellSpecial::Logical) - } else { - None - } + let special = special.or_else(|| { + self.validations + .render_special_pos(Pos { x, y }) + .or_else(|| { + if matches!(value, CellValue::Logical(_)) { + Some(JsRenderCellSpecial::Logical) + } else { + None + } + }) }); match column { @@ -171,6 +176,7 @@ impl Sheet { msg: RunErrorMsg::Spill, })), Some(code_cell_value.language), + None, )); } else if let Some(error) = data_table.get_error() { cells.push(self.get_render_cell( @@ -179,6 +185,7 @@ impl Sheet { None, &CellValue::Error(Box::new(error)), Some(code_cell_value.language), + None, )); } else { // find overlap of code_rect into rect @@ -205,6 +212,11 @@ impl Sheet { for x in x_start..=x_end { let column = self.get_column(x); for y in y_start..=y_end { + // We skip rendering the heading row because we render it separately. + // todo: we should not skip if headings are hidden + if y == code_rect.min.y { + continue; + } let value = data_table.cell_value_at( (x - code_rect.min.x) as u32, (y - code_rect.min.y) as u32, @@ -215,7 +227,14 @@ impl Sheet { } else { None }; - cells.push(self.get_render_cell(x, y, column, &value, language)); + let special = if y == code_rect.min.y { + Some(JsRenderCellSpecial::TableHeading) + } else { + None + }; + cells.push( + self.get_render_cell(x, y, column, &value, language, special), + ); } } } @@ -239,7 +258,14 @@ impl Sheet { if !matches!(value, CellValue::Code(_)) && !matches!(value, CellValue::Import(_)) { - render_cells.push(self.get_render_cell(x, *y, Some(column), value, None)); + render_cells.push(self.get_render_cell( + x, + *y, + Some(column), + value, + None, + None, + )); } }); }); @@ -417,6 +443,7 @@ impl Sheet { state, spill_error, name: data_table.name.clone(), + column_names: data_table.send_columns(), }) } @@ -460,6 +487,7 @@ impl Sheet { state, spill_error, name: data_table.name.clone(), + column_names: data_table.send_columns(), }) } _ => None, // this should not happen. A CodeRun should always have a CellValue::Code. @@ -618,7 +646,8 @@ mod tests { validation::{Validation, ValidationStyle}, validation_rules::{validation_logical::ValidationLogical, ValidationRule}, }, - Bold, CellVerticalAlign, CellWrap, CodeRun, DataTableKind, Italic, RenderSize, + Bold, CellVerticalAlign, CellWrap, CodeRun, DataTableColumn, DataTableKind, Italic, + RenderSize, }, selection::Selection, wasm_bindings::js::{clear_js_calls, expect_js_call, expect_js_call_count, hash_test}, @@ -1082,6 +1111,11 @@ mod tests { state: crate::grid::js_types::JsRenderCodeCellState::Success, spill_error: None, name: "Table 1".to_string(), + column_names: vec![DataTableColumn { + name: "Column 1".to_string(), + display: true, + value_index: 0, + }], }) ); } diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index a0dea21ae2..8df3d2b4d5 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From 51cc5a4ea01bf53951c76c091d9c16dc202a61fc Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 16 Oct 2024 05:49:28 -0700 Subject: [PATCH 035/373] stick grid headings --- .../src/app/gridGL/cells/CellsSheet.ts | 3 +- .../src/app/gridGL/cells/CellsSheets.ts | 4 +- .../src/app/gridGL/cells/tables/Tables.ts | 54 +++++++++++++++---- .../src/app/gridGL/pixiApp/Update.ts | 2 +- 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts index de90988122..fff875bcb3 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts @@ -105,9 +105,10 @@ export class CellsSheet extends Container { return this.cellsImages.children; } - update() { + update(dirtyViewport: boolean) { this.cellsFills.update(); this.borders.update(); + this.tables.update(dirtyViewport); } private renderValidations = ( diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts index 3971bda970..47c9687cb7 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts @@ -193,7 +193,7 @@ export class CellsSheets extends Container { return cellsSheet.cellsArray.isCodeCellOutput(cursor.x, cursor.y); } - update() { - this.current?.update(); + update(dirtyViewport: boolean) { + this.current?.update(dirtyViewport); } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 51a1851fc6..7cb5e22ab7 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -1,8 +1,13 @@ +//! Tables renders all pixi-based UI elements for tables. Right now that's the +//! headings. + /* eslint-disable @typescript-eslint/no-unused-vars */ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; import { FONT_SIZE, OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; @@ -13,8 +18,9 @@ interface Column { bounds: Rectangle; } -interface TableHeading { +interface Table { container: Container; + bounds: Rectangle; headingBounds: Rectangle; originalHeadingBounds: Rectangle; columns: Column[]; @@ -23,12 +29,13 @@ interface TableHeading { export class Tables extends Container { private cellsSheet: CellsSheet; + private headings: Table[]; constructor(cellsSheet: CellsSheet) { super(); this.cellsSheet = cellsSheet; + this.headings = []; events.on('renderCodeCells', this.renderCodeCells); - // todo: update code cells? } @@ -40,12 +47,18 @@ export class Tables extends Container { return sheet; } - cull() {} + cull() { + const bounds = pixiApp.viewport.getVisibleBounds(); + this.headings.forEach((heading) => { + heading.container.visible = intersects.rectangleRectangle(heading.bounds, bounds); + }); + } - private renderHeadings = (codeCell: JsRenderCodeCell): TableHeading => { + private renderCodeCell = (codeCell: JsRenderCodeCell) => { const container = this.addChild(new Container()); - console.log(codeCell.x); - const headingBounds = this.sheet.getScreenRectangle(codeCell.x, codeCell.y, codeCell.column_names.length - 1, 0); + const bounds = this.sheet.getScreenRectangle(codeCell.x, codeCell.y, codeCell.w - 1, codeCell.h - 1); + const headingHeight = this.sheet.offsets.getRowHeight(codeCell.y); + const headingBounds = new Rectangle(bounds.x, bounds.y, bounds.width, headingHeight); const originalHeadingBounds = headingBounds.clone(); container.position.set(headingBounds.x, headingBounds.y); @@ -73,19 +86,42 @@ export class Tables extends Container { return { heading, bounds }; }); - return { + this.headings.push({ container, + bounds, headingBounds, originalHeadingBounds, columns, codeCell, - }; + }); }; private renderCodeCells = (sheetId: string, codeCells: JsRenderCodeCell[]) => { if (sheetId === this.cellsSheet.sheetId) { this.removeChildren(); - codeCells.forEach((codeCell) => this.renderHeadings(codeCell)); + this.headings = []; + codeCells.forEach((codeCell) => this.renderCodeCell(codeCell)); } }; + + private headingPosition = () => { + const bounds = pixiApp.viewport.getVisibleBounds(); + const gridHeading = pixiApp.headings.headingSize.height; + this.headings.forEach((heading) => { + if (heading.container.visible) { + if (heading.headingBounds.top < bounds.top + gridHeading) { + heading.container.y = bounds.top + gridHeading; + } else { + heading.container.y = heading.bounds.top; + } + } + }); + }; + + update(dirtyViewport: boolean) { + if (dirtyViewport) { + this.cull(); + this.headingPosition(); + } + } } diff --git a/quadratic-client/src/app/gridGL/pixiApp/Update.ts b/quadratic-client/src/app/gridGL/pixiApp/Update.ts index 49bf238346..242f91e38a 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Update.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Update.ts @@ -154,7 +154,7 @@ export class Update { debugTimeCheck('[Update] uiImageResize'); pixiApp.cellMoving.update(); debugTimeCheck('[Update] cellMoving'); - pixiApp.cellsSheets.update(); + pixiApp.cellsSheets.update(pixiApp.viewport.dirty); debugTimeCheck('[Update] cellsSheets'); pixiApp.validations.update(pixiApp.viewport.dirty); From f8b7dba53e604b92bb47192c08dbfbfa8c0e06f8 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 16 Oct 2024 06:46:38 -0700 Subject: [PATCH 036/373] table outline --- .../HTMLGrid/tablesOverlay/TablesOverlay.tsx | 40 ++++++++- .../src/app/gridGL/cells/tables/Tables.ts | 85 ++++++++++++++++--- .../src/app/helpers/convertColor.ts | 9 +- 3 files changed, 120 insertions(+), 14 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx index 86fc92f583..273eb3788c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ //! This draws the table heading and provides its context menu. //! //! There are two overlays: the first is the active table that the sheet cursor @@ -46,6 +45,14 @@ export const TableOverlay = () => { return; } const viewport = pixiApp.viewport; + const bounds = viewport.getVisibleBounds(); + if (!bounds.intersects(rect)) { + tableRef.current.style.display = 'none'; + return; + } else { + tableRef.current.style.display = 'block'; + } + const headingHeight = pixiApp.headings.headingSize.height / pixiApp.viewport.scale.y; if (rect.y < viewport.top + headingHeight) { tableTop = rect.y + (viewport.top + headingHeight - rect.y); @@ -120,6 +127,37 @@ export const TableOverlay = () => { } }, [hoverName, hoverRect, hoverTable]); + useEffect(() => { + let tableTop = hoverRect ? hoverRect.y : 0; + const checkViewport = () => { + if (!hoverRect || !hoverTableRef.current) { + return; + } + const viewport = pixiApp.viewport; + const bounds = viewport.getVisibleBounds(); + if (!bounds.intersects(hoverRect)) { + hoverTableRef.current.style.display = 'none'; + return; + } else { + hoverTableRef.current.style.display = 'block'; + } + + const headingHeight = pixiApp.headings.headingSize.height / pixiApp.viewport.scale.y; + if (hoverRect.y < viewport.top + headingHeight) { + tableTop = hoverRect.y + (viewport.top + headingHeight - hoverRect.y); + hoverTableRef.current.style.top = `${tableTop}px`; + } else { + hoverTableRef.current.style.top = `${hoverRect.y}px`; + } + }; + checkViewport(); + + events.on('viewportChanged', checkViewport); + return () => { + events.off('viewportChanged', checkViewport); + }; + }, [hoverRect, hoverTable]); + useEffect(() => { const updateViewport = () => { if (table && tableRef.current) { diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 7cb5e22ab7..b506f0ad2e 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -8,6 +8,7 @@ import { Sheet } from '@/app/grid/sheet/Sheet'; import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; import { FONT_SIZE, OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; @@ -20,7 +21,9 @@ interface Column { interface Table { container: Container; + headingContainer: Container; bounds: Rectangle; + outline: Graphics; headingBounds: Rectangle; originalHeadingBounds: Rectangle; columns: Column[]; @@ -29,14 +32,20 @@ interface Table { export class Tables extends Container { private cellsSheet: CellsSheet; - private headings: Table[]; + private tables: Table[]; + + private activeTable: Table | undefined; + private hoverTable: Table | undefined; constructor(cellsSheet: CellsSheet) { super(); this.cellsSheet = cellsSheet; - this.headings = []; + this.tables = []; events.on('renderCodeCells', this.renderCodeCells); // todo: update code cells? + + events.on('cursorPosition', this.cursorPosition); + events.on('hoverTable', this.setHoverTable); } get sheet(): Sheet { @@ -49,31 +58,34 @@ export class Tables extends Container { cull() { const bounds = pixiApp.viewport.getVisibleBounds(); - this.headings.forEach((heading) => { + this.tables.forEach((heading) => { heading.container.visible = intersects.rectangleRectangle(heading.bounds, bounds); }); } private renderCodeCell = (codeCell: JsRenderCodeCell) => { const container = this.addChild(new Container()); + const bounds = this.sheet.getScreenRectangle(codeCell.x, codeCell.y, codeCell.w - 1, codeCell.h - 1); const headingHeight = this.sheet.offsets.getRowHeight(codeCell.y); const headingBounds = new Rectangle(bounds.x, bounds.y, bounds.width, headingHeight); const originalHeadingBounds = headingBounds.clone(); container.position.set(headingBounds.x, headingBounds.y); + // draw individual headings + const headingContainer = container.addChild(new Container()); + // draw heading background - const background = container.addChild(new Graphics()); + const background = headingContainer.addChild(new Graphics()); background.beginFill(colors.tableHeadingBackground); background.drawShape(new Rectangle(0, 0, headingBounds.width, headingBounds.height)); background.endFill(); - // draw individual headings let x = 0; const columns: Column[] = codeCell.column_names.map((column, index) => { const width = this.sheet.offsets.getColumnWidth(codeCell.x + index); const bounds = new Rectangle(x, headingBounds.y, width, headingBounds.height); - const heading = container.addChild(new Container()); + const heading = headingContainer.addChild(new Container()); heading.position.set(x + OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); heading.addChild( new BitmapText(column.name, { @@ -86,10 +98,18 @@ export class Tables extends Container { return { heading, bounds }; }); - this.headings.push({ + // draw outline + const outline = container.addChild(new Graphics()); + outline.lineStyle({ color: getCSSVariableTint('primary'), width: 2 }); + outline.drawShape(new Rectangle(0, 0, bounds.width, bounds.height)); + outline.visible = false; + + this.tables.push({ container, bounds, headingBounds, + headingContainer, + outline, originalHeadingBounds, columns, codeCell, @@ -99,20 +119,20 @@ export class Tables extends Container { private renderCodeCells = (sheetId: string, codeCells: JsRenderCodeCell[]) => { if (sheetId === this.cellsSheet.sheetId) { this.removeChildren(); - this.headings = []; + this.tables = []; codeCells.forEach((codeCell) => this.renderCodeCell(codeCell)); } }; private headingPosition = () => { const bounds = pixiApp.viewport.getVisibleBounds(); - const gridHeading = pixiApp.headings.headingSize.height; - this.headings.forEach((heading) => { + const gridHeading = pixiApp.headings.headingSize.height / pixiApp.viewport.scaled; + this.tables.forEach((heading) => { if (heading.container.visible) { if (heading.headingBounds.top < bounds.top + gridHeading) { - heading.container.y = bounds.top + gridHeading; + heading.headingContainer.y = bounds.top + gridHeading - heading.headingBounds.top; } else { - heading.container.y = heading.bounds.top; + heading.headingContainer.y = 0; } } }); @@ -124,4 +144,45 @@ export class Tables extends Container { this.headingPosition(); } } + + // Updates the active table when the cursor moves. + private cursorPosition = () => { + if (this.sheet.id !== sheets.sheet.id) { + return; + } + if (this.activeTable) { + this.activeTable.outline.visible = false; + pixiApp.setViewportDirty(); + } + const cursor = sheets.sheet.cursor.cursorPosition; + this.activeTable = this.tables.find((table) => { + const rect = new Rectangle(table.codeCell.x, table.codeCell.y, table.codeCell.w - 1, table.codeCell.h - 1); + return intersects.rectanglePoint(rect, cursor); + }); + if (this.activeTable) { + this.activeTable.outline.visible = true; + pixiApp.setViewportDirty(); + } + }; + + private setHoverTable = (codeCell?: JsRenderCodeCell) => { + if (this.sheet.id !== sheets.sheet.id) { + return; + } + if (!codeCell) { + if (this.hoverTable) { + if (this.hoverTable !== this.activeTable) { + this.hoverTable.outline.visible = false; + pixiApp.setViewportDirty(); + } + this.hoverTable = undefined; + } + return; + } + this.hoverTable = this.tables.find((table) => table.codeCell.x === codeCell.x && table.codeCell.y === codeCell.y); + if (this.hoverTable) { + this.hoverTable.outline.visible = true; + pixiApp.setViewportDirty(); + } + }; } diff --git a/quadratic-client/src/app/helpers/convertColor.ts b/quadratic-client/src/app/helpers/convertColor.ts index 702d9188af..bb7955dbee 100644 --- a/quadratic-client/src/app/helpers/convertColor.ts +++ b/quadratic-client/src/app/helpers/convertColor.ts @@ -1,8 +1,8 @@ import * as Sentry from '@sentry/react'; import Color from 'color'; import { ColorResult } from 'react-color'; -import { colors } from '../theme/colors'; import { Rgba } from '../quadratic-core-types'; +import { colors } from '../theme/colors'; export function convertReactColorToString(color: ColorResult): string { const rgb = color.rgb; @@ -72,3 +72,10 @@ export function convertRgbaToTint(rgba: Rgba): { tint: number; alpha: number } { const rgb = { r: rgba.red, g: rgba.green, b: rgba.blue }; return { tint: Color(rgb).rgbNumber(), alpha: rgba.alpha }; } + +export function getCSSVariableTint(variable: string): number { + // Add this function to get CSS variable value + const color = getComputedStyle(document.documentElement).getPropertyValue(`--${variable}`).trim(); + const parsed = Color.hsl(color.split(' ').map(parseFloat)); + return parsed.rgbNumber(); +} From 3ec557669bf5bd1c45ba4c8eccb3defa354363cf Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 16 Oct 2024 06:58:54 -0700 Subject: [PATCH 037/373] updates heading when sheet offsets change --- .../src/app/gridGL/cells/CellsSheet.ts | 1 + .../src/app/gridGL/cells/tables/Tables.ts | 31 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts index fff875bcb3..e85e901745 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts @@ -95,6 +95,7 @@ export class CellsSheet extends Container { adjustOffsets() { this.borders.setDirty(); + this.tables.sheetOffsets(this.sheetId); } updateCellsArray() { diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index b506f0ad2e..32784e7160 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -24,6 +24,7 @@ interface Table { headingContainer: Container; bounds: Rectangle; outline: Graphics; + // headingLine: Graphics; headingBounds: Rectangle; originalHeadingBounds: Rectangle; columns: Column[]; @@ -46,6 +47,8 @@ export class Tables extends Container { events.on('cursorPosition', this.cursorPosition); events.on('hoverTable', this.setHoverTable); + + events.on('sheetOffsets', this.sheetOffsets); } get sheet(): Sheet { @@ -81,6 +84,12 @@ export class Tables extends Container { background.drawShape(new Rectangle(0, 0, headingBounds.width, headingBounds.height)); background.endFill(); + // // draw heading line + // const headingLine = headingContainer.addChild(new Graphics()); + // headingLine.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); + // headingLine.moveTo(0, headingHeight).lineTo(headingBounds.width, headingHeight); + // headingLine.visible = false; + let x = 0; const columns: Column[] = codeCell.column_names.map((column, index) => { const width = this.sheet.offsets.getColumnWidth(codeCell.x + index); @@ -94,13 +103,18 @@ export class Tables extends Container { tint: colors.tableHeadingForeground, }) ); + + // // draw heading line between columns + // if (index !== codeCell.column_names.length - 1) { + // headingLine.moveTo(x + width, 0).lineTo(x + width, headingHeight); + // } x += width; return { heading, bounds }; }); // draw outline const outline = container.addChild(new Graphics()); - outline.lineStyle({ color: getCSSVariableTint('primary'), width: 2 }); + outline.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); outline.drawShape(new Rectangle(0, 0, bounds.width, bounds.height)); outline.visible = false; @@ -110,6 +124,7 @@ export class Tables extends Container { headingBounds, headingContainer, outline, + // headingLine, originalHeadingBounds, columns, codeCell, @@ -152,6 +167,7 @@ export class Tables extends Container { } if (this.activeTable) { this.activeTable.outline.visible = false; + // this.activeTable.headingLine.visible = false; pixiApp.setViewportDirty(); } const cursor = sheets.sheet.cursor.cursorPosition; @@ -161,6 +177,7 @@ export class Tables extends Container { }); if (this.activeTable) { this.activeTable.outline.visible = true; + // this.activeTable.headingLine.visible = true; pixiApp.setViewportDirty(); } }; @@ -173,6 +190,7 @@ export class Tables extends Container { if (this.hoverTable) { if (this.hoverTable !== this.activeTable) { this.hoverTable.outline.visible = false; + // this.hoverTable.headingLine.visible = false; pixiApp.setViewportDirty(); } this.hoverTable = undefined; @@ -182,7 +200,18 @@ export class Tables extends Container { this.hoverTable = this.tables.find((table) => table.codeCell.x === codeCell.x && table.codeCell.y === codeCell.y); if (this.hoverTable) { this.hoverTable.outline.visible = true; + // this.hoverTable.headingLine.visible = true; pixiApp.setViewportDirty(); } }; + + // Redraw the headings if the offsets change. + sheetOffsets = (sheetId: string) => { + if (sheetId === this.sheet.id) { + this.renderCodeCells( + sheetId, + this.tables.map((table) => table.codeCell) + ); + } + }; } From bd487eeba71fef7caf401474001747e37e657ad5 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 16 Oct 2024 08:23:04 -0700 Subject: [PATCH 038/373] context menu for table --- .../src/app/atoms/tableHeadingAtom.ts | 39 +++++ quadratic-client/src/app/events/events.ts | 3 + .../tablesOverlay/TableContextMenu.tsx | 133 ++++++++++++++++++ .../HTMLGrid/tablesOverlay/TablesOverlay.tsx | 22 ++- 4 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 quadratic-client/src/app/atoms/tableHeadingAtom.ts create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TableContextMenu.tsx diff --git a/quadratic-client/src/app/atoms/tableHeadingAtom.ts b/quadratic-client/src/app/atoms/tableHeadingAtom.ts new file mode 100644 index 0000000000..a74c690416 --- /dev/null +++ b/quadratic-client/src/app/atoms/tableHeadingAtom.ts @@ -0,0 +1,39 @@ +import { events } from '@/app/events/events'; +import { Point } from 'pixi.js'; +import { atom } from 'recoil'; + +interface TableHeading { + world?: Point; + column: number | null; + row: number | null; +} + +const defaultTableHeadingState: TableHeading = { + world: undefined, + column: null, + row: null, +}; + +export const tableHeadingAtom = atom({ + key: 'tableHeadingState', + default: defaultTableHeadingState, + effects: [ + ({ setSelf }) => { + const clear = () => { + setSelf(() => ({ world: undefined, column: null, row: null })); + }; + + const set = (world: Point, column: number | null, row: number | null) => { + setSelf(() => ({ world, column, row })); + }; + + events.on('cursorPosition', clear); + events.on('tableContextMenu', set); + + return () => { + events.off('cursorPosition', clear); + events.off('tableContextMenu', set); + }; + }, + ], +}); diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 270ffda6ff..f1eae29492 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -127,6 +127,9 @@ interface EventTypes { // context menu opens on a grid heading gridContextMenu: (world: Point, row: number | null, column: number | null) => void; + // context menu on a table + tableContextMenu: (world: Point, row: number | null, column: number | null) => void; + suggestionDropdownKeyboard: (key: 'ArrowDown' | 'ArrowUp' | 'Enter' | 'Escape' | 'Tab') => void; // use this to set a drawing element to dirty diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TableContextMenu.tsx new file mode 100644 index 0000000000..e3cbc51933 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TableContextMenu.tsx @@ -0,0 +1,133 @@ +//! This shows the table context menu. + +import { Action } from '@/app/actions/actions'; +import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; +import { tableHeadingAtom } from '@/app/atoms/tableHeadingAtom'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { focusGrid } from '@/app/helpers/focusGrid'; +import { keyboardShortcutEnumToDisplay } from '@/app/helpers/keyboardShortcutsDisplay'; +import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; +import { IconComponent } from '@/shared/components/Icons'; +import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu'; +import { useCallback, useEffect, useRef } from 'react'; +import { useRecoilState } from 'recoil'; + +export const TableContextMenu = () => { + const [show, setShow] = useRecoilState(tableHeadingAtom); + + const onClose = useCallback(() => { + setShow({ world: undefined, column: null, row: null }); + focusGrid(); + }, [setShow]); + + useEffect(() => { + pixiApp.viewport.on('moved', onClose); + pixiApp.viewport.on('zoomed', onClose); + + return () => { + pixiApp.viewport.off('moved', onClose); + pixiApp.viewport.off('zoomed', onClose); + }; + }, [onClose]); + + const ref = useRef(null); + + const isColumnRowAvailable = sheets.sheet.cursor.hasOneColumnRowSelection(true); + + return ( +
+ + + + + + + + + + {show.column === null ? null : ( + <> + + {isColumnRowAvailable && } + {isColumnRowAvailable && } + + + )} + + {show.row === null ? null : ( + <> + {isColumnRowAvailable && } + {isColumnRowAvailable && } + {isColumnRowAvailable && } + + + )} + + + + + + + + + +
+ ); +}; + +function MenuItemAction({ action }: { action: Action }) { + const { label, Icon, run, isAvailable } = defaultActionSpec[action]; + const isAvailableArgs = useIsAvailableArgs(); + const keyboardShortcut = keyboardShortcutEnumToDisplay(action); + + if (isAvailable && !isAvailable(isAvailableArgs)) { + return null; + } + + return ( + + {label} + + ); +} + +function MenuItemShadStyle({ + children, + Icon, + onClick, + keyboardShortcut, +}: { + children: string; + Icon?: IconComponent; + onClick: any; + keyboardShortcut?: string; +}) { + const menuItemClassName = + 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; + return ( + + + {Icon && } {children} + + {keyboardShortcut && ( + {keyboardShortcut} + )} + + ); +} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx index 273eb3788c..cc366ffd67 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx @@ -3,14 +3,19 @@ //! There are two overlays: the first is the active table that the sheet cursor //! is in. The second is the table that the mouse is hovering over. +import { tableHeadingAtom } from '@/app/atoms/tableHeadingAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { ArrowDropDownIcon } from '@/shared/components/Icons'; import { Rectangle } from 'pixi.js'; import { useEffect, useMemo, useRef, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; import { pixiApp } from '../../pixiApp/PixiApp'; export const TableOverlay = () => { + const setTableContextMenu = useSetRecoilState(tableHeadingAtom); + const tableRef = useRef(null); const hoverTableRef = useRef(null); @@ -100,13 +105,26 @@ export const TableOverlay = () => { top: rect.y, transformOrigin: 'bottom left', transform: `translateY(-100%) scale(${1 / pixiApp.viewport.scale.x})`, + pointerEvents: 'auto', + cursor: 'default', }} > -
{name}
+
{ + const world = pixiApp.viewport.toWorld(e.clientX, e.clientY); + setTableContextMenu({ world, row: table.x, column: table.y }); + e.stopPropagation(); + e.preventDefault(); + }} + className="flex text-nowrap bg-primary px-1 text-sm text-primary-foreground" + > + {name} + +
); } - }, [name, rect, table]); + }, [name, rect, table, setTableContextMenu]); const hoverTableRender = useMemo(() => { if (hoverTable && hoverRect && hoverName !== undefined) { From fd7e033d75b261cbdfb3e67a49403e142fe3dd89 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 16 Oct 2024 18:57:07 -0600 Subject: [PATCH 039/373] Fix forward and reverse operations for all things data tables --- .../execute_operation/execute_data_table.rs | 134 ++++++++++-------- .../execution/execute_operation/mod.rs | 14 ++ .../src/controller/user_actions/import.rs | 2 +- quadratic-core/src/test_util.rs | 2 +- quadratic-rust-shared/src/auto_gen_path.rs | 4 +- 5 files changed, 91 insertions(+), 65 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 739704663c..b2c033caa9 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -166,7 +166,6 @@ impl GridController { // Pull out the data table via a swap, removing it from the sheet let data_table = sheet.delete_data_table(data_table_pos)?; - let old_values = data_table.value.to_owned().into_array()?; let values = data_table.display_value()?.into_array()?; let ArraySize { w, h } = values.size(); @@ -178,8 +177,6 @@ impl GridController { let sheet_rect = SheetRect::new_pos_span(data_table_pos, max, sheet_id); let _ = sheet.set_cell_values(sheet_rect.into(), &values); - let old_cell_values = CellValues::from(old_values); - let cell_values = CellValues::from(values); // let the client know that the code cell changed to remove the styles if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() { @@ -188,22 +185,9 @@ impl GridController { self.send_to_wasm(transaction, &sheet_rect)?; - let forward_operations = vec![Operation::SetCellValues { - sheet_pos, - values: cell_values, - }]; + let forward_operations = vec![Operation::FlattenDataTable { sheet_pos }]; - let reverse_operations = vec![ - Operation::SetCellValues { - sheet_pos, - values: old_cell_values, - }, - Operation::SetCodeRun { - sheet_pos, - code_run: Some(data_table), - index: 0, - }, - ]; + let reverse_operations = vec![Operation::GridToDataTable { sheet_rect }]; self.data_table_operations( transaction, @@ -249,17 +233,7 @@ impl GridController { self.send_to_wasm(transaction, &sheet_rect)?; - let forward_operations = vec![ - Operation::SetCellValues { - sheet_pos, - values: CellValues::from(CellValue::Import(import)), - }, - Operation::SetCodeRun { - sheet_pos, - code_run: Some(data_table), - index: 0, - }, - ]; + let forward_operations = vec![Operation::GridToDataTable { sheet_rect }]; let reverse_operations = vec![Operation::SetCellValues { sheet_pos, @@ -368,10 +342,16 @@ impl GridController { #[cfg(test)] mod tests { use crate::{ - controller::user_actions::import::tests::{assert_simple_csv, simple_csv}, + controller::{ + execution::execute_operation::{ + execute_forward_operations, execute_reverse_operations, + }, + user_actions::import::tests::{assert_simple_csv, simple_csv}, + }, grid::SheetId, test_util::{ - assert_cell_value_row, assert_data_table_cell_value_row, print_data_table, print_table, + assert_cell_value_row, assert_data_table_cell_value, assert_data_table_cell_value_row, + print_data_table, print_table, }, SheetPos, }; @@ -383,7 +363,7 @@ mod tests { sheet_id: SheetId, pos: Pos, file_name: &'a str, - ) { + ) -> PendingTransaction { let sheet_pos = SheetPos::from((pos, sheet_id)); let op = Operation::FlattenDataTable { sheet_pos }; let mut transaction = PendingTransaction::default(); @@ -393,15 +373,15 @@ mod tests { gc.execute_flatten_data_table(&mut transaction, op).unwrap(); assert_eq!(transaction.forward_operations.len(), 1); - assert_eq!(transaction.reverse_operations.len(), 2); - - gc.finalize_transaction(transaction); + assert_eq!(transaction.reverse_operations.len(), 1); assert!(gc.sheet(sheet_id).first_data_table_within(pos).is_err()); assert_flattened_simple_csv(&gc, sheet_id, pos, file_name); print_table(&gc, sheet_id, Rect::new(0, 0, 2, 2)); + + transaction } #[track_caller] @@ -423,26 +403,54 @@ mod tests { (gc, sheet_id, pos, file_name) } + #[track_caller] + pub(crate) fn assert_sorted_data_table<'a>( + gc: &'a GridController, + sheet_id: SheetId, + pos: Pos, + file_name: &'a str, + ) -> (&'a GridController, SheetId, Pos, &'a str) { + let first_row = vec!["Concord", "NH", "United States", "42605"]; + assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 1, first_row); + + let second_row = vec!["Marlborough", "MA", "United States", "38334"]; + assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 2, second_row); + + let third_row = vec!["Northbridge", "MA", "United States", "14061"]; + assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 3, third_row); + + let last_row = vec!["Westborough", "MA", "United States", "29313"]; + assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 10, last_row); + (gc, sheet_id, pos, file_name) + } + #[test] fn test_execute_set_data_table_at() { - let (mut gc, sheet_id, pos, _) = simple_csv(); - let change_val_pos = Pos::new(1, 1); + let (mut gc, sheet_id, _, _) = simple_csv(); + let x = 1; + let y = 1; + let change_val_pos = Pos::new(x, y); let sheet_pos = SheetPos::from((change_val_pos, sheet_id)); let values = CellValue::Number(1.into()).into(); let op = Operation::SetDataTableAt { sheet_pos, values }; let mut transaction = PendingTransaction::default(); + // the initial value from the csv + assert_data_table_cell_value(&gc, sheet_id, x, y, "MA"); + gc.execute_set_data_table_at(&mut transaction, op).unwrap(); - assert_eq!(transaction.forward_operations.len(), 1); - assert_eq!(transaction.reverse_operations.len(), 1); + // expect the value to be "1" + assert_data_table_cell_value(&gc, sheet_id, x, y, "1"); - gc.finalize_transaction(transaction); + // undo, the value should be "MA" again + execute_reverse_operations(&mut gc, &transaction); + assert_data_table_cell_value(&gc, sheet_id, x, y, "MA"); - let data_table = gc.sheet(sheet_id).data_table(pos).unwrap(); - let expected = CellValue::Number(1.into()); - assert_eq!(data_table.get_cell_for_formula(1, 1), expected); + // redo, the value should be "1" again + execute_forward_operations(&mut gc, &mut transaction); + assert_data_table_cell_value(&gc, sheet_id, x, y, "1"); } #[test] @@ -451,10 +459,18 @@ mod tests { assert_simple_csv(&gc, sheet_id, pos, file_name); - flatten_data_table(&mut gc, sheet_id, pos, file_name); + let mut transaction = flatten_data_table(&mut gc, sheet_id, pos, file_name); print_table(&gc, sheet_id, Rect::new(0, 0, 2, 2)); assert_flattened_simple_csv(&gc, sheet_id, pos, file_name); + + // undo, the value should be a data table again + execute_reverse_operations(&mut gc, &transaction); + assert_simple_csv(&gc, sheet_id, pos, file_name); + + // redo, the value should be on the grid + execute_forward_operations(&mut gc, &mut transaction); + assert_flattened_simple_csv(&gc, sheet_id, pos, file_name); } #[test] @@ -469,13 +485,17 @@ mod tests { let mut transaction = PendingTransaction::default(); gc.execute_grid_to_data_table(&mut transaction, op).unwrap(); - // assert_eq!(transaction.forward_operations.len(), 1); - // assert_eq!(transaction.reverse_operations.len(), 2); - - gc.finalize_transaction(transaction); print_data_table(&gc, sheet_id, Rect::new(0, 0, 2, 2)); assert_simple_csv(&gc, sheet_id, pos, file_name); + + // undo, the value should be a data table again + execute_reverse_operations(&mut gc, &transaction); + assert_flattened_simple_csv(&gc, sheet_id, pos, file_name); + + // redo, the value should be on the grid + execute_forward_operations(&mut gc, &mut transaction); + assert_simple_csv(&gc, sheet_id, pos, file_name); } #[test] @@ -495,22 +515,14 @@ mod tests { let mut transaction = PendingTransaction::default(); gc.execute_sort_data_table(&mut transaction, op).unwrap(); - // assert_eq!(transaction.forward_operations.len(), 1); - // assert_eq!(transaction.reverse_operations.len(), 2); - - gc.finalize_transaction(transaction); print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); - let first_row = vec!["Concord", "NH", "United States", "42605"]; - assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 1, first_row); - - let second_row = vec!["Marlborough", "MA", "United States", "38334"]; - assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 2, second_row); + assert_sorted_data_table(&gc, sheet_id, pos, "simple.csv"); - let third_row = vec!["Northbridge", "MA", "United States", "14061"]; - assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 3, third_row); + // undo, the value should be a data table again + execute_reverse_operations(&mut gc, &transaction); - let last_row = vec!["Westborough", "MA", "United States", "29313"]; - assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 10, last_row); + // redo, the value should be on the grid + execute_forward_operations(&mut gc, &mut transaction); } } diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 50d683ce33..8131b9273c 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -102,3 +102,17 @@ impl GridController { } } } + +#[cfg(test)] +pub fn execute_reverse_operations(gc: &mut GridController, transaction: &PendingTransaction) { + let mut undo_transaction = PendingTransaction::default(); + undo_transaction.operations = transaction.reverse_operations.clone().into(); + gc.execute_operation(&mut undo_transaction); +} + +#[cfg(test)] +pub fn execute_forward_operations(gc: &mut GridController, transaction: &mut PendingTransaction) { + let mut undo_transaction = PendingTransaction::default(); + undo_transaction.operations = transaction.forward_operations.clone().into(); + gc.execute_operation(&mut undo_transaction); +} diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index 7861bca7e6..f00d5d3033 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -132,7 +132,7 @@ pub(crate) mod tests { ) -> (&'a GridController, SheetId, Pos, &'a str) { let import = Import::new(file_name.into()); let cell_value = CellValue::Import(import); - assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); + // assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); // data table should be at `pos` assert_eq!( diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index 6a10040a47..754ebb7a65 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -226,7 +226,7 @@ pub fn print_table(grid_controller: &GridController, sheet_id: SheetId, rect: Re pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rect: Rect) { if let Some(sheet) = grid_controller.try_sheet(sheet_id) { let data_table = sheet.data_table(rect.min).unwrap(); - println!("Data table: {:?}", data_table); + let max = rect.max.y - rect.min.y; crate::grid::data_table::test::pretty_print_data_table( data_table, diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index 8df3d2b4d5..a0dea21ae2 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From 49e64fa5aa2718ed3871f22292ed3c22d5a5ecac Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 17 Oct 2024 05:25:19 -0700 Subject: [PATCH 040/373] table context menu + move table name to pixi --- .../public/images/dropdown-white.png | Bin 0 -> 1236 bytes .../src/app/atoms/contextMenuAtoms.ts | 45 ++++ .../src/app/atoms/gridHeadingAtom.ts | 39 --- .../src/app/atoms/tableHeadingAtom.ts | 39 --- quadratic-client/src/app/events/events.ts | 8 +- .../app/gridGL/HTMLGrid/GridContextMenu.tsx | 19 +- .../app/gridGL/HTMLGrid/HTMLGridContainer.tsx | 4 +- .../{tablesOverlay => }/TableContextMenu.tsx | 28 +-- .../HTMLGrid/tablesOverlay/TablesOverlay.tsx | 202 ---------------- .../src/app/gridGL/cells/tables/Table.ts | 226 ++++++++++++++++++ .../src/app/gridGL/cells/tables/Tables.ts | 201 ++++------------ .../app/gridGL/interaction/pointer/Pointer.ts | 6 +- .../gridGL/interaction/pointer/PointerDown.ts | 5 +- .../interaction/pointer/PointerDownTable.ts | 24 ++ .../interaction/pointer/PointerHeading.ts | 9 +- .../interaction/pointer/pointerCursor.ts | 31 +-- quadratic-client/src/app/gridGL/loadAssets.ts | 1 + .../src/app/gridGL/pixiApp/PixiApp.ts | 6 + .../src/grid/file/serialize/data_table.rs | 2 - quadratic-core/src/grid/sheet/col_row/row.rs | 1 - 20 files changed, 393 insertions(+), 503 deletions(-) create mode 100644 quadratic-client/public/images/dropdown-white.png create mode 100644 quadratic-client/src/app/atoms/contextMenuAtoms.ts delete mode 100644 quadratic-client/src/app/atoms/gridHeadingAtom.ts delete mode 100644 quadratic-client/src/app/atoms/tableHeadingAtom.ts rename quadratic-client/src/app/gridGL/HTMLGrid/{tablesOverlay => }/TableContextMenu.tsx (85%) delete mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx create mode 100644 quadratic-client/src/app/gridGL/cells/tables/Table.ts create mode 100644 quadratic-client/src/app/gridGL/interaction/pointer/PointerDownTable.ts diff --git a/quadratic-client/public/images/dropdown-white.png b/quadratic-client/public/images/dropdown-white.png new file mode 100644 index 0000000000000000000000000000000000000000..b3e660a0ebefd3452e20f7dbc9ef0d3193545173 GIT binary patch literal 1236 zcmYLI3rrIS7(N8OD+ty?=Odna({r{IDTo9iMtW_r*A~*&77*=LVnJTI4z8Qifgo1} zfeoS9$?6cL4wNY(LZ(A)7iG+;M=SWK;>v{im^y)_Wei>2mM!_e@5`V6OaA14l*^LV z1^UnT2LK=t7YP(z)xH5V!^^s%Iwb)3?9S%%Wd(|L*kWABkB(R!9UT_AA_4%G@7M3Y z5h{>}^sLRydblou!{ICp42?=CuZpbQQ*$ih2hGZ{5mWuVQ|?T=_Cy{NjV=oF1l(rl zTFQ*AUzZ2&xc`=;8_vv@-j2kO{l>Ewk zabrf1r8+eec8U75Wf`(&10F0R?|ydQWhW&eoHxOc|O>+)j7CkzUSyrwLR-#>yUb2)Ayzs724PCC+Axy zwB6QOf8NnF#nO^qQVZg}M~>O(0C_0#G$U@t`i5T?if`b=l~Bg2EwFvON)m4LTfE z>nS;8QrcDEA%l*E-q%wbAd|wb27O5#4doNuc*rD+6o92?{L^uiuji&h64tzvrB*Ce+^d77bQ?o_?2%?S{eUb|2-cGHwB9IK%D}Qv2f>v? zNS(CbD)5dqQU%r;=;B@imcE58M;&^4JcJlYiU%O1jDdkXlB>e!!hW-DN;I9Ii=g#J z(#~6qNaYMMXeGHyJO=g)w#iTzL6<>%BYBe-kIa-a6d=z)sqj?TkBOzA>3Vu9q&AWR zJPks~8EK%^Kq>LW-_}fD$j0F_JLIDT t6E-y)M^76^r-moEI~)EvRWJAdL3@cFRK=b6bj14@04_`toWwM{{{mnf67T>3 literal 0 HcmV?d00001 diff --git a/quadratic-client/src/app/atoms/contextMenuAtoms.ts b/quadratic-client/src/app/atoms/contextMenuAtoms.ts new file mode 100644 index 0000000000..2f6e657289 --- /dev/null +++ b/quadratic-client/src/app/atoms/contextMenuAtoms.ts @@ -0,0 +1,45 @@ +import { events } from '@/app/events/events'; +import { Point } from 'pixi.js'; +import { atom } from 'recoil'; + +export enum ContextMenuType { + Grid = 'grid', + Table = 'table', +} + +interface ContextMenu { + type?: ContextMenuType; + world?: Point; + column: number | null; + row: number | null; +} + +const defaultContextMenuState: ContextMenu = { + world: undefined, + column: null, + row: null, +}; + +export const contextMenuAtom = atom({ + key: 'contextMenuState', + default: defaultContextMenuState, + effects: [ + ({ setSelf }) => { + const clear = () => { + setSelf(() => ({ type: undefined, world: undefined, column: null, row: null })); + }; + + const set = (type: ContextMenuType, world: Point, column: number | null, row: number | null) => { + setSelf(() => ({ type, world, column, row })); + }; + + events.on('cursorPosition', clear); + events.on('contextMenu', set); + + return () => { + events.off('cursorPosition', clear); + events.off('contextMenu', set); + }; + }, + ], +}); diff --git a/quadratic-client/src/app/atoms/gridHeadingAtom.ts b/quadratic-client/src/app/atoms/gridHeadingAtom.ts deleted file mode 100644 index c25df5581b..0000000000 --- a/quadratic-client/src/app/atoms/gridHeadingAtom.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { events } from '@/app/events/events'; -import { Point } from 'pixi.js'; -import { atom } from 'recoil'; - -interface GridHeading { - world?: Point; - column: number | null; - row: number | null; -} - -const defaultGridHeadingState: GridHeading = { - world: undefined, - column: null, - row: null, -}; - -export const gridHeadingAtom = atom({ - key: 'gridHeadingState', - default: defaultGridHeadingState, - effects: [ - ({ setSelf }) => { - const clear = () => { - setSelf(() => ({ world: undefined, column: null, row: null })); - }; - - const set = (world: Point, column: number | null, row: number | null) => { - setSelf(() => ({ world, column, row })); - }; - - events.on('cursorPosition', clear); - events.on('gridContextMenu', set); - - return () => { - events.off('cursorPosition', clear); - events.off('gridContextMenu', set); - }; - }, - ], -}); diff --git a/quadratic-client/src/app/atoms/tableHeadingAtom.ts b/quadratic-client/src/app/atoms/tableHeadingAtom.ts deleted file mode 100644 index a74c690416..0000000000 --- a/quadratic-client/src/app/atoms/tableHeadingAtom.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { events } from '@/app/events/events'; -import { Point } from 'pixi.js'; -import { atom } from 'recoil'; - -interface TableHeading { - world?: Point; - column: number | null; - row: number | null; -} - -const defaultTableHeadingState: TableHeading = { - world: undefined, - column: null, - row: null, -}; - -export const tableHeadingAtom = atom({ - key: 'tableHeadingState', - default: defaultTableHeadingState, - effects: [ - ({ setSelf }) => { - const clear = () => { - setSelf(() => ({ world: undefined, column: null, row: null })); - }; - - const set = (world: Point, column: number | null, row: number | null) => { - setSelf(() => ({ world, column, row })); - }; - - events.on('cursorPosition', clear); - events.on('tableContextMenu', set); - - return () => { - events.off('cursorPosition', clear); - events.off('tableContextMenu', set); - }; - }, - ], -}); diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index f1eae29492..82bf3db8bb 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -1,3 +1,4 @@ +import { ContextMenuType } from '@/app/atoms/contextMenuAtoms'; import { ErrorValidation } from '@/app/gridGL/cells/CellsSheet'; import { EditingCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; import { SheetPosTS } from '@/app/gridGL/types/size'; @@ -124,11 +125,8 @@ interface EventTypes { // when validation changes state validation: (validation: string | boolean) => void; - // context menu opens on a grid heading - gridContextMenu: (world: Point, row: number | null, column: number | null) => void; - - // context menu on a table - tableContextMenu: (world: Point, row: number | null, column: number | null) => void; + // trigger or clear a context menu + contextMenu: (type: ContextMenuType, world: Point, row: number | null, column: number | null) => void; suggestionDropdownKeyboard: (key: 'ArrowDown' | 'ArrowUp' | 'Enter' | 'Escape' | 'Tab') => void; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx index 3ef77cd784..80a03f168a 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx @@ -2,7 +2,7 @@ import { Action } from '@/app/actions/actions'; import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; -import { gridHeadingAtom } from '@/app/atoms/gridHeadingAtom'; +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtoms'; import { sheets } from '@/app/grid/controller/Sheets'; import { focusGrid } from '@/app/helpers/focusGrid'; import { keyboardShortcutEnumToDisplay } from '@/app/helpers/keyboardShortcutsDisplay'; @@ -14,12 +14,12 @@ import { useRecoilState } from 'recoil'; import { pixiApp } from '../pixiApp/PixiApp'; export const GridContextMenu = () => { - const [show, setShow] = useRecoilState(gridHeadingAtom); + const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { - setShow({ world: undefined, column: null, row: null }); + setContextMenu({ type: undefined, world: undefined, column: null, row: null }); focusGrid(); - }, [setShow]); + }, [setContextMenu]); useEffect(() => { pixiApp.viewport.on('moved', onClose); @@ -40,14 +40,15 @@ export const GridContextMenu = () => { className="absolute" ref={ref} style={{ - left: show.world?.x ?? 0, - top: show.world?.y ?? 0, + left: contextMenu.world?.x ?? 0, + top: contextMenu.world?.y ?? 0, transform: `scale(${1 / pixiApp.viewport.scale.x})`, pointerEvents: 'auto', + display: contextMenu.type === ContextMenuType.Grid ? 'block' : 'none', }} > { - {show.column === null ? null : ( + {contextMenu.column === null ? null : ( <> {isColumnRowAvailable && } @@ -70,7 +71,7 @@ export const GridContextMenu = () => { )} - {show.row === null ? null : ( + {contextMenu.row === null ? null : ( <> {isColumnRowAvailable && } {isColumnRowAvailable && } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index af6b14aac1..02e230bfaf 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -9,13 +9,13 @@ import { HtmlCells } from '@/app/gridGL/HTMLGrid/htmlCells/HtmlCells'; import { InlineEditor } from '@/app/gridGL/HTMLGrid/inlineEditor/InlineEditor'; import { MultiplayerCursors } from '@/app/gridGL/HTMLGrid/multiplayerCursor/MultiplayerCursors'; import { MultiplayerCellEdits } from '@/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdits'; +import { TableContextMenu } from '@/app/gridGL/HTMLGrid/TableContextMenu'; import { useHeadingSize } from '@/app/gridGL/HTMLGrid/useHeadingSize'; import { HtmlValidations } from '@/app/gridGL/HTMLGrid/validations/HtmlValidations'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Following } from '@/app/ui/components/Following'; import { ReactNode, useCallback, useEffect, useState } from 'react'; import { SuggestionDropDown } from './SuggestionDropdown'; -import { TableOverlay } from './tablesOverlay/TablesOverlay'; interface Props { parent?: HTMLDivElement; @@ -131,8 +131,8 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { pointerEvents: 'none', }} > - + ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/TableContextMenu.tsx similarity index 85% rename from quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TableContextMenu.tsx rename to quadratic-client/src/app/gridGL/HTMLGrid/TableContextMenu.tsx index e3cbc51933..cfec335429 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/TableContextMenu.tsx @@ -2,24 +2,23 @@ import { Action } from '@/app/actions/actions'; import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; -import { tableHeadingAtom } from '@/app/atoms/tableHeadingAtom'; -import { sheets } from '@/app/grid/controller/Sheets'; +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtoms'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { focusGrid } from '@/app/helpers/focusGrid'; import { keyboardShortcutEnumToDisplay } from '@/app/helpers/keyboardShortcutsDisplay'; import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; import { IconComponent } from '@/shared/components/Icons'; -import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu'; +import { ControlledMenu, MenuItem } from '@szhsin/react-menu'; import { useCallback, useEffect, useRef } from 'react'; import { useRecoilState } from 'recoil'; export const TableContextMenu = () => { - const [show, setShow] = useRecoilState(tableHeadingAtom); + const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { - setShow({ world: undefined, column: null, row: null }); + setContextMenu({ type: undefined, world: undefined, column: null, row: null }); focusGrid(); - }, [setShow]); + }, [setContextMenu]); useEffect(() => { pixiApp.viewport.on('moved', onClose); @@ -33,35 +32,34 @@ export const TableContextMenu = () => { const ref = useRef(null); - const isColumnRowAvailable = sheets.sheet.cursor.hasOneColumnRowSelection(true); - return (
- + {/* - {show.column === null ? null : ( + {contextMenu.column === null ? null : ( <> {isColumnRowAvailable && } @@ -70,7 +68,7 @@ export const TableContextMenu = () => { )} - {show.row === null ? null : ( + {contextMenu.row === null ? null : ( <> {isColumnRowAvailable && } {isColumnRowAvailable && } @@ -85,7 +83,7 @@ export const TableContextMenu = () => { - + */}
); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx deleted file mode 100644 index cc366ffd67..0000000000 --- a/quadratic-client/src/app/gridGL/HTMLGrid/tablesOverlay/TablesOverlay.tsx +++ /dev/null @@ -1,202 +0,0 @@ -//! This draws the table heading and provides its context menu. -//! -//! There are two overlays: the first is the active table that the sheet cursor -//! is in. The second is the table that the mouse is hovering over. - -import { tableHeadingAtom } from '@/app/atoms/tableHeadingAtom'; -import { events } from '@/app/events/events'; -import { sheets } from '@/app/grid/controller/Sheets'; -import { JsRenderCodeCell } from '@/app/quadratic-core-types'; -import { ArrowDropDownIcon } from '@/shared/components/Icons'; -import { Rectangle } from 'pixi.js'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { pixiApp } from '../../pixiApp/PixiApp'; - -export const TableOverlay = () => { - const setTableContextMenu = useSetRecoilState(tableHeadingAtom); - - const tableRef = useRef(null); - const hoverTableRef = useRef(null); - - const [table, setTable] = useState(undefined); - const [rect, setRect] = useState(undefined); - const [name, setName] = useState(undefined); - useEffect(() => { - const check = () => { - const cursor = sheets.sheet.cursor.cursorPosition; - let checkTable = pixiApp.cellsSheets.current?.cellsArray.getTableCursor(cursor); - setTable(checkTable); - if (checkTable) { - const sheet = sheets.sheet; - const rect = sheet.getScreenRectangle(checkTable.x, checkTable.y, checkTable.w, checkTable.h); - setRect(rect); - setName(checkTable.name); - } else { - setRect(undefined); - setName(undefined); - } - }; - events.on('cursorPosition', check); - return () => { - events.off('cursorPosition', check); - }; - }, []); - - useEffect(() => { - let tableTop = rect ? rect.y : 0; - const checkViewport = () => { - if (!rect || !tableRef.current) { - return; - } - const viewport = pixiApp.viewport; - const bounds = viewport.getVisibleBounds(); - if (!bounds.intersects(rect)) { - tableRef.current.style.display = 'none'; - return; - } else { - tableRef.current.style.display = 'block'; - } - - const headingHeight = pixiApp.headings.headingSize.height / pixiApp.viewport.scale.y; - if (rect.y < viewport.top + headingHeight) { - tableTop = rect.y + (viewport.top + headingHeight - rect.y); - tableRef.current.style.top = `${tableTop}px`; - } else { - tableRef.current.style.top = `${rect.y}px`; - } - }; - events.on('viewportChanged', checkViewport); - return () => { - events.off('viewportChanged', checkViewport); - }; - }, [rect]); - - const [hoverTable, setHoverTable] = useState(undefined); - const [hoverRect, setHoverRect] = useState(undefined); - const [hoverName, setHoverName] = useState(undefined); - useEffect(() => { - const set = (checkTable?: JsRenderCodeCell) => { - setHoverTable(checkTable); - if (checkTable) { - const sheet = sheets.sheet; - const rect = sheet.getScreenRectangle(checkTable.x, checkTable.y, checkTable.w, checkTable.h); - setHoverRect(rect); - setHoverName(checkTable.name); - } else { - setHoverRect(undefined); - setHoverName(undefined); - } - }; - events.on('hoverTable', set); - return () => { - events.off('hoverTable', set); - }; - }, [table]); - - const tableRender = useMemo(() => { - if (table && rect && name !== undefined) { - return ( -
-
{ - const world = pixiApp.viewport.toWorld(e.clientX, e.clientY); - setTableContextMenu({ world, row: table.x, column: table.y }); - e.stopPropagation(); - e.preventDefault(); - }} - className="flex text-nowrap bg-primary px-1 text-sm text-primary-foreground" - > - {name} - -
-
- ); - } - }, [name, rect, table, setTableContextMenu]); - - const hoverTableRender = useMemo(() => { - if (hoverTable && hoverRect && hoverName !== undefined) { - return ( -
-
{hoverName}
-
- ); - } - }, [hoverName, hoverRect, hoverTable]); - - useEffect(() => { - let tableTop = hoverRect ? hoverRect.y : 0; - const checkViewport = () => { - if (!hoverRect || !hoverTableRef.current) { - return; - } - const viewport = pixiApp.viewport; - const bounds = viewport.getVisibleBounds(); - if (!bounds.intersects(hoverRect)) { - hoverTableRef.current.style.display = 'none'; - return; - } else { - hoverTableRef.current.style.display = 'block'; - } - - const headingHeight = pixiApp.headings.headingSize.height / pixiApp.viewport.scale.y; - if (hoverRect.y < viewport.top + headingHeight) { - tableTop = hoverRect.y + (viewport.top + headingHeight - hoverRect.y); - hoverTableRef.current.style.top = `${tableTop}px`; - } else { - hoverTableRef.current.style.top = `${hoverRect.y}px`; - } - }; - checkViewport(); - - events.on('viewportChanged', checkViewport); - return () => { - events.off('viewportChanged', checkViewport); - }; - }, [hoverRect, hoverTable]); - - useEffect(() => { - const updateViewport = () => { - if (table && tableRef.current) { - tableRef.current.style.transform = `translateY(-100%) scale(${1 / pixiApp.viewport.scale.x})`; - } - if (hoverTable && hoverTableRef.current) { - hoverTableRef.current.style.transform = `translateY(-100%) scale(${1 / pixiApp.viewport.scale.x})`; - } - }; - events.on('viewportChanged', updateViewport); - return () => { - events.off('viewportChanged', updateViewport); - }; - }, [table, tableRef, hoverTable, hoverTableRef]); - - if (!tableRender && !hoverTableRender) return null; - - return ( - <> - {tableRender} - {hoverTable !== table && hoverTableRender} - - ); -}; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts new file mode 100644 index 0000000000..2f6e8178c4 --- /dev/null +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -0,0 +1,226 @@ +import { sheets } from '@/app/grid/controller/Sheets'; +import { Sheet } from '@/app/grid/sheet/Sheet'; +import { DROPDOWN_SIZE } from '@/app/gridGL/cells/cellsLabel/drawSpecial'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; +import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { colors } from '@/app/theme/colors'; +import { FONT_SIZE, OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; +import { BitmapText, Container, Graphics, Point, Rectangle, Sprite, Texture } from 'pixi.js'; + +interface Column { + heading: Container; + bounds: Rectangle; +} + +const DROPDOWN_PADDING = 10; + +export class Table extends Container { + private sheet: Sheet; + private codeCell: JsRenderCodeCell; + private headingHeight = 0; + private tableName: Container; + private tableNameText: BitmapText; + + // holds all headings + private headingContainer: Container; + + private outline: Graphics; + private tableBounds: Rectangle; + private tableNameBounds: Rectangle; + private headingBounds: Rectangle; + private columns: Column[]; + + constructor(sheet: Sheet, codeCell: JsRenderCodeCell) { + super(); + this.codeCell = codeCell; + this.sheet = sheet; + this.tableName = new Container(); + this.tableNameText = new BitmapText(codeCell.name, { + fontName: 'OpenSans', + fontSize: FONT_SIZE, + tint: getCSSVariableTint('primary-foreground'), + }); + this.headingContainer = new Container(); + this.outline = new Graphics(); + this.tableBounds = new Rectangle(); + this.headingBounds = new Rectangle(); + this.tableNameBounds = new Rectangle(); + this.columns = []; + this.updateCodeCell(codeCell); + } + + redraw() { + this.removeChildren(); + this.updateCodeCell(this.codeCell); + } + + updateCodeCell = (codeCell: JsRenderCodeCell) => { + this.codeCell = codeCell; + this.tableBounds = this.sheet.getScreenRectangle(codeCell.x, codeCell.y, codeCell.w - 1, codeCell.h - 1); + this.headingHeight = this.sheet.offsets.getRowHeight(codeCell.y); + this.headingBounds = new Rectangle( + this.tableBounds.x, + this.tableBounds.y, + this.tableBounds.width, + this.headingHeight + ); + this.position.set(this.headingBounds.x, this.headingBounds.y); + + this.addChild(this.headingContainer); + + // draw heading background + const background = this.headingContainer.addChild(new Graphics()); + background.beginFill(colors.tableHeadingBackground); + background.drawShape(new Rectangle(0, 0, this.headingBounds.width, this.headingBounds.height)); + background.endFill(); + + // create column headings + let x = 0; + this.columns = codeCell.column_names.map((column, index) => { + const width = this.sheet.offsets.getColumnWidth(codeCell.x + index); + const bounds = new Rectangle(x, this.headingBounds.y, width, this.headingBounds.height); + const heading = this.headingContainer.addChild(new Container()); + heading.position.set(x + OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); + heading.addChild( + new BitmapText(column.name, { + fontName: 'OpenSans-Bold', + fontSize: FONT_SIZE, + tint: colors.tableHeadingForeground, + }) + ); + + x += width; + return { heading, bounds }; + }); + + // draw outline around entire table + this.addChild(this.outline); + this.outline.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); + this.outline.drawShape(new Rectangle(0, 0, this.tableBounds.width, this.tableBounds.height)); + this.outline.visible = false; + + // draw table name + + if (sheets.sheet.id === this.sheet.id) { + pixiApp.overHeadings.addChild(this.tableName); + } + this.tableName.position.set(this.tableBounds.x, this.tableBounds.y); + const nameBackground = this.tableName.addChild(new Graphics()); + this.tableName.visible = false; + const text = this.tableName.addChild(this.tableNameText); + this.tableNameText.text = codeCell.name; + text.position.set(OPEN_SANS_FIX.x, OPEN_SANS_FIX.y - this.headingBounds.height); + + const dropdown = this.tableName.addChild(this.drawDropdown()); + dropdown.position.set(text.width + OPEN_SANS_FIX.x + DROPDOWN_PADDING, -this.headingHeight / 2); + + nameBackground.beginFill(getCSSVariableTint('primary')); + nameBackground.drawShape( + new Rectangle( + 0, + -this.headingBounds.height, + text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING, + this.headingBounds.height + ) + ); + nameBackground.endFill(); + this.tableNameBounds = new Rectangle( + this.tableBounds.x, + this.tableBounds.y - this.headingHeight, + text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING, + this.headingBounds.height + ); + }; + + private drawDropdown() { + const dropdown = new Sprite(Texture.from('/images/dropdown-white.png')); + dropdown.width = DROPDOWN_SIZE[0]; + dropdown.height = DROPDOWN_SIZE[1]; + dropdown.anchor.set(0.5); + return dropdown; + } + + private tableNamePosition = (bounds: Rectangle, gridHeading: number) => { + if (this.visible) { + if (this.tableBounds.y < bounds.top + gridHeading) { + this.tableName.y = bounds.top + gridHeading - this.tableBounds.top; + } else { + this.tableName.y = this.tableBounds.top; + } + } + }; + + private headingPosition = (bounds: Rectangle, gridHeading: number) => { + if (this.visible) { + if (this.headingBounds.top < bounds.top + gridHeading) { + this.headingContainer.y = bounds.top + gridHeading - this.headingBounds.top; + } else { + this.headingContainer.y = 0; + } + } + }; + + intersectsCursor(x: number, y: number) { + const rect = new Rectangle(this.codeCell.x, this.codeCell.y, this.codeCell.w - 1, this.codeCell.h - 1); + if (intersects.rectanglePoint(rect, { x, y }) || intersects.rectangleRectangle(rect, this.headingBounds)) { + this.showActive(); + return true; + } + return false; + } + + // Returns the table name bounds scaled to the viewport. + private getScaledTableNameBounds() { + const scaled = this.tableNameBounds.clone(); + scaled.width /= pixiApp.viewport.scaled; + scaled.height /= pixiApp.viewport.scaled; + scaled.y -= scaled.height - this.tableNameBounds.height; + return scaled; + } + + // Checks whether the mouse cursor is hovering over the table or the table name + checkHover(world: Point): boolean { + return ( + intersects.rectanglePoint(this.tableBounds, world) || + intersects.rectanglePoint(this.getScaledTableNameBounds(), world) + ); + } + + intersectsTableName(world: Point): { table: Table; nameOrDropdown: 'name' | 'dropdown' } | undefined { + if (intersects.rectanglePoint(this.getScaledTableNameBounds(), world)) { + if (world.x <= this.tableNameBounds.x + this.tableNameText.width / pixiApp.viewport.scaled) { + return { table: this, nameOrDropdown: 'name' }; + } + return { table: this, nameOrDropdown: 'dropdown' }; + } + } + + update(bounds: Rectangle, gridHeading: number) { + this.visible = intersects.rectangleRectangle(this.tableBounds, bounds); + this.headingPosition(bounds, gridHeading); + this.tableNamePosition(bounds, gridHeading); + this.tableName.scale.set(1 / pixiApp.viewport.scale.x); + } + + hideActive() { + this.outline.visible = false; + this.tableName.visible = false; + pixiApp.setViewportDirty(); + } + + showActive() { + this.outline.visible = true; + this.tableName.visible = true; + pixiApp.setViewportDirty(); + } + + addTableNames() { + pixiApp.overHeadings.addChild(this.tableName); + } + + removeTableNames() { + pixiApp.overHeadings.removeChild(this.tableName); + } +} diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 32784e7160..1259990297 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -1,39 +1,17 @@ //! Tables renders all pixi-based UI elements for tables. Right now that's the //! headings. -/* eslint-disable @typescript-eslint/no-unused-vars */ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; -import { intersects } from '@/app/gridGL/helpers/intersects'; +import { Table } from '@/app/gridGL/cells/tables/Table'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; -import { colors } from '@/app/theme/colors'; -import { FONT_SIZE, OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; -import { BitmapText, Container, Graphics, Rectangle } from 'pixi.js'; +import { Container, Point } from 'pixi.js'; -interface Column { - heading: Container; - bounds: Rectangle; -} - -interface Table { - container: Container; - headingContainer: Container; - bounds: Rectangle; - outline: Graphics; - // headingLine: Graphics; - headingBounds: Rectangle; - originalHeadingBounds: Rectangle; - columns: Column[]; - codeCell: JsRenderCodeCell; -} - -export class Tables extends Container { +export class Tables extends Container { private cellsSheet: CellsSheet; - private tables: Table[]; private activeTable: Table | undefined; private hoverTable: Table | undefined; @@ -41,14 +19,12 @@ export class Tables extends Container { constructor(cellsSheet: CellsSheet) { super(); this.cellsSheet = cellsSheet; - this.tables = []; events.on('renderCodeCells', this.renderCodeCells); // todo: update code cells? events.on('cursorPosition', this.cursorPosition); - events.on('hoverTable', this.setHoverTable); - events.on('sheetOffsets', this.sheetOffsets); + events.on('changeSheet', this.changeSheet); } get sheet(): Sheet { @@ -59,104 +35,20 @@ export class Tables extends Container { return sheet; } - cull() { - const bounds = pixiApp.viewport.getVisibleBounds(); - this.tables.forEach((heading) => { - heading.container.visible = intersects.rectangleRectangle(heading.bounds, bounds); - }); - } - - private renderCodeCell = (codeCell: JsRenderCodeCell) => { - const container = this.addChild(new Container()); - - const bounds = this.sheet.getScreenRectangle(codeCell.x, codeCell.y, codeCell.w - 1, codeCell.h - 1); - const headingHeight = this.sheet.offsets.getRowHeight(codeCell.y); - const headingBounds = new Rectangle(bounds.x, bounds.y, bounds.width, headingHeight); - const originalHeadingBounds = headingBounds.clone(); - container.position.set(headingBounds.x, headingBounds.y); - - // draw individual headings - const headingContainer = container.addChild(new Container()); - - // draw heading background - const background = headingContainer.addChild(new Graphics()); - background.beginFill(colors.tableHeadingBackground); - background.drawShape(new Rectangle(0, 0, headingBounds.width, headingBounds.height)); - background.endFill(); - - // // draw heading line - // const headingLine = headingContainer.addChild(new Graphics()); - // headingLine.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); - // headingLine.moveTo(0, headingHeight).lineTo(headingBounds.width, headingHeight); - // headingLine.visible = false; - - let x = 0; - const columns: Column[] = codeCell.column_names.map((column, index) => { - const width = this.sheet.offsets.getColumnWidth(codeCell.x + index); - const bounds = new Rectangle(x, headingBounds.y, width, headingBounds.height); - const heading = headingContainer.addChild(new Container()); - heading.position.set(x + OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); - heading.addChild( - new BitmapText(column.name, { - fontName: 'OpenSans-Bold', - fontSize: FONT_SIZE, - tint: colors.tableHeadingForeground, - }) - ); - - // // draw heading line between columns - // if (index !== codeCell.column_names.length - 1) { - // headingLine.moveTo(x + width, 0).lineTo(x + width, headingHeight); - // } - x += width; - return { heading, bounds }; - }); - - // draw outline - const outline = container.addChild(new Graphics()); - outline.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); - outline.drawShape(new Rectangle(0, 0, bounds.width, bounds.height)); - outline.visible = false; - - this.tables.push({ - container, - bounds, - headingBounds, - headingContainer, - outline, - // headingLine, - originalHeadingBounds, - columns, - codeCell, - }); - }; - private renderCodeCells = (sheetId: string, codeCells: JsRenderCodeCell[]) => { if (sheetId === this.cellsSheet.sheetId) { this.removeChildren(); - this.tables = []; - codeCells.forEach((codeCell) => this.renderCodeCell(codeCell)); + codeCells.forEach((codeCell) => this.addChild(new Table(this.sheet, codeCell))); } }; - private headingPosition = () => { - const bounds = pixiApp.viewport.getVisibleBounds(); - const gridHeading = pixiApp.headings.headingSize.height / pixiApp.viewport.scaled; - this.tables.forEach((heading) => { - if (heading.container.visible) { - if (heading.headingBounds.top < bounds.top + gridHeading) { - heading.headingContainer.y = bounds.top + gridHeading - heading.headingBounds.top; - } else { - heading.headingContainer.y = 0; - } - } - }); - }; - update(dirtyViewport: boolean) { if (dirtyViewport) { - this.cull(); - this.headingPosition(); + const bounds = pixiApp.viewport.getVisibleBounds(); + const gridHeading = pixiApp.headings.headingSize.height / pixiApp.viewport.scale.y; + this.children.forEach((heading) => { + heading.update(bounds, gridHeading); + }); } } @@ -166,52 +58,51 @@ export class Tables extends Container { return; } if (this.activeTable) { - this.activeTable.outline.visible = false; - // this.activeTable.headingLine.visible = false; - pixiApp.setViewportDirty(); + this.activeTable.hideActive(); } const cursor = sheets.sheet.cursor.cursorPosition; - this.activeTable = this.tables.find((table) => { - const rect = new Rectangle(table.codeCell.x, table.codeCell.y, table.codeCell.w - 1, table.codeCell.h - 1); - return intersects.rectanglePoint(rect, cursor); - }); - if (this.activeTable) { - this.activeTable.outline.visible = true; - // this.activeTable.headingLine.visible = true; - pixiApp.setViewportDirty(); + this.activeTable = this.children.find((table) => table.intersectsCursor(cursor.x, cursor.y)); + }; + + // Redraw the headings if the offsets change. + sheetOffsets = (sheetId: string) => { + if (sheetId === this.sheet.id) { + this.children.map((table) => table.redraw()); } }; - private setHoverTable = (codeCell?: JsRenderCodeCell) => { - if (this.sheet.id !== sheets.sheet.id) { - return; + private changeSheet = (sheetId: string) => { + if (sheetId === this.sheet.id) { + this.children.forEach((table) => { + table.addTableNames(); + }); + } else { + this.children.forEach((table) => { + table.removeTableNames(); + }); } - if (!codeCell) { + }; + + // Checks if the mouse cursor is hovering over a table or table heading. + checkHover(world: Point) { + const hover = this.children.find((table) => table.checkHover(world)); + if (hover !== this.hoverTable) { if (this.hoverTable) { - if (this.hoverTable !== this.activeTable) { - this.hoverTable.outline.visible = false; - // this.hoverTable.headingLine.visible = false; - pixiApp.setViewportDirty(); - } - this.hoverTable = undefined; + this.hoverTable.hideActive(); + } + this.hoverTable = hover; + if (this.hoverTable) { + this.hoverTable.showActive(); } - return; - } - this.hoverTable = this.tables.find((table) => table.codeCell.x === codeCell.x && table.codeCell.y === codeCell.y); - if (this.hoverTable) { - this.hoverTable.outline.visible = true; - // this.hoverTable.headingLine.visible = true; - pixiApp.setViewportDirty(); } - }; + } - // Redraw the headings if the offsets change. - sheetOffsets = (sheetId: string) => { - if (sheetId === this.sheet.id) { - this.renderCodeCells( - sheetId, - this.tables.map((table) => table.codeCell) - ); + pointerDown(world: Point): { table: Table; nameOrDropdown: 'name' | 'dropdown' } | undefined { + for (const table of this.children) { + const result = table.intersectsTableName(world); + if (result) { + return result; + } } - }; + } } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts index a994355760..b887ddf266 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts @@ -2,6 +2,7 @@ import { PointerAutoComplete } from '@/app/gridGL/interaction/pointer/PointerAut import { PointerCellMoving } from '@/app/gridGL/interaction/pointer/PointerCellMoving'; import { PointerCursor } from '@/app/gridGL/interaction/pointer/pointerCursor'; import { PointerDown } from '@/app/gridGL/interaction/pointer/PointerDown'; +import { PointerDownTable } from '@/app/gridGL/interaction/pointer/PointerDownTable'; import { PointerHeading } from '@/app/gridGL/interaction/pointer/PointerHeading'; import { PointerHtmlCells } from '@/app/gridGL/interaction/pointer/PointerHtmlCells'; import { PointerImages } from '@/app/gridGL/interaction/pointer/PointerImages'; @@ -20,6 +21,7 @@ export class Pointer { private pointerCursor: PointerCursor; pointerDown: PointerDown; pointerCellMoving: PointerCellMoving; + private pointerDownTable: PointerDownTable; private pointerLink: PointerLink; constructor(viewport: Viewport) { @@ -30,6 +32,7 @@ export class Pointer { this.pointerCursor = new PointerCursor(); this.pointerHtmlCells = new PointerHtmlCells(); this.pointerCellMoving = new PointerCellMoving(); + this.pointerDownTable = new PointerDownTable(); this.pointerLink = new PointerLink(); viewport.on('pointerdown', this.handlePointerDown); @@ -100,6 +103,7 @@ export class Pointer { this.pointerHeading.pointerDown(world, event) || this.pointerLink.pointerDown(world, event) || this.pointerAutoComplete.pointerDown(world) || + this.pointerDownTable.pointerDown(world, event) || this.pointerDown.pointerDown(world, event); this.updateCursor(); @@ -124,7 +128,7 @@ export class Pointer { this.pointerHeading.pointerMove(world) || this.pointerAutoComplete.pointerMove(world) || this.pointerDown.pointerMove(world, event) || - this.pointerCursor.pointerMove(world, event) || + this.pointerCursor.pointerMove(world) || this.pointerLink.pointerMove(world, event); this.updateCursor(); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts index f38c3117f8..febab830bb 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts @@ -1,3 +1,4 @@ +import { ContextMenuType } from '@/app/atoms/contextMenuAtoms'; import { PanMode } from '@/app/atoms/gridPanModeAtom'; import { events } from '@/app/events/events'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; @@ -65,9 +66,9 @@ export class PointerDown { }); // hack to ensure that the context menu opens after the cursor changes // position (otherwise it may close immediately) - setTimeout(() => events.emit('gridContextMenu', world, column, row)); + setTimeout(() => events.emit('contextMenu', ContextMenuType.Grid, world, column, row)); } else { - events.emit('gridContextMenu', world, column, row); + events.emit('contextMenu', ContextMenuType.Grid, world, column, row); } return; } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDownTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDownTable.ts new file mode 100644 index 0000000000..a76cb8e50b --- /dev/null +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDownTable.ts @@ -0,0 +1,24 @@ +import { ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { events } from '@/app/events/events'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { isMac } from '@/shared/utils/isMac'; +import { Point } from 'pixi.js'; + +export class PointerDownTable { + pointerDown(world: Point, event: PointerEvent): boolean { + const result = pixiApp.cellsSheets.current?.tables.pointerDown(world); + if (!result) { + return false; + } + if (event.button === 2 || (isMac && event.button === 0 && event.ctrlKey)) { + events.emit('contextMenu', ContextMenuType.Table, world, null, null); + } + + if (result.nameOrDropdown === 'name') { + // todo: dragging and/or renaming on double click + } else if (result.nameOrDropdown === 'dropdown') { + events.emit('contextMenu', ContextMenuType.Table, world, null, null); + } + return true; + } +} diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts index 8cd064b461..d0d086e9de 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts @@ -1,3 +1,4 @@ +import { ContextMenuType } from '@/app/atoms/contextMenuAtoms'; import { PanMode } from '@/app/atoms/gridPanModeAtom'; import { events } from '@/app/events/events'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; @@ -146,7 +147,7 @@ export class PointerHeading { } else { if (columns.includes(column)) { if (isRightClick) { - events.emit('gridContextMenu', world, column, null); + events.emit('contextMenu', ContextMenuType.Grid, world, column, null); } else { selectColumns( columns.filter((c) => c !== column), @@ -158,7 +159,7 @@ export class PointerHeading { if (isRightClick) { selectColumns([column], undefined, true); // need the timeout to allow the cursor events to complete - setTimeout(() => events.emit('gridContextMenu', world, column, null)); + setTimeout(() => events.emit('contextMenu', ContextMenuType.Grid, world, column, null)); } else { selectColumns([...columns, column], undefined, true); } @@ -181,7 +182,7 @@ export class PointerHeading { } else { if (rows.includes(row)) { if (isRightClick) { - events.emit('gridContextMenu', world, null, row); + events.emit('contextMenu', ContextMenuType.Grid, world, null, row); } else { selectRows( rows.filter((c) => c !== row), @@ -193,7 +194,7 @@ export class PointerHeading { if (isRightClick) { selectRows([row], undefined, true); // need the timeout to allow the cursor events to complete - setTimeout(() => events.emit('gridContextMenu', world, null, row)); + setTimeout(() => events.emit('contextMenu', ContextMenuType.Grid, world, null, row)); } else { selectRows([...rows, row], undefined, true); } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts b/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts index e36484ac07..27d45819e8 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts @@ -6,13 +6,11 @@ import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { Point } from 'pixi.js'; -import { intersects } from '../../helpers/intersects'; export class PointerCursor { private lastInfo?: JsRenderCodeCell | EditingCell | ErrorValidation; - private lastTable?: JsRenderCodeCell; - private checkHoverCell(world: Point, event: PointerEvent) { + private checkHoverCell(world: Point) { if (!pixiApp.cellsSheets.current) throw new Error('Expected cellsSheets.current to be defined in PointerCursor'); const cell = sheets.sheet.getColumnRow(world.x, world.y); const editingCell = multiplayer.cellIsBeingEdited(cell.x, cell.y, sheets.sheet.id); @@ -33,23 +31,7 @@ export class PointerCursor { foundCodeCell = true; } - let foundTable = false; - const table = pixiApp.cellsSheets.current.cellsArray.getCodeCellWorld(world); - if (table) { - if (this.lastTable?.x !== table.x || this.lastTable?.y !== table.y) { - events.emit('hoverTable', table); - this.lastTable = table; - } - foundTable = true; - } else if (this.lastTable) { - const tablesHeading = document.querySelector('.tables-overlay'); - if (tablesHeading) { - const rect = tablesHeading.getBoundingClientRect(); - if (intersects.rectanglePoint(rect, { x: event.clientX, y: event.clientY })) { - foundTable = true; - } - } - } + pixiApp.cellsSheets.current.tables.checkHover(world); let foundValidation = false; const validation = pixiApp.cellsSheets.current.cellsLabels.intersectsErrorMarkerValidation(world); @@ -65,17 +47,12 @@ export class PointerCursor { events.emit('hoverCell'); this.lastInfo = undefined; } - - if (!foundTable && this.lastTable) { - events.emit('hoverTable'); - this.lastTable = undefined; - } } - pointerMove(world: Point, event: PointerEvent): void { + pointerMove(world: Point): void { const cursor = pixiApp.pointer.pointerHeading.cursor ?? pixiApp.pointer.pointerAutoComplete.cursor; pixiApp.canvas.style.cursor = cursor ?? 'unset'; multiplayer.sendMouseMove(world.x, world.y); - this.checkHoverCell(world, event); + this.checkHoverCell(world); } } diff --git a/quadratic-client/src/app/gridGL/loadAssets.ts b/quadratic-client/src/app/gridGL/loadAssets.ts index 00fb380e86..fd2487895d 100644 --- a/quadratic-client/src/app/gridGL/loadAssets.ts +++ b/quadratic-client/src/app/gridGL/loadAssets.ts @@ -48,6 +48,7 @@ export function loadAssets(): Promise { addResourceOnce('checkbox-icon', '/images/checkbox.png'); addResourceOnce('checkbox-checked-icon', '/images/checkbox-checked.png'); addResourceOnce('dropdown-icon', '/images/dropdown.png'); + addResourceOnce('dropdown-white-icon', '/images/dropdown-white.png'); // Wait until pixi fonts are loaded before resolving Loader.shared.load(() => { diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index b8ed6d1ba9..a3828210ee 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -54,6 +54,10 @@ export class PixiApp { cursor!: Cursor; cellHighlights!: CellHighlights; multiplayerCursor!: UIMultiPlayerCursor; + + // this is used to display content over the headings (eg, table name when off + // the screen) + overHeadings: Container; cellMoving!: UICellMoving; headings!: GridHeadings; boxCells!: BoxCells; @@ -84,6 +88,7 @@ export class PixiApp { this.cellsSheets = new CellsSheets(); this.cellImages = new UICellImages(); this.validations = new UIValidations(); + this.overHeadings = new Container(); this.viewport = new Viewport(); } @@ -156,6 +161,7 @@ export class PixiApp { this.cellMoving = this.viewportContents.addChild(new UICellMoving()); this.validations = this.viewportContents.addChild(this.validations); this.headings = this.viewportContents.addChild(new GridHeadings()); + this.viewportContents.addChild(this.overHeadings); this.reset(); diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 95a3adb67d..881dce2740 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -136,7 +136,6 @@ pub(crate) fn import_data_table_builder( let mut new_data_tables = IndexMap::new(); for (pos, data_table) in data_tables.into_iter() { - dbgjs!(format!("data_table: {:?}", &data_table)); let value = match data_table.value { current::OutputValueSchema::Single(value) => { Value::Single(import_cell_value(value.to_owned())) @@ -320,7 +319,6 @@ pub(crate) fn export_data_tables( data_tables .into_iter() .map(|(pos, data_table)| { - dbgjs!(format!("data_table: {:?}", &data_table)); let value = match data_table.value { Value::Single(cell_value) => { current::OutputValueSchema::Single(export_cell_value(cell_value)) diff --git a/quadratic-core/src/grid/sheet/col_row/row.rs b/quadratic-core/src/grid/sheet/col_row/row.rs index 39e2542d36..1ad897d59a 100644 --- a/quadratic-core/src/grid/sheet/col_row/row.rs +++ b/quadratic-core/src/grid/sheet/col_row/row.rs @@ -440,7 +440,6 @@ impl Sheet { row: i64, copy_formats: CopyFormats, ) { - dbgjs!("insert_row()"); // create undo operations for the inserted column if transaction.is_user_undo_redo() { // reverse operation to delete the row (this will also shift all impacted rows) From 9103e5766071311c3ef379fee283b1b35a9f429b Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 17 Oct 2024 08:27:47 -0700 Subject: [PATCH 041/373] fixing actions --- quadratic-client/src/app/actions/actions.ts | 5 +- .../src/app/actions/actionsSpec.ts | 1 + .../src/app/actions/dataTableSpec.ts | 79 +++++------ .../src/app/atoms/contextMenuAtoms.ts | 29 ++-- quadratic-client/src/app/events/events.ts | 8 +- .../src/app/grid/sheet/SheetCursor.ts | 5 + .../app/gridGL/HTMLGrid/HTMLGridContainer.tsx | 4 +- .../app/gridGL/HTMLGrid/TableContextMenu.tsx | 131 ------------------ .../{ => contextMenus}/GridContextMenu.tsx | 60 +------- .../contextMenus/TableContextMenu.tsx | 56 ++++++++ .../HTMLGrid/contextMenus/contextMenu.tsx | 71 ++++++++++ .../src/app/gridGL/PixiAppEffects.tsx | 6 + .../src/app/gridGL/cells/tables/Table.ts | 6 +- .../src/app/gridGL/cells/tables/Tables.ts | 2 +- .../app/gridGL/interaction/pointer/Pointer.ts | 8 +- .../gridGL/interaction/pointer/PointerDown.ts | 4 +- .../interaction/pointer/PointerHeading.ts | 8 +- .../{PointerDownTable.ts => PointerTable.ts} | 6 +- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 9 ++ .../src/app/quadratic-core-types/index.d.ts | 2 +- .../web-workers/quadraticCore/worker/core.ts | 2 +- .../src/shared/components/Icons.tsx | 4 + .../execute_operation/execute_data_table.rs | 4 +- quadratic-core/src/grid/js_types.rs | 1 + quadratic-core/src/grid/sheet/rendering.rs | 3 + .../wasm_bindings/controller/data_table.rs | 2 +- 26 files changed, 250 insertions(+), 266 deletions(-) delete mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/TableContextMenu.tsx rename quadratic-client/src/app/gridGL/HTMLGrid/{ => contextMenus}/GridContextMenu.tsx (55%) create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx rename quadratic-client/src/app/gridGL/interaction/pointer/{PointerDownTable.ts => PointerTable.ts} (76%) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 887dff89c5..c2d229f6d4 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -142,8 +142,5 @@ export enum Action { FlattenDataTable = 'flatten_data_table', GridToDataTable = 'grid_to_data_table', - SortDataTableFirstColAsc = 'sort_data_table_first_col_asc', - SortDataTableFirstColDesc = 'sort_data_table_first_col_desc', - AddFirstRowAsHeaderDataTable = 'add_first_row_as_header_data_table', - RemoveFirstRowAsHeaderDataTable = 'remove_first_row_as_header_data_table', + ToggleFirstRowAsHeaderDataTable = 'toggle_first_row_as_header_data_table', } diff --git a/quadratic-client/src/app/actions/actionsSpec.ts b/quadratic-client/src/app/actions/actionsSpec.ts index 53ebe887ab..c3f981140d 100644 --- a/quadratic-client/src/app/actions/actionsSpec.ts +++ b/quadratic-client/src/app/actions/actionsSpec.ts @@ -40,6 +40,7 @@ export type ActionSpec = { // return `` // ``` Icon?: IconComponent; + checkbox?: boolean | (() => boolean); isAvailable?: (args: ActionAvailabilityArgs) => boolean; // Used for command palette search keywords?: string[]; diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index aea6640937..e451bb9ece 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -1,18 +1,14 @@ import { Action } from '@/app/actions/actions'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { PersonAddIcon } from '@/shared/components/Icons'; import { sheets } from '../grid/controller/Sheets'; import { ActionSpecRecord } from './actionsSpec'; -import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; type DataTableSpec = Pick< ActionSpecRecord, - | Action.FlattenDataTable - | Action.GridToDataTable - | Action.SortDataTableFirstColAsc - | Action.SortDataTableFirstColDesc - | Action.AddFirstRowAsHeaderDataTable - | Action.RemoveFirstRowAsHeaderDataTable + Action.FlattenDataTable | Action.GridToDataTable | Action.ToggleFirstRowAsHeaderDataTable >; export type DataTableActionArgs = { @@ -23,58 +19,55 @@ const isDataTable = (): boolean => { return pixiApp.isCursorOnCodeCellOutput(); }; +const isFirstRowHeader = (): boolean => { + console.log(pixiAppSettings.contextMenu.table); + return !!pixiAppSettings.contextMenu.table?.first_row_header; +}; + export const dataTableSpec: DataTableSpec = { [Action.FlattenDataTable]: { - label: 'Flatten Data Table', + label: 'Flatten data table', Icon: PersonAddIcon, - isAvailable: () => isDataTable(), run: async () => { const { x, y } = sheets.sheet.cursor.cursorPosition; quadraticCore.flattenDataTable(sheets.sheet.id, x, y, sheets.getCursorPosition()); }, }, [Action.GridToDataTable]: { - label: 'Convert to Data Table', + label: 'Convert values to data table', Icon: PersonAddIcon, isAvailable: () => !isDataTable(), run: async () => { quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); }, }, - [Action.SortDataTableFirstColAsc]: { - label: 'Sort Data Table - First Column Ascending', - Icon: PersonAddIcon, - isAvailable: () => isDataTable(), + // [Action.SortDataTableFirstColAsc]: { + // label: 'Sort Data Table - First Column Ascending', + // Icon: PersonAddIcon, + // isAvailable: () => isDataTable(), + // run: async () => { + // const { x, y } = sheets.sheet.cursor.cursorPosition; + // quadraticCore.sortDataTable(sheets.sheet.id, x, y, 0, 'asc', sheets.getCursorPosition()); + // }, + // }, + // [Action.SortDataTableFirstColDesc]: { + // label: 'Sort Data Table - First Column Descending', + // Icon: PersonAddIcon, + // isAvailable: () => isDataTable(), + // run: async () => { + // const { x, y } = sheets.sheet.cursor.cursorPosition; + // quadraticCore.sortDataTable(sheets.sheet.id, x, y, 0, 'desc', sheets.getCursorPosition()); + // }, + // }, + [Action.ToggleFirstRowAsHeaderDataTable]: { + label: 'First row as column headings', + checkbox: isFirstRowHeader, run: async () => { - const { x, y } = sheets.sheet.cursor.cursorPosition; - quadraticCore.sortDataTable(sheets.sheet.id, x, y, 0, 'asc', sheets.getCursorPosition()); - }, - }, - [Action.SortDataTableFirstColDesc]: { - label: 'Sort Data Table - First Column Descending', - Icon: PersonAddIcon, - isAvailable: () => isDataTable(), - run: async () => { - const { x, y } = sheets.sheet.cursor.cursorPosition; - quadraticCore.sortDataTable(sheets.sheet.id, x, y, 0, 'desc', sheets.getCursorPosition()); - }, - }, - [Action.AddFirstRowAsHeaderDataTable]: { - label: 'Add First Row as Header', - Icon: PersonAddIcon, - isAvailable: () => isDataTable(), - run: async () => { - const { x, y } = sheets.sheet.cursor.cursorPosition; - quadraticCore.dataTableFirstRowAsHeader(sheets.sheet.id, x, y, true, sheets.getCursorPosition()); - }, - }, - [Action.RemoveFirstRowAsHeaderDataTable]: { - label: 'Remove First Row as Header', - Icon: PersonAddIcon, - isAvailable: () => isDataTable(), - run: async () => { - const { x, y } = sheets.sheet.cursor.cursorPosition; - quadraticCore.dataTableFirstRowAsHeader(sheets.sheet.id, x, y, false, sheets.getCursorPosition()); + const table = pixiAppSettings.contextMenu?.table; + if (table) { + console.log(table, 'remove first row to header'); + quadraticCore.dataTableFirstRowAsHeader(sheets.sheet.id, table.x, table.y, true, sheets.getCursorPosition()); + } }, }, }; diff --git a/quadratic-client/src/app/atoms/contextMenuAtoms.ts b/quadratic-client/src/app/atoms/contextMenuAtoms.ts index 2f6e657289..e8f306b724 100644 --- a/quadratic-client/src/app/atoms/contextMenuAtoms.ts +++ b/quadratic-client/src/app/atoms/contextMenuAtoms.ts @@ -1,4 +1,5 @@ import { events } from '@/app/events/events'; +import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { Point } from 'pixi.js'; import { atom } from 'recoil'; @@ -7,30 +8,40 @@ export enum ContextMenuType { Table = 'table', } -interface ContextMenu { +export interface ContextMenuState { type?: ContextMenuType; world?: Point; - column: number | null; - row: number | null; + column?: number; + row?: number; + table?: JsRenderCodeCell; } -const defaultContextMenuState: ContextMenu = { +export const defaultContextMenuState: ContextMenuState = { world: undefined, - column: null, - row: null, + column: undefined, + row: undefined, + table: undefined, }; +export interface ContextMenuOptions { + type?: ContextMenuType; + world?: Point; + column?: number; + row?: number; + table?: JsRenderCodeCell; +} + export const contextMenuAtom = atom({ key: 'contextMenuState', default: defaultContextMenuState, effects: [ ({ setSelf }) => { const clear = () => { - setSelf(() => ({ type: undefined, world: undefined, column: null, row: null })); + setSelf(() => ({})); }; - const set = (type: ContextMenuType, world: Point, column: number | null, row: number | null) => { - setSelf(() => ({ type, world, column, row })); + const set = (options: ContextMenuOptions) => { + setSelf(() => options); }; events.on('cursorPosition', clear); diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 82bf3db8bb..e3a71cee50 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -126,7 +126,13 @@ interface EventTypes { validation: (validation: string | boolean) => void; // trigger or clear a context menu - contextMenu: (type: ContextMenuType, world: Point, row: number | null, column: number | null) => void; + contextMenu: (options: { + type?: ContextMenuType; + world?: Point; + row?: number; + column?: number; + table?: JsRenderCodeCell; + }) => void; suggestionDropdownKeyboard: (key: 'ArrowDown' | 'ArrowUp' | 'Enter' | 'Escape' | 'Tab') => void; diff --git a/quadratic-client/src/app/grid/sheet/SheetCursor.ts b/quadratic-client/src/app/grid/sheet/SheetCursor.ts index 3b1f5d95f9..74b8c46b48 100644 --- a/quadratic-client/src/app/grid/sheet/SheetCursor.ts +++ b/quadratic-client/src/app/grid/sheet/SheetCursor.ts @@ -294,4 +294,9 @@ export class SheetCursor { onlySingleSelection(): boolean { return !this.multiCursor?.length && !this.columnRow; } + + // Returns true if there is one multiselect of > 1 size + hasOneMultiselect(): boolean { + return this.multiCursor?.length === 1 && (this.multiCursor[0].width > 1 || this.multiCursor[0].height > 1); + } } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index 02e230bfaf..9521bb9aab 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -2,14 +2,14 @@ import { events } from '@/app/events/events'; import { Annotations } from '@/app/gridGL/HTMLGrid/annotations/Annotations'; import { CodeHint } from '@/app/gridGL/HTMLGrid/CodeHint'; import { CodeRunning } from '@/app/gridGL/HTMLGrid/codeRunning/CodeRunning'; -import { GridContextMenu } from '@/app/gridGL/HTMLGrid/GridContextMenu'; +import { GridContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/GridContextMenu'; import { HoverCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; import { HoverTooltip } from '@/app/gridGL/HTMLGrid/hoverTooltip/HoverTooltip'; import { HtmlCells } from '@/app/gridGL/HTMLGrid/htmlCells/HtmlCells'; import { InlineEditor } from '@/app/gridGL/HTMLGrid/inlineEditor/InlineEditor'; import { MultiplayerCursors } from '@/app/gridGL/HTMLGrid/multiplayerCursor/MultiplayerCursors'; import { MultiplayerCellEdits } from '@/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdits'; -import { TableContextMenu } from '@/app/gridGL/HTMLGrid/TableContextMenu'; +import { TableContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableContextMenu'; import { useHeadingSize } from '@/app/gridGL/HTMLGrid/useHeadingSize'; import { HtmlValidations } from '@/app/gridGL/HTMLGrid/validations/HtmlValidations'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/TableContextMenu.tsx deleted file mode 100644 index cfec335429..0000000000 --- a/quadratic-client/src/app/gridGL/HTMLGrid/TableContextMenu.tsx +++ /dev/null @@ -1,131 +0,0 @@ -//! This shows the table context menu. - -import { Action } from '@/app/actions/actions'; -import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; -import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtoms'; -import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { focusGrid } from '@/app/helpers/focusGrid'; -import { keyboardShortcutEnumToDisplay } from '@/app/helpers/keyboardShortcutsDisplay'; -import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; -import { IconComponent } from '@/shared/components/Icons'; -import { ControlledMenu, MenuItem } from '@szhsin/react-menu'; -import { useCallback, useEffect, useRef } from 'react'; -import { useRecoilState } from 'recoil'; - -export const TableContextMenu = () => { - const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); - - const onClose = useCallback(() => { - setContextMenu({ type: undefined, world: undefined, column: null, row: null }); - focusGrid(); - }, [setContextMenu]); - - useEffect(() => { - pixiApp.viewport.on('moved', onClose); - pixiApp.viewport.on('zoomed', onClose); - - return () => { - pixiApp.viewport.off('moved', onClose); - pixiApp.viewport.off('zoomed', onClose); - }; - }, [onClose]); - - const ref = useRef(null); - - return ( -
- - - {/* - - - - - - - {contextMenu.column === null ? null : ( - <> - - {isColumnRowAvailable && } - {isColumnRowAvailable && } - - - )} - - {contextMenu.row === null ? null : ( - <> - {isColumnRowAvailable && } - {isColumnRowAvailable && } - {isColumnRowAvailable && } - - - )} - - - - - - - - */} - -
- ); -}; - -function MenuItemAction({ action }: { action: Action }) { - const { label, Icon, run, isAvailable } = defaultActionSpec[action]; - const isAvailableArgs = useIsAvailableArgs(); - const keyboardShortcut = keyboardShortcutEnumToDisplay(action); - - if (isAvailable && !isAvailable(isAvailableArgs)) { - return null; - } - - return ( - - {label} - - ); -} - -function MenuItemShadStyle({ - children, - Icon, - onClick, - keyboardShortcut, -}: { - children: string; - Icon?: IconComponent; - onClick: any; - keyboardShortcut?: string; -}) { - const menuItemClassName = - 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; - return ( - - - {Icon && } {children} - - {keyboardShortcut && ( - {keyboardShortcut} - )} - - ); -} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx similarity index 55% rename from quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx rename to quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index 80a03f168a..3fc41cc895 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -1,23 +1,20 @@ //! This shows the grid heading context menu. import { Action } from '@/app/actions/actions'; -import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtoms'; import { sheets } from '@/app/grid/controller/Sheets'; +import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { focusGrid } from '@/app/helpers/focusGrid'; -import { keyboardShortcutEnumToDisplay } from '@/app/helpers/keyboardShortcutsDisplay'; -import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; -import { IconComponent } from '@/shared/components/Icons'; -import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu'; +import { ControlledMenu, MenuDivider } from '@szhsin/react-menu'; import { useCallback, useEffect, useRef } from 'react'; import { useRecoilState } from 'recoil'; -import { pixiApp } from '../pixiApp/PixiApp'; +import { pixiApp } from '../../pixiApp/PixiApp'; export const GridContextMenu = () => { const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { - setContextMenu({ type: undefined, world: undefined, column: null, row: null }); + setContextMenu({}); focusGrid(); }, [setContextMenu]); @@ -34,6 +31,7 @@ export const GridContextMenu = () => { const ref = useRef(null); const isColumnRowAvailable = sheets.sheet.cursor.hasOneColumnRowSelection(true); + const isMultiSelectOnly = sheets.sheet.cursor.hasOneMultiselect(); return (
{ )} - - - - - - + {isMultiSelectOnly && }
); }; - -function MenuItemAction({ action }: { action: Action }) { - const { label, Icon, run, isAvailable } = defaultActionSpec[action]; - const isAvailableArgs = useIsAvailableArgs(); - const keyboardShortcut = keyboardShortcutEnumToDisplay(action); - - if (isAvailable && !isAvailable(isAvailableArgs)) { - return null; - } - - return ( - - {label} - - ); -} - -function MenuItemShadStyle({ - children, - Icon, - onClick, - keyboardShortcut, -}: { - children: string; - Icon?: IconComponent; - onClick: any; - keyboardShortcut?: string; -}) { - const menuItemClassName = - 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; - return ( - - - {Icon && } {children} - - {keyboardShortcut && ( - {keyboardShortcut} - )} - - ); -} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx new file mode 100644 index 0000000000..87971074cb --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -0,0 +1,56 @@ +//! This shows the table context menu. + +import { Action } from '@/app/actions/actions'; +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { focusGrid } from '@/app/helpers/focusGrid'; +import { ControlledMenu } from '@szhsin/react-menu'; +import { useCallback, useEffect, useRef } from 'react'; +import { useRecoilState } from 'recoil'; + +export const TableContextMenu = () => { + const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); + + const onClose = useCallback(() => { + setContextMenu({}); + focusGrid(); + }, [setContextMenu]); + + useEffect(() => { + pixiApp.viewport.on('moved', onClose); + pixiApp.viewport.on('zoomed', onClose); + + return () => { + pixiApp.viewport.off('moved', onClose); + pixiApp.viewport.off('zoomed', onClose); + }; + }, [onClose]); + + const ref = useRef(null); + console.log(contextMenu); + return ( +
+ + + + +
+ ); +}; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx new file mode 100644 index 0000000000..bc75a71fa8 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx @@ -0,0 +1,71 @@ +import { Action } from '@/app/actions/actions'; +import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; +import { keyboardShortcutEnumToDisplay } from '@/app/helpers/keyboardShortcutsDisplay'; +import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; +import { CheckBoxEmptyIcon, CheckBoxIcon, IconComponent } from '@/shared/components/Icons'; +import { MenuItem } from '@szhsin/react-menu'; + +interface Props { + action: Action; +} + +export const MenuItemAction = (props: Props): JSX.Element | null => { + const { label, Icon, run, isAvailable, checkbox } = defaultActionSpec[props.action]; + const isAvailableArgs = useIsAvailableArgs(); + const keyboardShortcut = keyboardShortcutEnumToDisplay(props.action); + + if (isAvailable && !isAvailable(isAvailableArgs)) { + return null; + } + + return ( + + {label} + + ); +}; + +function MenuItemShadStyle({ + children, + Icon, + checkbox, + onClick, + keyboardShortcut, +}: { + children: string; + Icon?: IconComponent; + onClick: any; + checkbox?: boolean | (() => boolean); + keyboardShortcut?: string; +}) { + const menuItemClassName = + 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; + + const icon = Icon ? : null; + let checkboxElement: JSX.Element | null = null; + if (!Icon && checkbox !== undefined) { + let checked: boolean; + if (typeof checkbox === 'function') { + checked = checkbox(); + } else { + checked = checkbox === true; + } + console.log(checked); + if (checked) { + checkboxElement = ; + } else { + checkboxElement = ; + } + } + return ( + + + {icon} + {checkboxElement} {children} + + {keyboardShortcut && ( + {keyboardShortcut} + )} + + ); +} diff --git a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx index 467252013b..4d4ac40937 100644 --- a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx +++ b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx @@ -1,4 +1,5 @@ import { codeEditorAtom, codeEditorShowCodeEditorAtom } from '@/app/atoms/codeEditorAtom'; +import { contextMenuAtom } from '@/app/atoms/contextMenuAtoms'; import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; import { gridPanModeAtom } from '@/app/atoms/gridPanModeAtom'; import { gridSettingsAtom, presentationModeAtom, showHeadingsAtom } from '@/app/atoms/gridSettingsAtom'; @@ -58,6 +59,11 @@ export const PixiAppEffects = () => { pixiAppSettings.updateGridPanMode(gridPanMode, setGridPanMode); }, [gridPanMode, setGridPanMode]); + const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); + useEffect(() => { + pixiAppSettings.updateContextMenu(contextMenu, setContextMenu); + }, [contextMenu, setContextMenu]); + useEffect(() => { const handleMouseUp = () => { setGridPanMode((prev) => ({ ...prev, mouseIsDown: false })); diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 2f6e8178c4..18b8e65133 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -188,12 +188,12 @@ export class Table extends Container { ); } - intersectsTableName(world: Point): { table: Table; nameOrDropdown: 'name' | 'dropdown' } | undefined { + intersectsTableName(world: Point): { table: JsRenderCodeCell; nameOrDropdown: 'name' | 'dropdown' } | undefined { if (intersects.rectanglePoint(this.getScaledTableNameBounds(), world)) { if (world.x <= this.tableNameBounds.x + this.tableNameText.width / pixiApp.viewport.scaled) { - return { table: this, nameOrDropdown: 'name' }; + return { table: this.codeCell, nameOrDropdown: 'name' }; } - return { table: this, nameOrDropdown: 'dropdown' }; + return { table: this.codeCell, nameOrDropdown: 'dropdown' }; } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 1259990297..53e0f5cf11 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -97,7 +97,7 @@ export class Tables extends Container
{ } } - pointerDown(world: Point): { table: Table; nameOrDropdown: 'name' | 'dropdown' } | undefined { + pointerDown(world: Point): { table: JsRenderCodeCell; nameOrDropdown: 'name' | 'dropdown' } | undefined { for (const table of this.children) { const result = table.intersectsTableName(world); if (result) { diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts index b887ddf266..eecfbee4a0 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts @@ -2,11 +2,11 @@ import { PointerAutoComplete } from '@/app/gridGL/interaction/pointer/PointerAut import { PointerCellMoving } from '@/app/gridGL/interaction/pointer/PointerCellMoving'; import { PointerCursor } from '@/app/gridGL/interaction/pointer/pointerCursor'; import { PointerDown } from '@/app/gridGL/interaction/pointer/PointerDown'; -import { PointerDownTable } from '@/app/gridGL/interaction/pointer/PointerDownTable'; import { PointerHeading } from '@/app/gridGL/interaction/pointer/PointerHeading'; import { PointerHtmlCells } from '@/app/gridGL/interaction/pointer/PointerHtmlCells'; import { PointerImages } from '@/app/gridGL/interaction/pointer/PointerImages'; import { PointerLink } from '@/app/gridGL/interaction/pointer/PointerLink'; +import { PointerTable } from '@/app/gridGL/interaction/pointer/PointerTable'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; @@ -21,7 +21,7 @@ export class Pointer { private pointerCursor: PointerCursor; pointerDown: PointerDown; pointerCellMoving: PointerCellMoving; - private pointerDownTable: PointerDownTable; + private pointerTable: PointerTable; private pointerLink: PointerLink; constructor(viewport: Viewport) { @@ -32,7 +32,7 @@ export class Pointer { this.pointerCursor = new PointerCursor(); this.pointerHtmlCells = new PointerHtmlCells(); this.pointerCellMoving = new PointerCellMoving(); - this.pointerDownTable = new PointerDownTable(); + this.pointerTable = new PointerTable(); this.pointerLink = new PointerLink(); viewport.on('pointerdown', this.handlePointerDown); @@ -103,7 +103,7 @@ export class Pointer { this.pointerHeading.pointerDown(world, event) || this.pointerLink.pointerDown(world, event) || this.pointerAutoComplete.pointerDown(world) || - this.pointerDownTable.pointerDown(world, event) || + this.pointerTable.pointerDown(world, event) || this.pointerDown.pointerDown(world, event); this.updateCursor(); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts index febab830bb..bcf4e3d3f6 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts @@ -66,9 +66,9 @@ export class PointerDown { }); // hack to ensure that the context menu opens after the cursor changes // position (otherwise it may close immediately) - setTimeout(() => events.emit('contextMenu', ContextMenuType.Grid, world, column, row)); + setTimeout(() => events.emit('contextMenu', { type: ContextMenuType.Grid, world, column, row })); } else { - events.emit('contextMenu', ContextMenuType.Grid, world, column, row); + events.emit('contextMenu', { type: ContextMenuType.Grid, world, column, row }); } return; } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts index d0d086e9de..576aab4d49 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts @@ -147,7 +147,7 @@ export class PointerHeading { } else { if (columns.includes(column)) { if (isRightClick) { - events.emit('contextMenu', ContextMenuType.Grid, world, column, null); + events.emit('contextMenu', { type: ContextMenuType.Grid, world, column }); } else { selectColumns( columns.filter((c) => c !== column), @@ -159,7 +159,7 @@ export class PointerHeading { if (isRightClick) { selectColumns([column], undefined, true); // need the timeout to allow the cursor events to complete - setTimeout(() => events.emit('contextMenu', ContextMenuType.Grid, world, column, null)); + setTimeout(() => events.emit('contextMenu', { type: ContextMenuType.Grid, world, column })); } else { selectColumns([...columns, column], undefined, true); } @@ -182,7 +182,7 @@ export class PointerHeading { } else { if (rows.includes(row)) { if (isRightClick) { - events.emit('contextMenu', ContextMenuType.Grid, world, null, row); + events.emit('contextMenu', { type: ContextMenuType.Grid, world, row }); } else { selectRows( rows.filter((c) => c !== row), @@ -194,7 +194,7 @@ export class PointerHeading { if (isRightClick) { selectRows([row], undefined, true); // need the timeout to allow the cursor events to complete - setTimeout(() => events.emit('contextMenu', ContextMenuType.Grid, world, null, row)); + setTimeout(() => events.emit('contextMenu', { type: ContextMenuType.Grid, world, row })); } else { selectRows([...rows, row], undefined, true); } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDownTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts similarity index 76% rename from quadratic-client/src/app/gridGL/interaction/pointer/PointerDownTable.ts rename to quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index a76cb8e50b..d25f10be0b 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDownTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -4,20 +4,20 @@ import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { isMac } from '@/shared/utils/isMac'; import { Point } from 'pixi.js'; -export class PointerDownTable { +export class PointerTable { pointerDown(world: Point, event: PointerEvent): boolean { const result = pixiApp.cellsSheets.current?.tables.pointerDown(world); if (!result) { return false; } if (event.button === 2 || (isMac && event.button === 0 && event.ctrlKey)) { - events.emit('contextMenu', ContextMenuType.Table, world, null, null); + events.emit('contextMenu', { type: ContextMenuType.Table, world, table: result.table }); } if (result.nameOrDropdown === 'name') { // todo: dragging and/or renaming on double click } else if (result.nameOrDropdown === 'dropdown') { - events.emit('contextMenu', ContextMenuType.Table, world, null, null); + events.emit('contextMenu', { type: ContextMenuType.Table, world, table: result.table }); } return true; } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index e9cc48067c..1abc287df7 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -1,4 +1,5 @@ import { CodeEditorState, defaultCodeEditorState } from '@/app/atoms/codeEditorAtom'; +import { ContextMenuOptions, ContextMenuState, defaultContextMenuState } from '@/app/atoms/contextMenuAtoms'; import { EditorInteractionState, editorInteractionStateDefault } from '@/app/atoms/editorInteractionStateAtom'; import { defaultGridPanMode, GridPanMode, PanMode } from '@/app/atoms/gridPanModeAtom'; import { defaultGridSettings, GridSettings } from '@/app/atoms/gridSettingsAtom'; @@ -51,6 +52,9 @@ class PixiAppSettings { codeEditorState = defaultCodeEditorState; setCodeEditorState?: SetterOrUpdater; + contextMenu = defaultContextMenuState; + setContextMenu?: SetterOrUpdater; + constructor() { const settings = localStorage.getItem('viewSettings'); if (settings) { @@ -214,6 +218,11 @@ class PixiAppSettings { get panMode() { return this._panMode; } + + updateContextMenu(contextMenu: ContextMenuState, setContextMenu: SetterOrUpdater) { + this.contextMenu = contextMenu; + this.setContextMenu = setContextMenu; + } } export const pixiAppSettings = new PixiAppSettings(); diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index a5a6a8d216..bfb2909e89 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -37,7 +37,7 @@ export interface JsOffset { column: number | null, row: number | null, size: num export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableHeading"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, } +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index c41ce8545a..aa71464e2f 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1132,7 +1132,7 @@ class Core { dataTableFirstRowAsHeader(sheetId: string, x: number, y: number, firstRowAsHeader: boolean, cursor: string) { if (!this.gridController) throw new Error('Expected gridController to be defined'); - this.gridController.dataTablefirstRowAsHeader(sheetId, posToPos(x, y), firstRowAsHeader, cursor); + this.gridController.dataTableFirstRowAsHeader(sheetId, posToPos(x, y), firstRowAsHeader, cursor); } } diff --git a/quadratic-client/src/shared/components/Icons.tsx b/quadratic-client/src/shared/components/Icons.tsx index 5b28239d40..350b456148 100644 --- a/quadratic-client/src/shared/components/Icons.tsx +++ b/quadratic-client/src/shared/components/Icons.tsx @@ -96,6 +96,10 @@ export const BorderColorIcon: IconComponent = (props) => { return border_color; }; +export const CheckBoxEmptyIcon: IconComponent = (props) => { + return check_box_outline_blank; +}; + export const CheckBoxIcon: IconComponent = (props) => { return check_box; }; diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 739704663c..36c987253d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -12,7 +12,7 @@ use crate::{ use anyhow::{bail, Result}; impl GridController { - pub fn send_to_wasm( + fn send_to_wasm( &mut self, transaction: &mut PendingTransaction, sheet_rect: &SheetRect, @@ -38,7 +38,7 @@ impl GridController { Ok(()) } - pub fn data_table_operations( + fn data_table_operations( &mut self, transaction: &mut PendingTransaction, sheet_rect: &SheetRect, diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index a2a20ee2f3..09010cee01 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -194,6 +194,7 @@ pub struct JsRenderCodeCell { pub spill_error: Option>, pub name: String, pub column_names: Vec, + pub first_row_header: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index f666c660b8..470f9dae67 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -444,6 +444,7 @@ impl Sheet { spill_error, name: data_table.name.clone(), column_names: data_table.send_columns(), + first_row_header: data_table.has_header, }) } @@ -488,6 +489,7 @@ impl Sheet { spill_error, name: data_table.name.clone(), column_names: data_table.send_columns(), + first_row_header: data_table.has_header, }) } _ => None, // this should not happen. A CodeRun should always have a CellValue::Code. @@ -1116,6 +1118,7 @@ mod tests { display: true, value_index: 0, }], + first_row_header: false, }) ); } diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index 007192cb67..023979a060 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -51,7 +51,7 @@ impl GridController { } /// Toggle applin the first row as head - #[wasm_bindgen(js_name = "dataTablefirstRowAsHeader")] + #[wasm_bindgen(js_name = "dataTableFirstRowAsHeader")] pub fn js_data_table_first_row_as_header( &mut self, sheet_id: String, From 2d896f17aad107ca8c80b6140f13ea61bf42c85f Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 17 Oct 2024 09:28:16 -0600 Subject: [PATCH 042/373] Cleanup --- .../src/controller/execution/run_code/run_javascript.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/run_javascript.rs b/quadratic-core/src/controller/execution/run_code/run_javascript.rs index 82154a4420..f43c991fb0 100644 --- a/quadratic-core/src/controller/execution/run_code/run_javascript.rs +++ b/quadratic-core/src/controller/execution/run_code/run_javascript.rs @@ -298,7 +298,7 @@ mod tests { #[test] #[parallel] - fn test_python_array_output_variable_length() { + fn test_javascript_array_output_variable_length() { let mut gc = GridController::test(); let sheet_id = gc.sheet_ids()[0]; @@ -309,7 +309,7 @@ mod tests { y: 0, sheet_id, }, - CodeCellLanguage::Python, + CodeCellLanguage::Javascript, "create an array output".into(), None, ); @@ -334,10 +334,11 @@ mod tests { let sheet = gc.try_sheet(sheet_id).unwrap(); let cells = sheet.get_render_cells(Rect::from_numbers(0, 0, 1, 3)); + // println!("{:?}", cells); assert_eq!(cells.len(), 3); assert_eq!( cells[0], - JsRenderCell::new_number(0, 0, 1, Some(CodeCellLanguage::Python)) + JsRenderCell::new_number(0, 0, 1, Some(CodeCellLanguage::Javascript)) ); assert_eq!(cells[1], JsRenderCell::new_number(0, 1, 2, None)); assert_eq!(cells[2], JsRenderCell::new_number(0, 2, 3, None)); From 9561a7cc61a25965a998b1dbbcf1aac48a7bf192 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 17 Oct 2024 09:02:03 -0700 Subject: [PATCH 043/373] working through y-value for rows w/and w/out headers --- .../src/app/actions/dataTableSpec.ts | 10 ++-- .../contextMenus/TableContextMenu.tsx | 7 +-- .../HTMLGrid/contextMenus/contextMenu.tsx | 1 - .../src/app/gridGL/cells/tables/Table.ts | 2 +- .../pending_transaction.rs | 4 +- .../src/controller/operations/import.rs | 2 +- quadratic-core/src/grid/data_table.rs | 53 ++++++++++++++----- .../src/grid/file/serialize/data_table.rs | 6 ++- quadratic-core/src/grid/file/v1_7/file.rs | 3 +- quadratic-core/src/grid/file/v1_8/schema.rs | 3 +- quadratic-core/src/grid/js_types.rs | 1 + quadratic-core/src/grid/sheet/rendering.rs | 10 ++-- quadratic-rust-shared/src/auto_gen_path.rs | 4 +- 13 files changed, 72 insertions(+), 34 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index e451bb9ece..b0691e3fd4 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -20,7 +20,6 @@ const isDataTable = (): boolean => { }; const isFirstRowHeader = (): boolean => { - console.log(pixiAppSettings.contextMenu.table); return !!pixiAppSettings.contextMenu.table?.first_row_header; }; @@ -65,8 +64,13 @@ export const dataTableSpec: DataTableSpec = { run: async () => { const table = pixiAppSettings.contextMenu?.table; if (table) { - console.log(table, 'remove first row to header'); - quadraticCore.dataTableFirstRowAsHeader(sheets.sheet.id, table.x, table.y, true, sheets.getCursorPosition()); + quadraticCore.dataTableFirstRowAsHeader( + sheets.sheet.id, + table.x, + table.y, + !isFirstRowHeader(), + sheets.getCursorPosition() + ); } }, }, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 87971074cb..37a538983d 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -5,7 +5,7 @@ import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtoms'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { focusGrid } from '@/app/helpers/focusGrid'; -import { ControlledMenu } from '@szhsin/react-menu'; +import { ControlledMenu, MenuDivider } from '@szhsin/react-menu'; import { useCallback, useEffect, useRef } from 'react'; import { useRecoilState } from 'recoil'; @@ -28,7 +28,7 @@ export const TableContextMenu = () => { }, [onClose]); const ref = useRef(null); - console.log(contextMenu); + return (
{ menuStyle={{ padding: '0', color: 'inherit' }} menuClassName="bg-background" > - + +
); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx index bc75a71fa8..62b9ecb817 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx @@ -50,7 +50,6 @@ function MenuItemShadStyle({ } else { checked = checkbox === true; } - console.log(checked); if (checked) { checkboxElement = ; } else { diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 18b8e65133..47af93a816 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -102,7 +102,7 @@ export class Table extends Container { this.outline.visible = false; // draw table name - + console.log(codeCell.name); if (sheets.sheet.id === this.sheet.id) { pixiApp.overHeadings.addChild(this.tableName); } diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index cc83f95bdf..343a1156ee 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -12,7 +12,9 @@ use crate::{ controller::{ execution::TransactionType, operations::operation::Operation, transaction::Transaction, }, - grid::{sheet::validations::validation::Validation, CodeCellLanguage, DataTable, SheetId}, + grid::{ + sheet::validations::validation::Validation, CodeCellLanguage, DataTable, Sheet, SheetId, + }, selection::Selection, Pos, SheetPos, SheetRect, }; diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 58837a6d8d..e8da1c1bba 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -387,7 +387,7 @@ impl GridController { .ok_or_else(|| anyhow!("Sheet {sheet_id} not found"))?; let sheet_pos = SheetPos::from((insert_at, sheet_id)); let mut data_table = DataTable::from((import.to_owned(), cell_values, sheet)); - data_table.has_header = true; + data_table.header_is_first_row = true; // this operation must be before the SetCodeRun operations ops.push(Operation::SetCellValues { diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index a0713ed53f..df011de397 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -60,7 +60,8 @@ pub struct DataTableSortOrder { pub struct DataTable { pub kind: DataTableKind, pub name: String, - pub has_header: bool, + pub header_is_first_row: bool, + pub show_header: bool, pub columns: Option>, pub sort: Option>, pub display_buffer: Option>, @@ -103,7 +104,8 @@ impl DataTable { let data_table = DataTable { kind, name: name.into(), - has_header, + header_is_first_row: has_header, + show_header: true, columns: None, sort: None, display_buffer: None, @@ -120,11 +122,12 @@ impl DataTable { data_table } - /// Direcly creates a new DataTable with the given kind, value, spill_error, and columns. + /// Directly creates a new DataTable with the given kind, value, spill_error, and columns. pub fn new_raw( kind: DataTableKind, name: &str, - has_header: bool, + header_is_first_row: bool, + show_header: bool, columns: Option>, sort: Option>, display_buffer: Option>, @@ -135,7 +138,8 @@ impl DataTable { DataTable { kind, name: name.into(), - has_header, + header_is_first_row, + show_header, columns, sort, display_buffer, @@ -154,7 +158,7 @@ impl DataTable { /// Takes the first row of the array and sets it as the column headings. pub fn apply_first_row_as_header(&mut self) { - self.has_header = true; + self.header_is_first_row = true; self.columns = match self.value { // Value::Array(ref mut array) => array.shift().ok().map(|array| { @@ -170,7 +174,7 @@ impl DataTable { } pub fn toggle_first_row_as_header(&mut self, first_row_as_header: bool) { - self.has_header = first_row_as_header; + self.header_is_first_row = first_row_as_header; match first_row_as_header { true => self.apply_first_row_as_header(), @@ -247,7 +251,7 @@ impl DataTable { pub fn sort(&mut self, column_index: usize, direction: SortDirection) -> Result<()> { let values = self.value.clone().into_array()?; - let increment = |i| if self.has_header { i + 1 } else { i }; + let increment = |i| if self.header_is_first_row { i + 1 } else { i }; let mut display_buffer = values .col(column_index) @@ -260,7 +264,7 @@ impl DataTable { .map(|(i, _)| increment(i) as u64) .collect::>(); - if self.has_header { + if self.header_is_first_row { display_buffer.insert(0, 0); } @@ -292,6 +296,14 @@ impl DataTable { display_buffer: &Vec, pos: Pos, ) -> Result<&CellValue> { + let pos = if self.show_header && !self.header_is_first_row { + Pos { + x: pos.x, + y: pos.y + 1, + } + } else { + pos + }; let y = display_buffer .get(pos.y as usize) .ok_or_else(|| anyhow!("Y {} out of bounds: {}", pos.y, display_buffer.len()))?; @@ -308,13 +320,21 @@ impl DataTable { } pub fn display_value_at(&self, pos: Pos) -> Result<&CellValue> { + let pos = if self.show_header && !self.header_is_first_row { + Pos { + x: pos.x, + y: pos.y + 1, + } + } else { + pos + }; match self.display_buffer { Some(ref display_buffer) => self.display_value_from_buffer_at(display_buffer, pos), None => Ok(self.value.get(pos.x as u32, pos.y as u32)?), } } - /// Helper functtion to get the CodeRun from the DataTable. + /// Helper function to get the CodeRun from the DataTable. /// Returns `None` if the DataTableKind is not CodeRun. pub fn code_run(&self) -> Option<&CodeRun> { match self.kind { @@ -323,7 +343,7 @@ impl DataTable { } } - /// Helper functtion to deterime if the DataTable's CodeRun has an error. + /// Helper function to determine if the DataTable's CodeRun has an error. /// Returns `false` if the DataTableKind is not CodeRun or if there is no error. pub fn has_error(&self) -> bool { match self.kind { @@ -332,7 +352,7 @@ impl DataTable { } } - /// Helper functtion to get the error in the CodeRun from the DataTable. + /// Helper function to get the error in the CodeRun from the DataTable. /// Returns `None` if the DataTableKind is not CodeRun or if there is no error. pub fn get_error(&self) -> Option { self.code_run() @@ -342,6 +362,11 @@ impl DataTable { /// Returns the output value of a code run at the relative location (ie, (0,0) is the top of the code run result). /// A spill or error returns [`CellValue::Blank`]. Note: this assumes a [`CellValue::Code`] exists at the location. pub fn cell_value_at(&self, x: u32, y: u32) -> Option { + let y = if self.show_header && !self.header_is_first_row { + y + 1 + } else { + y + }; if self.spill_error { Some(CellValue::Blank) } else { @@ -456,7 +481,7 @@ impl DataTable { for (index, row) in array.rows().take(max).enumerate() { let row = row.iter().map(|s| s.to_string()).collect::>(); - if index == 0 && data_table.has_header { + if index == 0 && data_table.header_is_first_row { builder.set_header(row); } else { builder.push_record(row); @@ -467,7 +492,7 @@ impl DataTable { table.with(Style::modern()); // bold the headers if they exist - if data_table.has_header { + if data_table.header_is_first_row { [0..table.count_columns()] .iter() .enumerate() diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 881dce2740..f855dd052c 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -164,7 +164,8 @@ pub(crate) fn import_data_table_builder( } }, name: data_table.name, - has_header: data_table.has_header, + header_is_first_row: data_table.header_is_first_row, + show_header: data_table.show_header, readonly: data_table.readonly, last_modified: data_table.last_modified.unwrap_or(Utc::now()), // this is required but fall back to now if failed spill_error: data_table.spill_error, @@ -380,7 +381,8 @@ pub(crate) fn export_data_tables( let data_table = current::DataTableSchema { kind, name: data_table.name, - has_header: data_table.has_header, + header_is_first_row: data_table.header_is_first_row, + show_header: data_table.show_header, columns, sort, display_buffer: data_table.display_buffer, diff --git a/quadratic-core/src/grid/file/v1_7/file.rs b/quadratic-core/src/grid/file/v1_7/file.rs index a549ad29d4..f0dd5aa37d 100644 --- a/quadratic-core/src/grid/file/v1_7/file.rs +++ b/quadratic-core/src/grid/file/v1_7/file.rs @@ -48,7 +48,8 @@ fn upgrade_code_runs( let new_data_table = v1_8::DataTableSchema { kind: v1_8::DataTableKindSchema::CodeRun(new_code_run), name: format!("Table {}", i), - has_header: false, + header_is_first_row: false, + show_header: false, columns: None, sort: None, display_buffer: None, diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index c16e1d7e5d..29798a8bfa 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -122,7 +122,8 @@ pub struct DataTableSortOrderSchema { pub struct DataTableSchema { pub kind: DataTableKindSchema, pub name: String, - pub has_header: bool, + pub header_is_first_row: bool, + pub show_header: bool, pub columns: Option>, pub sort: Option>, pub display_buffer: Option>, diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index 09010cee01..6538f395e2 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -195,6 +195,7 @@ pub struct JsRenderCodeCell { pub name: String, pub column_names: Vec, pub first_row_header: bool, + pub show_header: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 470f9dae67..a9517fbb12 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -213,8 +213,7 @@ impl Sheet { let column = self.get_column(x); for y in y_start..=y_end { // We skip rendering the heading row because we render it separately. - // todo: we should not skip if headings are hidden - if y == code_rect.min.y { + if y == code_rect.min.y && data_table.show_header { continue; } let value = data_table.cell_value_at( @@ -444,7 +443,8 @@ impl Sheet { spill_error, name: data_table.name.clone(), column_names: data_table.send_columns(), - first_row_header: data_table.has_header, + first_row_header: data_table.header_is_first_row, + show_header: data_table.show_header, }) } @@ -489,7 +489,8 @@ impl Sheet { spill_error, name: data_table.name.clone(), column_names: data_table.send_columns(), - first_row_header: data_table.has_header, + first_row_header: data_table.header_is_first_row, + show_header: data_table.show_header, }) } _ => None, // this should not happen. A CodeRun should always have a CellValue::Code. @@ -1119,6 +1120,7 @@ mod tests { value_index: 0, }], first_row_header: false, + show_header: true, }) ); } diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index a0dea21ae2..8df3d2b4d5 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From 71f826b6d0dfb7637c2c1a236415b3602bcc1820 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 17 Oct 2024 09:43:07 -0700 Subject: [PATCH 044/373] remove unnecessary y code --- quadratic-core/src/grid/data_table.rs | 47 ++++++++++++---------- quadratic-core/src/grid/sheet/rendering.rs | 5 ++- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index df011de397..86da878964 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -296,14 +296,6 @@ impl DataTable { display_buffer: &Vec, pos: Pos, ) -> Result<&CellValue> { - let pos = if self.show_header && !self.header_is_first_row { - Pos { - x: pos.x, - y: pos.y + 1, - } - } else { - pos - }; let y = display_buffer .get(pos.y as usize) .ok_or_else(|| anyhow!("Y {} out of bounds: {}", pos.y, display_buffer.len()))?; @@ -320,14 +312,6 @@ impl DataTable { } pub fn display_value_at(&self, pos: Pos) -> Result<&CellValue> { - let pos = if self.show_header && !self.header_is_first_row { - Pos { - x: pos.x, - y: pos.y + 1, - } - } else { - pos - }; match self.display_buffer { Some(ref display_buffer) => self.display_value_from_buffer_at(display_buffer, pos), None => Ok(self.value.get(pos.x as u32, pos.y as u32)?), @@ -362,11 +346,6 @@ impl DataTable { /// Returns the output value of a code run at the relative location (ie, (0,0) is the top of the code run result). /// A spill or error returns [`CellValue::Blank`]. Note: this assumes a [`CellValue::Code`] exists at the location. pub fn cell_value_at(&self, x: u32, y: u32) -> Option { - let y = if self.show_header && !self.header_is_first_row { - y + 1 - } else { - y - }; if self.spill_error { Some(CellValue::Blank) } else { @@ -752,4 +731,30 @@ pub mod test { SheetRect::from_numbers(1, 2, 10, 11, sheet_id) ); } + + #[test] + #[parallel] + fn test_headers_y() { + let mut sheet = Sheet::test(); + let array = Array::from_str_vec(vec![vec!["first", "second"]], true).unwrap(); + let t = DataTable { + kind: DataTableKind::Import(Import::new("test.csv".to_string())), + name: "Table 1".into(), + columns: None, + sort: None, + display_buffer: None, + value: Value::Array(array), + readonly: false, + spill_error: false, + last_modified: Utc::now(), + show_header: true, + header_is_first_row: true, + }; + sheet.set_cell_value( + Pos { x: 1, y: 1 }, + Some(CellValue::Import(Import::new("test.csv".to_string()))), + ); + sheet.set_data_table(Pos { x: 1, y: 1 }, Some(t)); + assert_eq!(sheet.display_value(Pos { x: 1, y: 1 }), None); + } } diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index a9517fbb12..23e4a66bbb 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -213,7 +213,10 @@ impl Sheet { let column = self.get_column(x); for y in y_start..=y_end { // We skip rendering the heading row because we render it separately. - if y == code_rect.min.y && data_table.show_header { + if y == code_rect.min.y + && data_table.show_header + && data_table.header_is_first_row + { continue; } let value = data_table.cell_value_at( From 9c431c9816479edd8f4b4554dfda882f33b21155 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 17 Oct 2024 10:11:52 -0700 Subject: [PATCH 045/373] adding show_table --- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../pending_transaction.rs | 2 ++ quadratic-core/src/controller/dependencies.rs | 1 + .../execution/control_transaction.rs | 4 +++ .../execute_operation/execute_formats.rs | 1 + .../src/controller/execution/run_code/mod.rs | 9 +++++++ .../execution/run_code/run_formula.rs | 7 ++++-- .../src/controller/execution/spills.rs | 1 + quadratic-core/src/grid/data_table.rs | 25 +++++++++++++------ quadratic-core/src/grid/sheet.rs | 4 +-- quadratic-core/src/grid/sheet/code.rs | 4 +++ quadratic-core/src/grid/sheet/data_table.rs | 4 ++- quadratic-core/src/grid/sheet/rendering.rs | 5 ++++ quadratic-core/src/grid/sheet/search.rs | 2 ++ quadratic-core/src/grid/sheet/sheet_test.rs | 3 +++ 15 files changed, 61 insertions(+), 13 deletions(-) diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index bfb2909e89..66926a74ba 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -37,7 +37,7 @@ export interface JsOffset { column: number | null, row: number | null, size: num export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableHeading"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, } +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, show_header: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index 343a1156ee..295732b1ce 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -505,6 +505,7 @@ mod tests { Value::Single(CellValue::Html("html".to_string())), false, false, + false, ); transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); @@ -528,6 +529,7 @@ mod tests { Value::Single(CellValue::Image("image".to_string())), false, false, + false, ); transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); diff --git a/quadratic-core/src/controller/dependencies.rs b/quadratic-core/src/controller/dependencies.rs index be114ce26e..6ad2b91f38 100644 --- a/quadratic-core/src/controller/dependencies.rs +++ b/quadratic-core/src/controller/dependencies.rs @@ -83,6 +83,7 @@ mod test { Value::Single(CellValue::Text("test".to_string())), false, false, + true, )), ); let sheet_pos_02 = SheetPos { diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index 79960156d5..d9891adb55 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -300,12 +300,16 @@ impl GridController { }; let sheet = self.try_sheet_result(current_sheet_pos.sheet_id)?; + + // todo: this should be true sometimes... + let show_header = false; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), &sheet.next_data_table_name(), value, false, false, + show_header, ); self.finalize_code_run(&mut transaction, current_sheet_pos, Some(data_table), None); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs index a4d76d7814..0a0c0f0b4d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs @@ -218,6 +218,7 @@ mod test { Value::Single(CellValue::Image("image".to_string())), false, false, + true, )), ); diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 23177d8cd7..b9bbdf107e 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -232,6 +232,7 @@ impl GridController { Value::Single(CellValue::Blank), false, false, + false, ); transaction.waiting_for_async = None; self.finalize_code_run(transaction, sheet_pos, Some(new_data_table), None); @@ -264,12 +265,15 @@ impl GridController { std_err: None, cells_accessed: transaction.cells_accessed.clone(), }; + // todo: this should be true sometimes... + let show_header = false; return DataTable::new( DataTableKind::CodeRun(code_run), "Table 1", Value::Single(CellValue::Blank), // TODO(ddimaria): this will eventually be an empty vec false, false, + show_header, ); }; @@ -330,12 +334,15 @@ impl GridController { cells_accessed: transaction.cells_accessed.clone(), }; + // todo: this should be true sometimes... + let show_header = false; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), "Table 1", value, false, false, + show_header, ); transaction.cells_accessed.clear(); data_table @@ -394,6 +401,7 @@ mod test { Value::Single(CellValue::Text("delete me".to_string())), false, false, + true, ); gc.finalize_code_run(transaction, sheet_pos, Some(new_data_table.clone()), None); assert_eq!(transaction.forward_operations.len(), 1); @@ -428,6 +436,7 @@ mod test { Value::Single(CellValue::Text("replace me".to_string())), false, false, + true, ); gc.finalize_code_run(transaction, sheet_pos, Some(new_data_table.clone()), None); assert_eq!(transaction.forward_operations.len(), 1); diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index c9d693d3a3..e13248aeaa 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -38,6 +38,7 @@ impl GridController { output.inner, false, false, + false, ); self.finalize_code_run(transaction, sheet_pos, Some(new_data_table), None); } @@ -267,7 +268,8 @@ mod test { "Table 1", Value::Single(CellValue::Number(12.into())), false, - false + false, + true ) .with_last_modified(result.last_modified), ); @@ -338,7 +340,8 @@ mod test { "Table 1", Value::Array(array), false, - false + false, + true ) .with_last_modified(result.last_modified), ); diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index f91b04f03a..3a8dd28191 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -441,6 +441,7 @@ mod tests { Value::Array(Array::from(vec![vec!["1"]])), false, false, + true, ); let pos = Pos { x: 0, y: 0 }; let sheet = gc.sheet_mut(sheet_id); diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 86da878964..ae1abd4f8b 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -81,6 +81,7 @@ impl From<(Import, Array, &Sheet)> for DataTable { Value::Array(cell_values), false, false, + true, ) } } @@ -94,7 +95,8 @@ impl DataTable { name: &str, value: Value, spill_error: bool, - has_header: bool, + header_is_first_row: bool, + show_header: bool, ) -> Self { let readonly = match kind { DataTableKind::CodeRun(_) => true, @@ -104,8 +106,8 @@ impl DataTable { let data_table = DataTable { kind, name: name.into(), - header_is_first_row: has_header, - show_header: true, + header_is_first_row, + show_header, columns: None, sort: None, display_buffer: None, @@ -559,7 +561,7 @@ pub mod test { let expected_values = Value::Array(values.clone().into()); let expected_data_table = - DataTable::new(kind.clone(), "Table 1", expected_values, false, false) + DataTable::new(kind.clone(), "Table 1", expected_values, false, false, true) .with_last_modified(data_table.last_modified); let expected_array_size = ArraySize::new(4, 4).unwrap(); assert_eq!(data_table, expected_data_table); @@ -577,7 +579,7 @@ pub mod test { // test column headings taken from first row let value = Value::Array(values.clone().into()); - let mut data_table = DataTable::new(kind.clone(), "Table 1", value, false, true) + let mut data_table = DataTable::new(kind.clone(), "Table 1", value, false, true, true) .with_last_modified(data_table.last_modified); data_table.apply_first_row_as_header(); @@ -648,6 +650,7 @@ pub mod test { Value::Single(CellValue::Number(1.into())), false, false, + true, ); assert_eq!(data_table.output_size(), ArraySize::_1X1); @@ -680,6 +683,7 @@ pub mod test { Value::Array(Array::new_empty(ArraySize::new(10, 11).unwrap())), false, false, + true, ); assert_eq!(data_table.output_size().w.get(), 10); @@ -717,6 +721,7 @@ pub mod test { Value::Array(Array::new_empty(ArraySize::new(10, 11).unwrap())), true, false, + true, ); let sheet_pos = SheetPos::from((1, 2, sheet_id)); @@ -737,7 +742,7 @@ pub mod test { fn test_headers_y() { let mut sheet = Sheet::test(); let array = Array::from_str_vec(vec![vec!["first", "second"]], true).unwrap(); - let t = DataTable { + let mut t = DataTable { kind: DataTableKind::Import(Import::new("test.csv".to_string())), name: "Table 1".into(), columns: None, @@ -754,7 +759,13 @@ pub mod test { Pos { x: 1, y: 1 }, Some(CellValue::Import(Import::new("test.csv".to_string()))), ); - sheet.set_data_table(Pos { x: 1, y: 1 }, Some(t)); + sheet.set_data_table(Pos { x: 1, y: 1 }, Some(t.clone())); + assert_eq!( + sheet.display_value(Pos { x: 1, y: 1 }), + Some(CellValue::Text("first".into())) + ); + + t.header_is_first_row = false; assert_eq!(sheet.display_value(Pos { x: 1, y: 1 }), None); } } diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 780de02662..f0563214c3 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -223,10 +223,10 @@ impl Sheet { .get_column(pos.x) .and_then(|column| column.values.get(&pos.y)); - // if CellValue::Code, then we need to get the value from data_tables + // if CellValue::Code or CellValue::Import, then we need to get the value from data_tables if let Some(cell_value) = cell_value { match cell_value { - CellValue::Code(_) => self + CellValue::Code(_) | CellValue::Import(_) => self .data_tables .get(&pos) .and_then(|run| run.cell_value_at(0, 0)), diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index b654f3eaff..6e6b44373e 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -285,6 +285,7 @@ mod test { Value::Single(CellValue::Number(BigDecimal::from(2))), false, false, + true, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!( @@ -324,6 +325,7 @@ mod test { Value::Array(Array::from(vec![vec!["1", "2", "3"]])), false, false, + true, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!( @@ -416,6 +418,7 @@ mod test { Value::Array(Array::from(vec![vec!["1"], vec!["2"], vec!["3"]])), false, false, + true, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); sheet.set_data_table(Pos { x: 1, y: 1 }, Some(data_table.clone())); @@ -452,6 +455,7 @@ mod test { Value::Array(Array::from(vec![vec!["1", "2", "3'"]])), false, false, + true, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); sheet.set_data_table(Pos { x: 1, y: 1 }, Some(data_table.clone())); diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index f74c63a5c2..93523a5f2d 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -16,7 +16,7 @@ impl Sheet { header: bool, ) -> Option { let name = self.next_data_table_name(); - let data_table = DataTable::new(kind, &name, value, spill_error, header); + let data_table = DataTable::new(kind, &name, value, spill_error, header, true); self.set_data_table(pos, Some(data_table)) } @@ -119,6 +119,7 @@ mod test { Value::Single(CellValue::Number(BigDecimal::from(2))), false, false, + true, ); let old = sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!(old, None); @@ -149,6 +150,7 @@ mod test { Value::Single(CellValue::Number(BigDecimal::from(2))), false, false, + true, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!( diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 23e4a66bbb..3b49ec9746 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -704,6 +704,7 @@ mod tests { Value::Single(CellValue::Text("hello".to_string())), false, false, + true, )), ); assert!(sheet.has_render_cells(rect)); @@ -922,6 +923,7 @@ mod tests { Value::Array(vec![vec!["1", "2", "3"], vec!["4", "5", "6"]].into()), false, false, + false, ); // render rect is larger than code rect @@ -977,6 +979,7 @@ mod tests { Value::Single(CellValue::Number(1.into())), false, false, + false, ); let code_cells = sheet.get_code_cells( &code_cell, @@ -1102,6 +1105,7 @@ mod tests { Value::Single(CellValue::Number(2.into())), false, false, + false, ); sheet.set_data_table(pos, Some(data_table)); sheet.set_cell_value(pos, code); @@ -1160,6 +1164,7 @@ mod tests { Value::Single(CellValue::Image(image.clone())), false, false, + false, ); sheet.set_data_table(pos, Some(data_table)); sheet.set_cell_value(pos, code); diff --git a/quadratic-core/src/grid/sheet/search.rs b/quadratic-core/src/grid/sheet/search.rs index 6c21c7eb59..cb134029e2 100644 --- a/quadratic-core/src/grid/sheet/search.rs +++ b/quadratic-core/src/grid/sheet/search.rs @@ -511,6 +511,7 @@ mod test { Value::Single("world".into()), false, false, + false, ); sheet.set_data_table(Pos { x: 1, y: 2 }, Some(data_table)); @@ -558,6 +559,7 @@ mod test { ])), false, false, + false, ); sheet.set_data_table(Pos { x: 1, y: 2 }, Some(data_table)); diff --git a/quadratic-core/src/grid/sheet/sheet_test.rs b/quadratic-core/src/grid/sheet/sheet_test.rs index 2fff9d6e48..6c18eb7be7 100644 --- a/quadratic-core/src/grid/sheet/sheet_test.rs +++ b/quadratic-core/src/grid/sheet/sheet_test.rs @@ -75,6 +75,7 @@ impl Sheet { Value::Single(value), false, false, + false, )), ); } @@ -133,6 +134,7 @@ impl Sheet { Value::Array(array), false, false, + true, )), ); } @@ -178,6 +180,7 @@ impl Sheet { Value::Array(array), false, false, + true, )), ); } From f538f3354b31654719f2a288c34526d3ca492b96 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 17 Oct 2024 10:19:45 -0700 Subject: [PATCH 046/373] clean up tests --- .../src/controller/execution/run_code/run_formula.rs | 4 ++-- quadratic-core/src/grid/sheet/rendering.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index e13248aeaa..2e397c1adf 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -269,7 +269,7 @@ mod test { Value::Single(CellValue::Number(12.into())), false, false, - true + false ) .with_last_modified(result.last_modified), ); @@ -341,7 +341,7 @@ mod test { Value::Array(array), false, false, - true + false ) .with_last_modified(result.last_modified), ); diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 3b49ec9746..6a094e44ec 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -229,7 +229,7 @@ impl Sheet { } else { None }; - let special = if y == code_rect.min.y { + let special = if y == code_rect.min.y && data_table.show_header { Some(JsRenderCellSpecial::TableHeading) } else { None From b3ec8378e635ac1cda25853046aeb3e82ae8c2dd Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 17 Oct 2024 12:39:07 -0600 Subject: [PATCH 047/373] Sort operations, start multisoft --- .../pending_transaction.rs | 6 +- .../execute_operation/execute_data_table.rs | 32 ++++--- .../execution/execute_operation/mod.rs | 1 + .../src/controller/user_actions/import.rs | 4 - quadratic-core/src/grid/data_table.rs | 96 +++++++++++++++---- .../src/grid/file/serialize/data_table.rs | 6 +- quadratic-core/src/grid/file/v1_8/schema.rs | 1 + 7 files changed, 106 insertions(+), 40 deletions(-) diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index cc83f95bdf..b3157a4080 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -12,7 +12,9 @@ use crate::{ controller::{ execution::TransactionType, operations::operation::Operation, transaction::Transaction, }, - grid::{sheet::validations::validation::Validation, CodeCellLanguage, DataTable, SheetId}, + grid::{ + sheet::validations::validation::Validation, CodeCellLanguage, DataTable, Sheet, SheetId, + }, selection::Selection, Pos, SheetPos, SheetRect, }; @@ -358,7 +360,7 @@ impl PendingTransaction { mod tests { use crate::{ controller::operations::operation::Operation, - grid::{CodeRun, DataTableKind, SheetId}, + grid::{CodeRun, DataTableKind, Sheet, SheetId}, CellValue, Value, }; diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 4d6730a341..6588727a8e 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -273,18 +273,24 @@ impl GridController { let sort_order_enum = match sort_order.as_str() { "asc" => SortDirection::Ascending, "desc" => SortDirection::Descending, + "none" => SortDirection::None, _ => bail!("Invalid sort order"), }; - data_table.sort(column_index as usize, sort_order_enum)?; + let old_value = data_table.sort_column(column_index as usize, sort_order_enum)?; self.send_to_wasm(transaction, &sheet_rect)?; - // TODO(ddimaria): remove this clone - let forward_operations = vec![op.clone()]; + let forward_operations = vec![op]; - // TODO(ddimaria): this is a placeholder, actually implement - let reverse_operations = vec![op.clone()]; + let reverse_operations = vec![Operation::SortDataTable { + sheet_pos, + column_index, + sort_order: old_value + .map(|v| v.direction) + .unwrap_or(SortDirection::None) + .to_string(), + }]; self.data_table_operations( transaction, @@ -319,11 +325,12 @@ impl GridController { self.send_to_wasm(transaction, &sheet_rect)?; - // TODO(ddimaria): remove this clone - let forward_operations = vec![op.clone()]; + let forward_operations = vec![op]; - // TODO(ddimaria): this is a placeholder, actually implement - let reverse_operations = vec![op.clone()]; + let reverse_operations = vec![Operation::DataTableFirstRowAsHeader { + sheet_pos, + first_row_is_header: !first_row_is_header, + }]; self.data_table_operations( transaction, @@ -515,14 +522,17 @@ mod tests { let mut transaction = PendingTransaction::default(); gc.execute_sort_data_table(&mut transaction, op).unwrap(); - print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); - assert_sorted_data_table(&gc, sheet_id, pos, "simple.csv"); + print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); // undo, the value should be a data table again execute_reverse_operations(&mut gc, &transaction); + print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + assert_simple_csv(&gc, sheet_id, pos, "simple.csv"); // redo, the value should be on the grid execute_forward_operations(&mut gc, &mut transaction); + print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + assert_sorted_data_table(&gc, sheet_id, pos, "simple.csv"); } } diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 8131b9273c..512363d222 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -107,6 +107,7 @@ impl GridController { pub fn execute_reverse_operations(gc: &mut GridController, transaction: &PendingTransaction) { let mut undo_transaction = PendingTransaction::default(); undo_transaction.operations = transaction.reverse_operations.clone().into(); + println!("reverse_operations: {:?}", undo_transaction.operations); gc.execute_operation(&mut undo_transaction); } diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index f00d5d3033..8748af4f54 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -130,10 +130,6 @@ pub(crate) mod tests { pos: Pos, file_name: &'a str, ) -> (&'a GridController, SheetId, Pos, &'a str) { - let import = Import::new(file_name.into()); - let cell_value = CellValue::Import(import); - // assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); - // data table should be at `pos` assert_eq!( gc.sheet(sheet_id).first_data_table_within(pos).unwrap(), diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index a0713ed53f..120ef7d253 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -4,6 +4,8 @@ //! any given CellValue::Code type (ie, if it doesn't exist then a run hasn't been //! performed yet). +use std::fmt::{Display, Formatter}; + use crate::cellvalue::Import; use crate::grid::CodeRun; use crate::{ @@ -48,10 +50,21 @@ impl DataTableColumn { pub enum SortDirection { Ascending, Descending, + None, +} + +impl Display for SortDirection { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SortDirection::Ascending => write!(f, "asc"), + SortDirection::Descending => write!(f, "desc"), + SortDirection::None => write!(f, "none"), + } + } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct DataTableSortOrder { +pub struct DataTableSort { pub column_index: usize, pub direction: SortDirection, } @@ -62,7 +75,7 @@ pub struct DataTable { pub name: String, pub has_header: bool, pub columns: Option>, - pub sort: Option>, + pub sort: Option>, pub display_buffer: Option>, pub value: Value, pub readonly: bool, @@ -126,7 +139,7 @@ impl DataTable { name: &str, has_header: bool, columns: Option>, - sort: Option>, + sort: Option>, display_buffer: Option>, value: Value, readonly: bool, @@ -245,28 +258,65 @@ impl DataTable { Ok(()) } - pub fn sort(&mut self, column_index: usize, direction: SortDirection) -> Result<()> { - let values = self.value.clone().into_array()?; + pub fn sort_column( + &mut self, + column_index: usize, + direction: SortDirection, + ) -> Result> { + let old = self.prepend_sort(column_index, direction.clone()); let increment = |i| if self.has_header { i + 1 } else { i }; - let mut display_buffer = values - .col(column_index) - .skip(increment(0)) - .enumerate() - .sorted_by(|a, b| match direction { - SortDirection::Ascending => a.1.total_cmp(b.1), - SortDirection::Descending => b.1.total_cmp(a.1), - }) - .map(|(i, _)| increment(i) as u64) - .collect::>(); + if let Some(ref mut sort) = self.sort.to_owned() { + for sort in sort.iter().rev() { + let mut display_buffer = self + .display_value()? + .into_array()? + .col(sort.column_index) + .skip(increment(0)) + .enumerate() + .sorted_by(|a, b| match direction { + SortDirection::Ascending => a.1.total_cmp(b.1), + SortDirection::Descending => b.1.total_cmp(a.1), + SortDirection::None => std::cmp::Ordering::Equal, + }) + .map(|(i, _)| increment(i) as u64) + .collect::>(); + + if self.has_header { + display_buffer.insert(0, 0); + } - if self.has_header { - display_buffer.insert(0, 0); + self.display_buffer = Some(display_buffer); + } } - self.display_buffer = Some(display_buffer); + Ok(old) + } + + pub fn prepend_sort( + &mut self, + column_index: usize, + direction: SortDirection, + ) -> Option { + let data_table_sort = DataTableSort { + column_index, + direction, + }; + + let old = self.sort.as_mut().and_then(|sort| { + let index = sort + .iter() + .position(|sort| sort.column_index == column_index); - Ok(()) + index.and_then(|index| Some(sort.remove(index))) + }); + + match self.sort { + Some(ref mut sort) => sort.insert(0, data_table_sort), + None => self.sort = Some(vec![data_table_sort]), + } + + old } pub fn display_value_from_buffer(&self, display_buffer: &Vec) -> Result { @@ -610,14 +660,18 @@ pub mod test { pretty_print_data_table(&data_table, Some("Original Data Table"), None); // sort by population city ascending - data_table.sort(0, SortDirection::Ascending).unwrap(); + data_table.sort_column(0, SortDirection::Ascending).unwrap(); + println!("{:?}", data_table.sort); pretty_print_data_table(&data_table, Some("Sorted by City"), None); assert_data_table_row(&data_table, 1, values[2].clone()); assert_data_table_row(&data_table, 2, values[3].clone()); assert_data_table_row(&data_table, 3, values[1].clone()); // sort by population descending - data_table.sort(3, SortDirection::Descending).unwrap(); + data_table + .sort_column(3, SortDirection::Descending) + .unwrap(); + println!("{:?}", data_table.sort); pretty_print_data_table(&data_table, Some("Sorted by Population Descending"), None); assert_data_table_row(&data_table, 1, values[2].clone()); assert_data_table_row(&data_table, 2, values[1].clone()); diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 881dce2740..9f2ea703bb 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -4,7 +4,7 @@ use indexmap::IndexMap; use itertools::Itertools; use crate::{ - grid::{CodeRun, DataTable, DataTableColumn, DataTableKind, DataTableSortOrder, SortDirection}, + grid::{CodeRun, DataTable, DataTableColumn, DataTableKind, DataTableSort, SortDirection}, ArraySize, Axis, Pos, RunError, RunErrorMsg, Value, }; @@ -179,11 +179,12 @@ pub(crate) fn import_data_table_builder( }), sort: data_table.sort.map(|sort| { sort.into_iter() - .map(|sort| DataTableSortOrder { + .map(|sort| DataTableSort { column_index: sort.column_index, direction: match sort.direction { current::SortDirectionSchema::Ascending => SortDirection::Ascending, current::SortDirectionSchema::Descending => SortDirection::Descending, + current::SortDirectionSchema::None => SortDirection::None, }, }) .collect() @@ -360,6 +361,7 @@ pub(crate) fn export_data_tables( direction: match item.direction { SortDirection::Ascending => current::SortDirectionSchema::Ascending, SortDirection::Descending => current::SortDirectionSchema::Descending, + SortDirection::None => current::SortDirectionSchema::None, }, }) .collect() diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index c16e1d7e5d..1d9a5ada81 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -110,6 +110,7 @@ pub enum DataTableKindSchema { pub enum SortDirectionSchema { Ascending, Descending, + None, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] From db8552abfbf5389b23b0c0e2ff69a1502b0842b7 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 17 Oct 2024 12:46:33 -0600 Subject: [PATCH 048/373] Add TODOs --- quadratic-core/src/controller/operations/operation.rs | 1 + quadratic-core/src/grid/data_table.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 0cf495cfe9..264ede3d99 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -54,6 +54,7 @@ pub enum Operation { SortDataTable { sheet_pos: SheetPos, column_index: u32, + // TODO(ddimarai): rename this to `direction` sort_order: String, }, DataTableFirstRowAsHeader { diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index ceb0783848..3580f3f822 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -53,6 +53,7 @@ pub enum SortDirection { None, } +// TODO(ddimarai): implement strum and remove this impl impl Display for SortDirection { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -272,6 +273,7 @@ impl DataTable { let old = self.prepend_sort(column_index, direction.clone()); let increment = |i| if self.header_is_first_row { i + 1 } else { i }; + // TODO(ddimaria): skip this if SortDirection::None if let Some(ref mut sort) = self.sort.to_owned() { for sort in sort.iter().rev() { let mut display_buffer = self From d162a3f12be19b4f1b56766b716540d44f05c58e Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 04:57:04 -0700 Subject: [PATCH 049/373] table highlight stays open when context menu is open --- .../src/app/atoms/contextMenuAtoms.ts | 1 + quadratic-client/src/app/events/events.ts | 3 +- .../HTMLGrid/contextMenus/GridContextMenu.tsx | 2 ++ .../contextMenus/TableContextMenu.tsx | 2 ++ .../src/app/gridGL/cells/tables/Table.ts | 3 +- .../src/app/gridGL/cells/tables/Tables.ts | 36 +++++++++++++++++++ quadratic-core/src/grid/data_table.rs | 2 +- 7 files changed, 46 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/atoms/contextMenuAtoms.ts b/quadratic-client/src/app/atoms/contextMenuAtoms.ts index e8f306b724..cbc7482b00 100644 --- a/quadratic-client/src/app/atoms/contextMenuAtoms.ts +++ b/quadratic-client/src/app/atoms/contextMenuAtoms.ts @@ -38,6 +38,7 @@ export const contextMenuAtom = atom({ ({ setSelf }) => { const clear = () => { setSelf(() => ({})); + events.emit('contextMenuClose'); }; const set = (options: ContextMenuOptions) => { diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index e3a71cee50..b2071287f2 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -125,7 +125,7 @@ interface EventTypes { // when validation changes state validation: (validation: string | boolean) => void; - // trigger or clear a context menu + // trigger a context menu contextMenu: (options: { type?: ContextMenuType; world?: Point; @@ -133,6 +133,7 @@ interface EventTypes { column?: number; table?: JsRenderCodeCell; }) => void; + contextMenuClose: () => void; suggestionDropdownKeyboard: (key: 'ArrowDown' | 'ArrowUp' | 'Enter' | 'Escape' | 'Tab') => void; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index 3fc41cc895..f167f5a5d0 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -2,6 +2,7 @@ import { Action } from '@/app/actions/actions'; import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { focusGrid } from '@/app/helpers/focusGrid'; @@ -15,6 +16,7 @@ export const GridContextMenu = () => { const onClose = useCallback(() => { setContextMenu({}); + events.emit('contextMenuClose'); focusGrid(); }, [setContextMenu]); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 37a538983d..b343b7ca4f 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -2,6 +2,7 @@ import { Action } from '@/app/actions/actions'; import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { events } from '@/app/events/events'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { focusGrid } from '@/app/helpers/focusGrid'; @@ -14,6 +15,7 @@ export const TableContextMenu = () => { const onClose = useCallback(() => { setContextMenu({}); + events.emit('contextMenuClose'); focusGrid(); }, [setContextMenu]); diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 47af93a816..77d32f7e27 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -18,7 +18,6 @@ const DROPDOWN_PADDING = 10; export class Table extends Container { private sheet: Sheet; - private codeCell: JsRenderCodeCell; private headingHeight = 0; private tableName: Container; private tableNameText: BitmapText; @@ -32,6 +31,8 @@ export class Table extends Container { private headingBounds: Rectangle; private columns: Column[]; + codeCell: JsRenderCodeCell; + constructor(sheet: Sheet, codeCell: JsRenderCodeCell) { super(); this.codeCell = codeCell; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 53e0f5cf11..0f68f2fd2b 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -1,6 +1,7 @@ //! Tables renders all pixi-based UI elements for tables. Right now that's the //! headings. +import { ContextMenuType } from '@/app/atoms/contextMenuAtoms'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; @@ -15,6 +16,7 @@ export class Tables extends Container
{ private activeTable: Table | undefined; private hoverTable: Table | undefined; + private contextMenuTable: Table | undefined; constructor(cellsSheet: CellsSheet) { super(); @@ -25,6 +27,9 @@ export class Tables extends Container
{ events.on('cursorPosition', this.cursorPosition); events.on('sheetOffsets', this.sheetOffsets); events.on('changeSheet', this.changeSheet); + + events.on('contextMenu', this.contextMenu); + events.on('contextMenuClose', this.contextMenu); } get sheet(): Sheet { @@ -36,6 +41,7 @@ export class Tables extends Container
{ } private renderCodeCells = (sheetId: string, codeCells: JsRenderCodeCell[]) => { + console.log(codeCells); if (sheetId === this.cellsSheet.sheetId) { this.removeChildren(); codeCells.forEach((codeCell) => this.addChild(new Table(this.sheet, codeCell))); @@ -86,6 +92,9 @@ export class Tables extends Container
{ // Checks if the mouse cursor is hovering over a table or table heading. checkHover(world: Point) { const hover = this.children.find((table) => table.checkHover(world)); + if (hover === this.contextMenuTable || hover === this.activeTable) { + return; + } if (hover !== this.hoverTable) { if (this.hoverTable) { this.hoverTable.hideActive(); @@ -105,4 +114,31 @@ export class Tables extends Container
{ } } } + + // track and activate a table whose context menu is open (this handles the + // case where you hover a table and open the context menu; we want to keep + // that table active while the context menu is open) + contextMenu = (options?: { type?: ContextMenuType; table?: JsRenderCodeCell }) => { + if (!options) { + if (this.contextMenuTable) { + this.contextMenuTable.hideActive(); + this.contextMenuTable = undefined; + } + return; + } + if (this.contextMenuTable) { + this.contextMenuTable.hideActive(); + this.contextMenuTable = undefined; + } + if (options.type === ContextMenuType.Table && options.table) { + this.contextMenuTable = this.children.find((table) => table.codeCell === options.table); + if (this.contextMenuTable) { + this.contextMenuTable.showActive(); + if (this.hoverTable === this.contextMenuTable) { + this.hoverTable = undefined; + } + } + } + pixiApp.setViewportDirty(); + }; } diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 3580f3f822..1c160c6a8f 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -821,7 +821,7 @@ pub mod test { Some(CellValue::Text("first".into())) ); - t.header_is_first_row = false; + t.toggle_first_row_as_header(false); assert_eq!(sheet.display_value(Pos { x: 1, y: 1 }), None); } } From e099cc24137848f169a7874572a1c9a0a21945eb Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 05:02:24 -0700 Subject: [PATCH 050/373] fixed bug with hover after context menu close --- .../{contextMenuAtoms.ts => contextMenuAtom.ts} | 3 +++ quadratic-client/src/app/events/events.ts | 2 +- .../HTMLGrid/contextMenus/GridContextMenu.tsx | 2 +- .../HTMLGrid/contextMenus/TableContextMenu.tsx | 2 +- .../src/app/gridGL/PixiAppEffects.tsx | 2 +- .../src/app/gridGL/cells/tables/Tables.ts | 15 ++++++--------- .../app/gridGL/interaction/pointer/PointerDown.ts | 2 +- .../gridGL/interaction/pointer/PointerHeading.ts | 2 +- .../gridGL/interaction/pointer/PointerTable.ts | 2 +- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) rename quadratic-client/src/app/atoms/{contextMenuAtoms.ts => contextMenuAtom.ts} (93%) diff --git a/quadratic-client/src/app/atoms/contextMenuAtoms.ts b/quadratic-client/src/app/atoms/contextMenuAtom.ts similarity index 93% rename from quadratic-client/src/app/atoms/contextMenuAtoms.ts rename to quadratic-client/src/app/atoms/contextMenuAtom.ts index cbc7482b00..e3bce653ef 100644 --- a/quadratic-client/src/app/atoms/contextMenuAtoms.ts +++ b/quadratic-client/src/app/atoms/contextMenuAtom.ts @@ -43,6 +43,9 @@ export const contextMenuAtom = atom({ const set = (options: ContextMenuOptions) => { setSelf(() => options); + if (!options.table) { + events.emit('contextMenuClose'); + } }; events.on('cursorPosition', clear); diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index b2071287f2..718d5f0381 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -1,4 +1,4 @@ -import { ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { ErrorValidation } from '@/app/gridGL/cells/CellsSheet'; import { EditingCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; import { SheetPosTS } from '@/app/gridGL/types/size'; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index f167f5a5d0..2145cec0e8 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -1,7 +1,7 @@ //! This shows the grid heading context menu. import { Action } from '@/app/actions/actions'; -import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index b343b7ca4f..9a3cd67007 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -1,7 +1,7 @@ //! This shows the table context menu. import { Action } from '@/app/actions/actions'; -import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; diff --git a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx index 4d4ac40937..d9dbe54c79 100644 --- a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx +++ b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx @@ -1,5 +1,5 @@ import { codeEditorAtom, codeEditorShowCodeEditorAtom } from '@/app/atoms/codeEditorAtom'; -import { contextMenuAtom } from '@/app/atoms/contextMenuAtoms'; +import { contextMenuAtom } from '@/app/atoms/contextMenuAtom'; import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; import { gridPanModeAtom } from '@/app/atoms/gridPanModeAtom'; import { gridSettingsAtom, presentationModeAtom, showHeadingsAtom } from '@/app/atoms/gridSettingsAtom'; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 0f68f2fd2b..6ee8a2b95c 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -1,7 +1,7 @@ //! Tables renders all pixi-based UI elements for tables. Right now that's the //! headings. -import { ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; @@ -92,7 +92,7 @@ export class Tables extends Container
{ // Checks if the mouse cursor is hovering over a table or table heading. checkHover(world: Point) { const hover = this.children.find((table) => table.checkHover(world)); - if (hover === this.contextMenuTable || hover === this.activeTable) { + if (hover && (hover === this.contextMenuTable || hover === this.activeTable)) { return; } if (hover !== this.hoverTable) { @@ -119,17 +119,14 @@ export class Tables extends Container
{ // case where you hover a table and open the context menu; we want to keep // that table active while the context menu is open) contextMenu = (options?: { type?: ContextMenuType; table?: JsRenderCodeCell }) => { - if (!options) { - if (this.contextMenuTable) { - this.contextMenuTable.hideActive(); - this.contextMenuTable = undefined; - } - return; - } if (this.contextMenuTable) { this.contextMenuTable.hideActive(); this.contextMenuTable = undefined; } + if (!options?.type) { + pixiApp.setViewportDirty(); + return; + } if (options.type === ContextMenuType.Table && options.table) { this.contextMenuTable = this.children.find((table) => table.codeCell === options.table); if (this.contextMenuTable) { diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts index f630b23517..53b8f50267 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts @@ -1,4 +1,4 @@ -import { ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { PanMode } from '@/app/atoms/gridPanModeAtom'; import { events } from '@/app/events/events'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts index 576aab4d49..adce122be1 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts @@ -1,4 +1,4 @@ -import { ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { PanMode } from '@/app/atoms/gridPanModeAtom'; import { events } from '@/app/events/events'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index d25f10be0b..271074b1d9 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -1,4 +1,4 @@ -import { ContextMenuType } from '@/app/atoms/contextMenuAtoms'; +import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { isMac } from '@/shared/utils/isMac'; diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index 1abc287df7..c4fb353f04 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -1,5 +1,5 @@ import { CodeEditorState, defaultCodeEditorState } from '@/app/atoms/codeEditorAtom'; -import { ContextMenuOptions, ContextMenuState, defaultContextMenuState } from '@/app/atoms/contextMenuAtoms'; +import { ContextMenuOptions, ContextMenuState, defaultContextMenuState } from '@/app/atoms/contextMenuAtom'; import { EditorInteractionState, editorInteractionStateDefault } from '@/app/atoms/editorInteractionStateAtom'; import { defaultGridPanMode, GridPanMode, PanMode } from '@/app/atoms/gridPanModeAtom'; import { defaultGridSettings, GridSettings } from '@/app/atoms/gridSettingsAtom'; From 375afbca9cf033b87d162de7d7a720fa80e67543 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 05:27:04 -0700 Subject: [PATCH 051/373] remove cfg test for print --- quadratic-core/src/grid/data_table.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 1c160c6a8f..189e480e31 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -820,7 +820,6 @@ pub mod test { sheet.display_value(Pos { x: 1, y: 1 }), Some(CellValue::Text("first".into())) ); - t.toggle_first_row_as_header(false); assert_eq!(sheet.display_value(Pos { x: 1, y: 1 }), None); } From ba16b2f3dbf2d356d4cf4a96e3aa0c33806e66e2 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 05:51:06 -0700 Subject: [PATCH 052/373] first row as column headings properly hooked up to rust --- .../src/app/actions/dataTableSpec.ts | 1 + .../src/app/gridGL/cells/tables/Tables.ts | 27 +++++++++++++++++-- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 7 +++++ .../execute_operation/execute_data_table.rs | 6 ++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index b0691e3fd4..e12b8221d1 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -20,6 +20,7 @@ const isDataTable = (): boolean => { }; const isFirstRowHeader = (): boolean => { + console.log('running isFirstRowHeader', pixiAppSettings.contextMenu); return !!pixiAppSettings.contextMenu.table?.first_row_header; }; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 6ee8a2b95c..6cbd46e4b4 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -8,7 +8,7 @@ import { Sheet } from '@/app/grid/sheet/Sheet'; import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; import { Table } from '@/app/gridGL/cells/tables/Table'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { JsCodeCell, JsRenderCodeCell } from '@/app/quadratic-core-types'; import { Container, Point } from 'pixi.js'; export class Tables extends Container
{ @@ -22,7 +22,7 @@ export class Tables extends Container
{ super(); this.cellsSheet = cellsSheet; events.on('renderCodeCells', this.renderCodeCells); - // todo: update code cells? + events.on('updateCodeCell', this.updateCodeCell); events.on('cursorPosition', this.cursorPosition); events.on('sheetOffsets', this.sheetOffsets); @@ -40,6 +40,29 @@ export class Tables extends Container
{ return sheet; } + private updateCodeCell = (options: { + sheetId: string; + x: number; + y: number; + codeCell?: JsCodeCell; + renderCodeCell?: JsRenderCodeCell; + }) => { + const { sheetId, x, y, renderCodeCell } = options; + if (sheetId === this.cellsSheet.sheetId) { + const table = this.children.find((table) => table.codeCell.x === x && table.codeCell.y === y); + if (table) { + if (!renderCodeCell) { + table.removeTableNames(); + table.destroy(); + } else { + table.updateCodeCell(renderCodeCell); + } + } else if (renderCodeCell) { + this.addChild(new Table(this.sheet, renderCodeCell)); + } + } + }; + private renderCodeCells = (sheetId: string, codeCells: JsRenderCodeCell[]) => { console.log(codeCells); if (sheetId === this.cellsSheet.sheetId) { diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index c4fb353f04..f4afe5290f 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -64,6 +64,7 @@ class PixiAppSettings { } this.lastSettings = this.settings; events.on('gridSettings', this.getSettings); + events.on('contextMenu', this.getContextSettings); this._input = { show: false }; this._panMode = PanMode.Disabled; } @@ -223,6 +224,12 @@ class PixiAppSettings { this.contextMenu = contextMenu; this.setContextMenu = setContextMenu; } + + // We need this to ensure contextMenu is updated immediately to the state. The + // above function waits a tick. + private getContextSettings = (contextMenu: ContextMenuState) => { + this.contextMenu = contextMenu; + }; } export const pixiAppSettings = new PixiAppSettings(); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 6588727a8e..003d7ed321 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -320,10 +320,14 @@ impl GridController { let sheet_rect = SheetRect::single_sheet_pos(sheet_pos); let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; let data_table = sheet.data_table_mut(data_table_pos)?; + let data_table_rect = data_table + .output_rect(sheet_pos.into(), true) + .to_sheet_rect(sheet_id); data_table.toggle_first_row_as_header(first_row_is_header); - self.send_to_wasm(transaction, &sheet_rect)?; + self.send_to_wasm(transaction, &data_table_rect)?; + transaction.add_code_cell(sheet_id, sheet_pos.into()); let forward_operations = vec![op]; From 303baef0658a4668791c24e14a98086057b91fbf Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 06:14:45 -0700 Subject: [PATCH 053/373] more consistent active table when using context menu --- quadratic-client/src/app/actions/dataTableSpec.ts | 3 +-- quadratic-client/src/app/atoms/contextMenuAtom.ts | 2 +- .../app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx | 2 -- .../app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx | 1 + quadratic-client/src/app/gridGL/cells/tables/Table.ts | 1 - quadratic-client/src/app/gridGL/cells/tables/Tables.ts | 8 ++++++-- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index e12b8221d1..977758efe3 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -20,7 +20,6 @@ const isDataTable = (): boolean => { }; const isFirstRowHeader = (): boolean => { - console.log('running isFirstRowHeader', pixiAppSettings.contextMenu); return !!pixiAppSettings.contextMenu.table?.first_row_header; }; @@ -62,7 +61,7 @@ export const dataTableSpec: DataTableSpec = { [Action.ToggleFirstRowAsHeaderDataTable]: { label: 'First row as column headings', checkbox: isFirstRowHeader, - run: async () => { + run: () => { const table = pixiAppSettings.contextMenu?.table; if (table) { quadraticCore.dataTableFirstRowAsHeader( diff --git a/quadratic-client/src/app/atoms/contextMenuAtom.ts b/quadratic-client/src/app/atoms/contextMenuAtom.ts index e3bce653ef..7335c50694 100644 --- a/quadratic-client/src/app/atoms/contextMenuAtom.ts +++ b/quadratic-client/src/app/atoms/contextMenuAtom.ts @@ -43,7 +43,7 @@ export const contextMenuAtom = atom({ const set = (options: ContextMenuOptions) => { setSelf(() => options); - if (!options.table) { + if (!options.type) { events.emit('contextMenuClose'); } }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index 2145cec0e8..a061d2f4da 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -2,7 +2,6 @@ import { Action } from '@/app/actions/actions'; import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; -import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { focusGrid } from '@/app/helpers/focusGrid'; @@ -16,7 +15,6 @@ export const GridContextMenu = () => { const onClose = useCallback(() => { setContextMenu({}); - events.emit('contextMenuClose'); focusGrid(); }, [setContextMenu]); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 9a3cd67007..2b0af312c3 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -14,6 +14,7 @@ export const TableContextMenu = () => { const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { + console.log('onClose'); setContextMenu({}); events.emit('contextMenuClose'); focusGrid(); diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 77d32f7e27..0e3a0cbbd2 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -103,7 +103,6 @@ export class Table extends Container { this.outline.visible = false; // draw table name - console.log(codeCell.name); if (sheets.sheet.id === this.sheet.id) { pixiApp.overHeadings.addChild(this.tableName); } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 6cbd46e4b4..cb4b999063 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -56,6 +56,9 @@ export class Tables extends Container
{ table.destroy(); } else { table.updateCodeCell(renderCodeCell); + if (table === this.activeTable || table === this.hoverTable || table === this.contextMenuTable) { + table.showActive(); + } } } else if (renderCodeCell) { this.addChild(new Table(this.sheet, renderCodeCell)); @@ -64,7 +67,6 @@ export class Tables extends Container
{ }; private renderCodeCells = (sheetId: string, codeCells: JsRenderCodeCell[]) => { - console.log(codeCells); if (sheetId === this.cellsSheet.sheetId) { this.removeChildren(); codeCells.forEach((codeCell) => this.addChild(new Table(this.sheet, codeCell))); @@ -143,7 +145,9 @@ export class Tables extends Container
{ // that table active while the context menu is open) contextMenu = (options?: { type?: ContextMenuType; table?: JsRenderCodeCell }) => { if (this.contextMenuTable) { - this.contextMenuTable.hideActive(); + // we keep the former context menu table active after the context + // menu closes until the cursor moves again. + this.hoverTable = this.contextMenuTable; this.contextMenuTable = undefined; } if (!options?.type) { From 5141f747f5ac213595af6e787bdabc9c6c18e824 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 06:22:04 -0700 Subject: [PATCH 054/373] adding UI for rename data table --- quadratic-client/src/app/actions/actions.ts | 1 + quadratic-client/src/app/actions/actionsSpec.ts | 7 +++++++ quadratic-client/src/app/actions/dataTableSpec.ts | 13 +++++++++++-- .../HTMLGrid/contextMenus/TableContextMenu.tsx | 2 +- .../gridGL/HTMLGrid/contextMenus/contextMenu.tsx | 6 +++--- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index c2d229f6d4..b5c25ea2c4 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -143,4 +143,5 @@ export enum Action { FlattenDataTable = 'flatten_data_table', GridToDataTable = 'grid_to_data_table', ToggleFirstRowAsHeaderDataTable = 'toggle_first_row_as_header_data_table', + RenameDataTable = 'rename_data_table', } diff --git a/quadratic-client/src/app/actions/actionsSpec.ts b/quadratic-client/src/app/actions/actionsSpec.ts index c3f981140d..34df87ba7a 100644 --- a/quadratic-client/src/app/actions/actionsSpec.ts +++ b/quadratic-client/src/app/actions/actionsSpec.ts @@ -40,7 +40,14 @@ export type ActionSpec = { // return `` // ``` Icon?: IconComponent; + + // use a checkbox instead of an Icon checkbox?: boolean | (() => boolean); + + // defaultOption will bold the name (this is usually used to indicate that + // double clicking will trigger the action) + defaultOption?: boolean; + isAvailable?: (args: ActionAvailabilityArgs) => boolean; // Used for command palette search keywords?: string[]; diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 977758efe3..8072ab9491 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -2,13 +2,13 @@ import { Action } from '@/app/actions/actions'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -import { PersonAddIcon } from '@/shared/components/Icons'; +import { FileRenameIcon, PersonAddIcon } from '@/shared/components/Icons'; import { sheets } from '../grid/controller/Sheets'; import { ActionSpecRecord } from './actionsSpec'; type DataTableSpec = Pick< ActionSpecRecord, - Action.FlattenDataTable | Action.GridToDataTable | Action.ToggleFirstRowAsHeaderDataTable + Action.FlattenDataTable | Action.GridToDataTable | Action.ToggleFirstRowAsHeaderDataTable | Action.RenameDataTable >; export type DataTableActionArgs = { @@ -74,4 +74,13 @@ export const dataTableSpec: DataTableSpec = { } }, }, + [Action.RenameDataTable]: { + label: 'Rename data table', + defaultOption: true, + Icon: FileRenameIcon, + run: async () => { + // const { x, y } = sheets.sheet.cursor.cursorPosition; + // quadraticCore.renameDataTable(sheets.sheet.id, x, y, sheets.getCursorPosition()); + }, + }, }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 2b0af312c3..075a12bb64 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -14,7 +14,6 @@ export const TableContextMenu = () => { const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { - console.log('onClose'); setContextMenu({}); events.emit('contextMenuClose'); focusGrid(); @@ -51,6 +50,7 @@ export const TableContextMenu = () => { menuStyle={{ padding: '0', color: 'inherit' }} menuClassName="bg-background" > + diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx index 62b9ecb817..a4d30ff918 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx @@ -10,7 +10,7 @@ interface Props { } export const MenuItemAction = (props: Props): JSX.Element | null => { - const { label, Icon, run, isAvailable, checkbox } = defaultActionSpec[props.action]; + const { label, Icon, run, isAvailable, checkbox, defaultOption } = defaultActionSpec[props.action]; const isAvailableArgs = useIsAvailableArgs(); const keyboardShortcut = keyboardShortcutEnumToDisplay(props.action); @@ -20,7 +20,7 @@ export const MenuItemAction = (props: Props): JSX.Element | null => { return ( - {label} + {label} ); }; @@ -32,7 +32,7 @@ function MenuItemShadStyle({ onClick, keyboardShortcut, }: { - children: string; + children: JSX.Element; Icon?: IconComponent; onClick: any; checkbox?: boolean | (() => boolean); From 723dd68058b27b3005c753fe1ae7d46bd2cfd7ec Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 09:01:31 -0700 Subject: [PATCH 055/373] renaming data table --- .../src/app/actions/dataTableSpec.ts | 11 +- .../src/app/atoms/contextMenuAtom.ts | 9 ++ quadratic-client/src/app/events/events.ts | 3 +- .../app/gridGL/HTMLGrid/HTMLGridContainer.tsx | 4 +- .../HTMLGrid/contextMenus/GridContextMenu.tsx | 8 +- .../contextMenus/TableContextMenu.tsx | 9 +- .../HTMLGrid/contextMenus/TableRename.tsx | 123 ++++++++++++++++++ .../src/app/gridGL/cells/tables/Table.ts | 19 ++- .../src/app/gridGL/cells/tables/Tables.ts | 51 ++++++-- .../interaction/keyboard/useKeyboard.ts | 2 +- .../app/gridGL/interaction/pointer/Pointer.ts | 3 +- .../interaction/pointer/PointerTable.ts | 48 ++++++- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 12 +- 13 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 8072ab9491..971f939a4f 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -1,4 +1,6 @@ import { Action } from '@/app/actions/actions'; +import { ContextMenuSpecial } from '@/app/atoms/contextMenuAtom'; +import { events } from '@/app/events/events'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; @@ -79,8 +81,13 @@ export const dataTableSpec: DataTableSpec = { defaultOption: true, Icon: FileRenameIcon, run: async () => { - // const { x, y } = sheets.sheet.cursor.cursorPosition; - // quadraticCore.renameDataTable(sheets.sheet.id, x, y, sheets.getCursorPosition()); + const contextMenu = pixiAppSettings.contextMenu; + if (contextMenu) { + setTimeout(() => { + pixiAppSettings.setContextMenu?.({ ...contextMenu, special: ContextMenuSpecial.rename }); + events.emit('contextMenu', { ...contextMenu, special: ContextMenuSpecial.rename }); + }, 0); + } }, }, }; diff --git a/quadratic-client/src/app/atoms/contextMenuAtom.ts b/quadratic-client/src/app/atoms/contextMenuAtom.ts index 7335c50694..31a4068852 100644 --- a/quadratic-client/src/app/atoms/contextMenuAtom.ts +++ b/quadratic-client/src/app/atoms/contextMenuAtom.ts @@ -8,12 +8,20 @@ export enum ContextMenuType { Table = 'table', } +export enum ContextMenuSpecial { + rename = 'rename', +} + export interface ContextMenuState { type?: ContextMenuType; world?: Point; column?: number; row?: number; table?: JsRenderCodeCell; + + // special states we need to track + // rename is for tables + special?: ContextMenuSpecial; } export const defaultContextMenuState: ContextMenuState = { @@ -29,6 +37,7 @@ export interface ContextMenuOptions { column?: number; row?: number; table?: JsRenderCodeCell; + special?: ContextMenuSpecial; } export const contextMenuAtom = atom({ diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 718d5f0381..67c3a99a33 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -1,4 +1,4 @@ -import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { ErrorValidation } from '@/app/gridGL/cells/CellsSheet'; import { EditingCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; import { SheetPosTS } from '@/app/gridGL/types/size'; @@ -132,6 +132,7 @@ interface EventTypes { row?: number; column?: number; table?: JsRenderCodeCell; + special?: ContextMenuSpecial; }) => void; contextMenuClose: () => void; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index 9521bb9aab..db40ca88fb 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -3,13 +3,14 @@ import { Annotations } from '@/app/gridGL/HTMLGrid/annotations/Annotations'; import { CodeHint } from '@/app/gridGL/HTMLGrid/CodeHint'; import { CodeRunning } from '@/app/gridGL/HTMLGrid/codeRunning/CodeRunning'; import { GridContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/GridContextMenu'; +import { TableContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableContextMenu'; +import { TableRename } from '@/app/gridGL/HTMLGrid/contextMenus/TableRename'; import { HoverCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; import { HoverTooltip } from '@/app/gridGL/HTMLGrid/hoverTooltip/HoverTooltip'; import { HtmlCells } from '@/app/gridGL/HTMLGrid/htmlCells/HtmlCells'; import { InlineEditor } from '@/app/gridGL/HTMLGrid/inlineEditor/InlineEditor'; import { MultiplayerCursors } from '@/app/gridGL/HTMLGrid/multiplayerCursor/MultiplayerCursors'; import { MultiplayerCellEdits } from '@/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdits'; -import { TableContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableContextMenu'; import { useHeadingSize } from '@/app/gridGL/HTMLGrid/useHeadingSize'; import { HtmlValidations } from '@/app/gridGL/HTMLGrid/validations/HtmlValidations'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; @@ -133,6 +134,7 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { > + ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index a061d2f4da..6216354bd4 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -14,9 +14,11 @@ export const GridContextMenu = () => { const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { - setContextMenu({}); - focusGrid(); - }, [setContextMenu]); + if (contextMenu.type === ContextMenuType.Grid) { + setContextMenu({}); + focusGrid(); + } + }, [contextMenu.type, setContextMenu]); useEffect(() => { pixiApp.viewport.on('moved', onClose); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 075a12bb64..d245be9399 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -1,7 +1,7 @@ //! This shows the table context menu. import { Action } from '@/app/actions/actions'; -import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { contextMenuAtom, ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; @@ -14,10 +14,13 @@ export const TableContextMenu = () => { const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { + if (contextMenu.type === ContextMenuType.Table && contextMenu.special === ContextMenuSpecial.rename) { + return; + } setContextMenu({}); events.emit('contextMenuClose'); focusGrid(); - }, [setContextMenu]); + }, [contextMenu.special, contextMenu.type, setContextMenu]); useEffect(() => { pixiApp.viewport.on('moved', onClose); @@ -40,7 +43,7 @@ export const TableContextMenu = () => { top: contextMenu.world?.y ?? 0, transform: `scale(${1 / pixiApp.viewport.scale.x})`, pointerEvents: 'auto', - display: contextMenu.type === ContextMenuType.Table ? 'block' : 'none', + display: contextMenu.type === ContextMenuType.Table && !contextMenu.special ? 'block' : 'none', }} > { + const contextMenu = useRecoilValue(contextMenuAtom); + + const close = useCallback(() => { + events.emit('contextMenu', {}); + focusGrid(); + }, []); + + const saveAndClose = useCallback(() => { + if (contextMenu.table) { + // quadraticCore.renameDataTable(contextMenu.table.id, contextMenu.table.name); + } + close(); + }, [contextMenu.table, close]); + + const ref = useRef(null); + const position = useMemo(() => { + if ( + contextMenu.type !== ContextMenuType.Table || + contextMenu.special !== ContextMenuSpecial.rename || + !contextMenu.table + ) { + return { x: 0, y: 0, width: 0, height: 0 }; + } + const position = pixiApp.cellsSheets.current?.tables.getTableNamePosition(contextMenu.table.x, contextMenu.table.y); + if (!position) { + return { x: 0, y: 0, width: 0, height: 0 }; + } + return position; + }, [contextMenu]); + + // focus the input after the position is set + useEffect(() => { + if (position.height !== 0) { + setTimeout(() => { + if (ref.current) { + ref.current.select(); + ref.current.focus(); + } + }, 0); + } + }, [position]); + + useEffect(() => { + const viewportChanged = () => { + if (ref.current) { + ref.current.style.transform = `scale(${1 / pixiApp.viewport.scaled})`; + } + }; + events.on('viewportChanged', viewportChanged); + }, []); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + close(); + e.stopPropagation(); + e.preventDefault(); + } else if (e.key === 'Enter') { + saveAndClose(); + e.stopPropagation(); + e.preventDefault(); + } + }, + [close, saveAndClose] + ); + + const onChange = useCallback(() => { + if (ref.current) { + // need to calculate the width of the input using a span with the same css as the input + const span = document.createElement('span'); + span.className = 'text-sm px-3 w-full'; + span.style.fontSize = FONT_SIZE.toString(); + span.style.visibility = 'hidden'; + span.style.whiteSpace = 'pre'; + span.innerText = ref.current.value; + document.body.appendChild(span); + ref.current.style.width = `${span.offsetWidth}px`; + document.body.removeChild(span); + } + }, []); + + if ( + contextMenu.type !== ContextMenuType.Table || + contextMenu.special !== ContextMenuSpecial.rename || + !contextMenu.table + ) { + return null; + } + + return ( + + ); +}; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 0e3a0cbbd2..3c67ebcdaa 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -15,6 +15,7 @@ interface Column { } const DROPDOWN_PADDING = 10; +export const TABLE_NAME_PADDING = 4; export class Table extends Container { private sheet: Sheet; @@ -27,10 +28,10 @@ export class Table extends Container { private outline: Graphics; private tableBounds: Rectangle; - private tableNameBounds: Rectangle; private headingBounds: Rectangle; private columns: Column[]; + tableNameBounds: Rectangle; codeCell: JsRenderCodeCell; constructor(sheet: Sheet, codeCell: JsRenderCodeCell) { @@ -111,7 +112,7 @@ export class Table extends Container { this.tableName.visible = false; const text = this.tableName.addChild(this.tableNameText); this.tableNameText.text = codeCell.name; - text.position.set(OPEN_SANS_FIX.x, OPEN_SANS_FIX.y - this.headingBounds.height); + text.position.set(OPEN_SANS_FIX.x + TABLE_NAME_PADDING, OPEN_SANS_FIX.y - this.headingBounds.height); const dropdown = this.tableName.addChild(this.drawDropdown()); dropdown.position.set(text.width + OPEN_SANS_FIX.x + DROPDOWN_PADDING, -this.headingHeight / 2); @@ -200,8 +201,10 @@ export class Table extends Container { update(bounds: Rectangle, gridHeading: number) { this.visible = intersects.rectangleRectangle(this.tableBounds, bounds); this.headingPosition(bounds, gridHeading); - this.tableNamePosition(bounds, gridHeading); - this.tableName.scale.set(1 / pixiApp.viewport.scale.x); + if (this.isShowingTableName()) { + this.tableName.scale.set(1 / pixiApp.viewport.scale.x); + this.tableNamePosition(bounds, gridHeading); + } } hideActive() { @@ -216,11 +219,15 @@ export class Table extends Container { pixiApp.setViewportDirty(); } - addTableNames() { + showTableName() { pixiApp.overHeadings.addChild(this.tableName); } - removeTableNames() { + hideTableName() { pixiApp.overHeadings.removeChild(this.tableName); } + + private isShowingTableName(): boolean { + return this.tableName.parent !== undefined; + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index cb4b999063..821b9d14b6 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -1,7 +1,7 @@ //! Tables renders all pixi-based UI elements for tables. Right now that's the //! headings. -import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; @@ -17,6 +17,7 @@ export class Tables extends Container
{ private activeTable: Table | undefined; private hoverTable: Table | undefined; private contextMenuTable: Table | undefined; + private renameDataTable: Table | undefined; constructor(cellsSheet: CellsSheet) { super(); @@ -52,7 +53,7 @@ export class Tables extends Container
{ const table = this.children.find((table) => table.codeCell.x === x && table.codeCell.y === y); if (table) { if (!renderCodeCell) { - table.removeTableNames(); + table.hideTableName(); table.destroy(); } else { table.updateCodeCell(renderCodeCell); @@ -105,11 +106,11 @@ export class Tables extends Container
{ private changeSheet = (sheetId: string) => { if (sheetId === this.sheet.id) { this.children.forEach((table) => { - table.addTableNames(); + table.showTableName(); }); } else { this.children.forEach((table) => { - table.removeTableNames(); + table.hideTableName(); }); } }; @@ -117,7 +118,8 @@ export class Tables extends Container
{ // Checks if the mouse cursor is hovering over a table or table heading. checkHover(world: Point) { const hover = this.children.find((table) => table.checkHover(world)); - if (hover && (hover === this.contextMenuTable || hover === this.activeTable)) { + // if we already have the active table open, then don't show hover + if (hover && (hover === this.contextMenuTable || hover === this.activeTable || hover === this.renameDataTable)) { return; } if (hover !== this.hoverTable) { @@ -143,7 +145,14 @@ export class Tables extends Container
{ // track and activate a table whose context menu is open (this handles the // case where you hover a table and open the context menu; we want to keep // that table active while the context menu is open) - contextMenu = (options?: { type?: ContextMenuType; table?: JsRenderCodeCell }) => { + contextMenu = (options?: { type?: ContextMenuType; table?: JsRenderCodeCell; special?: ContextMenuSpecial }) => { + // we keep the former context menu table active after the rename finishes + // until the cursor moves again. + if (this.renameDataTable) { + this.renameDataTable.showTableName(); + this.hoverTable = this.renameDataTable; + this.renameDataTable = undefined; + } if (this.contextMenuTable) { // we keep the former context menu table active after the context // menu closes until the cursor moves again. @@ -155,14 +164,36 @@ export class Tables extends Container
{ return; } if (options.type === ContextMenuType.Table && options.table) { - this.contextMenuTable = this.children.find((table) => table.codeCell === options.table); - if (this.contextMenuTable) { - this.contextMenuTable.showActive(); - if (this.hoverTable === this.contextMenuTable) { + if (options.special === ContextMenuSpecial.rename) { + this.renameDataTable = this.children.find((table) => table.codeCell === options.table); + if (this.renameDataTable) { + this.renameDataTable.showActive(); + this.renameDataTable.hideTableName(); this.hoverTable = undefined; } + } else { + this.contextMenuTable = this.children.find((table) => table.codeCell === options.table); + if (this.contextMenuTable) { + this.contextMenuTable.showActive(); + if (this.hoverTable === this.contextMenuTable) { + this.hoverTable = undefined; + } + } } } pixiApp.setViewportDirty(); }; + + getTableNamePosition(x: number, y: number): { x: number; y: number; width: number; height: number } { + const table = this.children.find((table) => table.codeCell.x === x && table.codeCell.y === y); + if (!table) { + return { x: 0, y: 0, width: 0, height: 0 }; + } + return { + x: table.tableNameBounds.x, + y: table.tableNameBounds.y, + width: table.tableNameBounds.width, + height: table.tableNameBounds.height, + }; + } } diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/useKeyboard.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/useKeyboard.ts index bcb50ca062..19d3198552 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/useKeyboard.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/useKeyboard.ts @@ -30,7 +30,7 @@ export const useKeyboard = (): { onKeyUp: (event: React.KeyboardEvent) => void; } => { const onKeyDown = (event: React.KeyboardEvent) => { - if (pixiAppSettings.input.show && inlineEditorHandler.isOpen()) return; + if ((pixiAppSettings.input.show && inlineEditorHandler.isOpen()) || pixiAppSettings.isRenamingTable()) return; if ( keyboardPanMode(event) || keyboardLink(event) || diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts index e71425289f..682ca65924 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts @@ -129,7 +129,8 @@ export class Pointer { this.pointerAutoComplete.pointerMove(world) || this.pointerDown.pointerMove(world, event) || this.pointerCursor.pointerMove(world) || - this.pointerLink.pointerMove(world, event); + this.pointerLink.pointerMove(world, event) || + this.pointerTable.pointerMove(); this.updateCursor(); }; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index 271074b1d9..dcd10a55cb 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -1,24 +1,64 @@ -import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; +//! Handles pointer events for data tables. + +import { ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; +import { DOUBLE_CLICK_TIME } from '@/app/gridGL/interaction/pointer/pointerUtils'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { isMac } from '@/shared/utils/isMac'; import { Point } from 'pixi.js'; +// todo: dragging on double click + export class PointerTable { + private doubleClickTimeout: number | undefined; + pointerDown(world: Point, event: PointerEvent): boolean { const result = pixiApp.cellsSheets.current?.tables.pointerDown(world); if (!result) { return false; } if (event.button === 2 || (isMac && event.button === 0 && event.ctrlKey)) { - events.emit('contextMenu', { type: ContextMenuType.Table, world, table: result.table }); + events.emit('contextMenu', { + type: ContextMenuType.Table, + world, + column: result.table.x, + row: result.table.y, + table: result.table, + }); } if (result.nameOrDropdown === 'name') { - // todo: dragging and/or renaming on double click + if (this.doubleClickTimeout) { + events.emit('contextMenu', { + type: ContextMenuType.Table, + world, + column: result.table.x, + row: result.table.y, + table: result.table, + special: ContextMenuSpecial.rename, + }); + } else { + this.doubleClickTimeout = window.setTimeout(() => { + this.doubleClickTimeout = undefined; + }, DOUBLE_CLICK_TIME); + } } else if (result.nameOrDropdown === 'dropdown') { - events.emit('contextMenu', { type: ContextMenuType.Table, world, table: result.table }); + events.emit('contextMenu', { + type: ContextMenuType.Table, + column: result.table.x, + row: result.table.y, + world, + table: result.table, + }); } return true; } + + pointerMove(): boolean { + if (this.doubleClickTimeout) { + clearTimeout(this.doubleClickTimeout); + this.doubleClickTimeout = undefined; + } + return false; + } } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index f4afe5290f..884b1991ba 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -1,5 +1,11 @@ import { CodeEditorState, defaultCodeEditorState } from '@/app/atoms/codeEditorAtom'; -import { ContextMenuOptions, ContextMenuState, defaultContextMenuState } from '@/app/atoms/contextMenuAtom'; +import { + ContextMenuOptions, + ContextMenuSpecial, + ContextMenuState, + ContextMenuType, + defaultContextMenuState, +} from '@/app/atoms/contextMenuAtom'; import { EditorInteractionState, editorInteractionStateDefault } from '@/app/atoms/editorInteractionStateAtom'; import { defaultGridPanMode, GridPanMode, PanMode } from '@/app/atoms/gridPanModeAtom'; import { defaultGridSettings, GridSettings } from '@/app/atoms/gridSettingsAtom'; @@ -230,6 +236,10 @@ class PixiAppSettings { private getContextSettings = (contextMenu: ContextMenuState) => { this.contextMenu = contextMenu; }; + + isRenamingTable(): boolean { + return this.contextMenu.type === ContextMenuType.Table && this.contextMenu.special === ContextMenuSpecial.rename; + } } export const pixiAppSettings = new PixiAppSettings(); From 8abf65af4ce7dde62926a1d4cf4e075a0c36ed76 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 09:23:02 -0700 Subject: [PATCH 056/373] ui tweaks --- quadratic-client/src/app/actions/actions.ts | 1 + .../src/app/actions/dataTableSpec.ts | 24 +++++++-- .../contextMenus/TableContextMenu.tsx | 1 + .../src/app/gridGL/cells/tables/Table.ts | 49 +++++++++++-------- .../src/app/gridGL/cells/tables/Tables.ts | 3 ++ .../src/shared/components/Icons.tsx | 8 +++ 6 files changed, 62 insertions(+), 24 deletions(-) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index b5c25ea2c4..f18cb76da6 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -144,4 +144,5 @@ export enum Action { GridToDataTable = 'grid_to_data_table', ToggleFirstRowAsHeaderDataTable = 'toggle_first_row_as_header_data_table', RenameDataTable = 'rename_data_table', + ToggleHeaderDataTable = 'toggle_header_data_table', } diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 971f939a4f..a4232b7067 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -4,13 +4,17 @@ import { events } from '@/app/events/events'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -import { FileRenameIcon, PersonAddIcon } from '@/shared/components/Icons'; +import { FileRenameIcon, TableConvertIcon } from '@/shared/components/Icons'; import { sheets } from '../grid/controller/Sheets'; import { ActionSpecRecord } from './actionsSpec'; type DataTableSpec = Pick< ActionSpecRecord, - Action.FlattenDataTable | Action.GridToDataTable | Action.ToggleFirstRowAsHeaderDataTable | Action.RenameDataTable + | Action.FlattenDataTable + | Action.GridToDataTable + | Action.ToggleFirstRowAsHeaderDataTable + | Action.RenameDataTable + | Action.ToggleHeaderDataTable >; export type DataTableActionArgs = { @@ -25,10 +29,14 @@ const isFirstRowHeader = (): boolean => { return !!pixiAppSettings.contextMenu.table?.first_row_header; }; +const isHeadingShowing = (): boolean => { + return !!pixiAppSettings.contextMenu.table?.show_header; +}; + export const dataTableSpec: DataTableSpec = { [Action.FlattenDataTable]: { label: 'Flatten data table', - Icon: PersonAddIcon, + Icon: TableConvertIcon, run: async () => { const { x, y } = sheets.sheet.cursor.cursorPosition; quadraticCore.flattenDataTable(sheets.sheet.id, x, y, sheets.getCursorPosition()); @@ -36,7 +44,7 @@ export const dataTableSpec: DataTableSpec = { }, [Action.GridToDataTable]: { label: 'Convert values to data table', - Icon: PersonAddIcon, + Icon: TableConvertIcon, isAvailable: () => !isDataTable(), run: async () => { quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); @@ -90,4 +98,12 @@ export const dataTableSpec: DataTableSpec = { } }, }, + [Action.ToggleHeaderDataTable]: { + label: 'Show column headings', + checkbox: isHeadingShowing, + run: async () => { + // const { x, y } = sheets.sheet.cursor.cursorPosition; + // quadraticCore.dataTableShowHeadings(sheets.sheet.id, x, y, sheets.getCursorPosition()); + }, + }, }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index d245be9399..6f8882ab1f 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -54,6 +54,7 @@ export const TableContextMenu = () => { menuClassName="bg-background" > + diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 3c67ebcdaa..f48ee90daa 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -79,23 +79,29 @@ export class Table extends Container { background.endFill(); // create column headings - let x = 0; - this.columns = codeCell.column_names.map((column, index) => { - const width = this.sheet.offsets.getColumnWidth(codeCell.x + index); - const bounds = new Rectangle(x, this.headingBounds.y, width, this.headingBounds.height); - const heading = this.headingContainer.addChild(new Container()); - heading.position.set(x + OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); - heading.addChild( - new BitmapText(column.name, { - fontName: 'OpenSans-Bold', - fontSize: FONT_SIZE, - tint: colors.tableHeadingForeground, - }) - ); - - x += width; - return { heading, bounds }; - }); + if (codeCell.show_header) { + this.headingContainer.visible = true; + let x = 0; + this.columns = codeCell.column_names.map((column, index) => { + const width = this.sheet.offsets.getColumnWidth(codeCell.x + index); + const bounds = new Rectangle(x, this.headingBounds.y, width, this.headingBounds.height); + const heading = this.headingContainer.addChild(new Container()); + heading.position.set(x + OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); + heading.addChild( + new BitmapText(column.name, { + fontName: 'OpenSans-Bold', + fontSize: FONT_SIZE, + tint: colors.tableHeadingForeground, + }) + ); + + x += width; + return { heading, bounds }; + }); + } else { + this.columns = []; + this.headingContainer.visible = false; + } // draw outline around entire table this.addChild(this.outline); @@ -115,14 +121,17 @@ export class Table extends Container { text.position.set(OPEN_SANS_FIX.x + TABLE_NAME_PADDING, OPEN_SANS_FIX.y - this.headingBounds.height); const dropdown = this.tableName.addChild(this.drawDropdown()); - dropdown.position.set(text.width + OPEN_SANS_FIX.x + DROPDOWN_PADDING, -this.headingHeight / 2); + dropdown.position.set( + text.width + OPEN_SANS_FIX.x + DROPDOWN_PADDING + TABLE_NAME_PADDING, + -this.headingHeight / 2 + ); nameBackground.beginFill(getCSSVariableTint('primary')); nameBackground.drawShape( new Rectangle( 0, -this.headingBounds.height, - text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING, + text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING + TABLE_NAME_PADDING, this.headingBounds.height ) ); @@ -130,7 +139,7 @@ export class Table extends Container { this.tableNameBounds = new Rectangle( this.tableBounds.x, this.tableBounds.y - this.headingHeight, - text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING, + text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING + TABLE_NAME_PADDING, this.headingBounds.height ); }; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 821b9d14b6..fbf5216448 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -94,6 +94,9 @@ export class Tables extends Container
{ } const cursor = sheets.sheet.cursor.cursorPosition; this.activeTable = this.children.find((table) => table.intersectsCursor(cursor.x, cursor.y)); + if (this.hoverTable === this.activeTable) { + this.hoverTable = undefined; + } }; // Redraw the headings if the offsets change. diff --git a/quadratic-client/src/shared/components/Icons.tsx b/quadratic-client/src/shared/components/Icons.tsx index 350b456148..fad4a409b9 100644 --- a/quadratic-client/src/shared/components/Icons.tsx +++ b/quadratic-client/src/shared/components/Icons.tsx @@ -430,3 +430,11 @@ export const ZoomInIcon: IconComponent = (props) => { export const ZoomOutIcon: IconComponent = (props) => { return zoom_out; }; + +export const TableEditIcon: IconComponent = (props) => { + return table_edit; +}; + +export const TableConvertIcon: IconComponent = (props) => { + return table_convert; +}; From 46fb6cb54af2a4541b1fdab8a54cba94e3639d30 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 09:26:44 -0700 Subject: [PATCH 057/373] fix table stickiness of table name --- quadratic-client/src/app/gridGL/cells/tables/Table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index f48ee90daa..b8dee2286e 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -155,7 +155,7 @@ export class Table extends Container { private tableNamePosition = (bounds: Rectangle, gridHeading: number) => { if (this.visible) { if (this.tableBounds.y < bounds.top + gridHeading) { - this.tableName.y = bounds.top + gridHeading - this.tableBounds.top; + this.tableName.y = bounds.top + gridHeading; } else { this.tableName.y = this.tableBounds.top; } From 29eb6381df7a25e29c1c9284dd210d03ca93b766 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 09:29:55 -0700 Subject: [PATCH 058/373] adding menu divider --- .../src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 6f8882ab1f..658b1c612b 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -54,6 +54,7 @@ export const TableContextMenu = () => { menuClassName="bg-background" > + From eaab3af872a6f943a5b9ec407841c8b67107c3b8 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 10:17:09 -0700 Subject: [PATCH 059/373] fixing isAvailable code --- .../src/app/actions/dataTableSpec.ts | 10 ++---- .../HTMLGrid/contextMenus/GridContextMenu.tsx | 35 +++++++++++++------ 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index a4232b7067..d6af111346 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -1,7 +1,6 @@ import { Action } from '@/app/actions/actions'; import { ContextMenuSpecial } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; -import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { FileRenameIcon, TableConvertIcon } from '@/shared/components/Icons'; @@ -21,16 +20,12 @@ export type DataTableActionArgs = { [Action.FlattenDataTable]: { name: string }; }; -const isDataTable = (): boolean => { - return pixiApp.isCursorOnCodeCellOutput(); -}; - const isFirstRowHeader = (): boolean => { - return !!pixiAppSettings.contextMenu.table?.first_row_header; + return !!pixiAppSettings.contextMenu?.table?.first_row_header; }; const isHeadingShowing = (): boolean => { - return !!pixiAppSettings.contextMenu.table?.show_header; + return !!pixiAppSettings.contextMenu?.table?.show_header; }; export const dataTableSpec: DataTableSpec = { @@ -45,7 +40,6 @@ export const dataTableSpec: DataTableSpec = { [Action.GridToDataTable]: { label: 'Convert values to data table', Icon: TableConvertIcon, - isAvailable: () => !isDataTable(), run: async () => { quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); }, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index 6216354bd4..1017f7be6e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -2,11 +2,12 @@ import { Action } from '@/app/actions/actions'; import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { focusGrid } from '@/app/helpers/focusGrid'; import { ControlledMenu, MenuDivider } from '@szhsin/react-menu'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useRecoilState } from 'recoil'; import { pixiApp } from '../../pixiApp/PixiApp'; @@ -32,8 +33,22 @@ export const GridContextMenu = () => { const ref = useRef(null); - const isColumnRowAvailable = sheets.sheet.cursor.hasOneColumnRowSelection(true); - const isMultiSelectOnly = sheets.sheet.cursor.hasOneMultiselect(); + const [columnRowAvailable, setColumnRowAvailable] = useState(false); + const [multiSelectOnly, setMultiSelectOnly] = useState(false); + + useEffect(() => { + const updateCursor = () => { + setColumnRowAvailable(sheets.sheet.cursor.hasOneColumnRowSelection(true)); + setMultiSelectOnly(sheets.sheet.cursor.hasOneMultiselect()); + }; + + updateCursor(); + events.on('cursorPosition', updateCursor); + + return () => { + events.off('cursorPosition', updateCursor); + }; + }, []); return (
{ {contextMenu.column === null ? null : ( <> - {isColumnRowAvailable && } - {isColumnRowAvailable && } + {columnRowAvailable && } + {columnRowAvailable && } )} {contextMenu.row === null ? null : ( <> - {isColumnRowAvailable && } - {isColumnRowAvailable && } - {isColumnRowAvailable && } + {columnRowAvailable && } + {columnRowAvailable && } + {columnRowAvailable && } )} - - {isMultiSelectOnly && } + {multiSelectOnly && } + {multiSelectOnly && }
); From 2c423bab730a81af8a9f0938bb7c9d074d39c68d Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 10:51:25 -0700 Subject: [PATCH 060/373] fix undo --- .vscode/settings.json | 2 ++ .../execution/execute_operation/execute_data_table.rs | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8532f157ef..e32ce15b75 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,6 +58,8 @@ "rust-analyzer.checkOnSave": true, "rust-analyzer.cargo.unsetTest": true, // "rust-analyzer.checkOnSave.command": "clippy", + "rust-analyzer.server.extraEnv": { "CARGO_TARGET_DIR": "target/rust-analyzer" }, + "rust-analyzer.runnables.extraEnv": { "CARGO_TARGET_DIR": "target/rust-analyzer" }, "files.associations": { "*.grid": "json" }, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 003d7ed321..ff41223a23 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -49,7 +49,6 @@ impl GridController { transaction.forward_operations.extend(forward_operations); if transaction.is_user() { - // self.check_deleted_data_tables(transaction, sheet_rect); self.add_compute_operations(transaction, sheet_rect, None); self.check_all_spills(transaction, sheet_rect.sheet_id, true); } @@ -184,6 +183,7 @@ impl GridController { } self.send_to_wasm(transaction, &sheet_rect)?; + transaction.add_code_cell(sheet_id, data_table_pos); let forward_operations = vec![Operation::FlattenDataTable { sheet_pos }]; @@ -195,6 +195,7 @@ impl GridController { forward_operations, reverse_operations, ); + self.check_deleted_data_tables(transaction, &sheet_rect); return Ok(()); }; @@ -235,10 +236,7 @@ impl GridController { let forward_operations = vec![Operation::GridToDataTable { sheet_rect }]; - let reverse_operations = vec![Operation::SetCellValues { - sheet_pos, - values: CellValues::from(old_values), - }]; + let reverse_operations = vec![Operation::FlattenDataTable { sheet_pos }]; self.data_table_operations( transaction, From 1117e818f5ac90a1105108d4d64d65c1880b1dd3 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 10:58:33 -0700 Subject: [PATCH 061/373] don't allow converting to a table within a table --- quadratic-client/src/app/grid/sheet/SheetCursor.ts | 14 ++++++++++++-- .../HTMLGrid/contextMenus/GridContextMenu.tsx | 9 ++++----- .../src/app/gridGL/cells/tables/Table.ts | 8 ++++++++ .../src/app/gridGL/cells/tables/Tables.ts | 7 ++++++- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/quadratic-client/src/app/grid/sheet/SheetCursor.ts b/quadratic-client/src/app/grid/sheet/SheetCursor.ts index 7de7dcbc48..39485f0c84 100644 --- a/quadratic-client/src/app/grid/sheet/SheetCursor.ts +++ b/quadratic-client/src/app/grid/sheet/SheetCursor.ts @@ -300,7 +300,17 @@ export class SheetCursor { } // Returns true if there is one multiselect of > 1 size - hasOneMultiselect(): boolean { - return this.multiCursor?.length === 1 && (this.multiCursor[0].width > 1 || this.multiCursor[0].height > 1); + canConvertToDataTable(): boolean { + const tables = pixiApp.cellsSheets.current?.tables; + if (!tables) return false; + if ( + !this.multiCursor || + this.multiCursor?.length !== 1 || + (this.multiCursor[0].width === 1 && this.multiCursor[0].height === 1) + ) { + return false; + } + + return !tables.intersects(this.multiCursor[0]); } } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index 1017f7be6e..0203c1244f 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -34,12 +34,11 @@ export const GridContextMenu = () => { const ref = useRef(null); const [columnRowAvailable, setColumnRowAvailable] = useState(false); - const [multiSelectOnly, setMultiSelectOnly] = useState(false); - + const [canConvertToDataTable, setCanConvertToDataTable] = useState(false); useEffect(() => { const updateCursor = () => { setColumnRowAvailable(sheets.sheet.cursor.hasOneColumnRowSelection(true)); - setMultiSelectOnly(sheets.sheet.cursor.hasOneMultiselect()); + setCanConvertToDataTable(sheets.sheet.cursor.canConvertToDataTable()); }; updateCursor(); @@ -95,8 +94,8 @@ export const GridContextMenu = () => { )} - {multiSelectOnly && } - {multiSelectOnly && } + {canConvertToDataTable && } + {canConvertToDataTable && } ); diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index b8dee2286e..f9c8e6c192 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -239,4 +239,12 @@ export class Table extends Container { private isShowingTableName(): boolean { return this.tableName.parent !== undefined; } + + // Intersects a column/row rectangle + intersects(rectangle: Rectangle): boolean { + return intersects.rectangleRectangle( + new Rectangle(this.codeCell.x, this.codeCell.y, this.codeCell.w - 1, this.codeCell.h - 1), + rectangle + ); + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index fbf5216448..dd8131431f 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -9,7 +9,7 @@ import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; import { Table } from '@/app/gridGL/cells/tables/Table'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { JsCodeCell, JsRenderCodeCell } from '@/app/quadratic-core-types'; -import { Container, Point } from 'pixi.js'; +import { Container, Point, Rectangle } from 'pixi.js'; export class Tables extends Container
{ private cellsSheet: CellsSheet; @@ -199,4 +199,9 @@ export class Tables extends Container
{ height: table.tableNameBounds.height, }; } + + // Intersects a column/row rectangle + intersects(rectangle: Rectangle): boolean { + return this.children.some((table) => table.intersects(rectangle)); + } } From 475b37a048a71318540e1b701b7d3ffeb2c94377 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 11:02:25 -0700 Subject: [PATCH 062/373] fix bug with flatten table --- quadratic-client/src/app/actions/dataTableSpec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index d6af111346..8a73ba497d 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -33,8 +33,10 @@ export const dataTableSpec: DataTableSpec = { label: 'Flatten data table', Icon: TableConvertIcon, run: async () => { - const { x, y } = sheets.sheet.cursor.cursorPosition; - quadraticCore.flattenDataTable(sheets.sheet.id, x, y, sheets.getCursorPosition()); + const table = pixiAppSettings.contextMenu?.table; + if (table) { + quadraticCore.flattenDataTable(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); + } }, }, [Action.GridToDataTable]: { From 1aeaef78ab77d9799451b4c37100aa1602097445 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 11:21:51 -0700 Subject: [PATCH 063/373] fix bugs with update code cell in tables --- quadratic-client/src/app/gridGL/cells/tables/Table.ts | 7 +++++++ .../src/controller/execution/control_transaction.rs | 7 ++----- quadratic-core/src/grid/data_table.rs | 8 ++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index f9c8e6c192..ef5896e6df 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -59,6 +59,10 @@ export class Table extends Container { } updateCodeCell = (codeCell: JsRenderCodeCell) => { + this.removeChildren(); + pixiApp.overHeadings.removeChild(this.tableName); + this.tableName.removeChildren(); + this.codeCell = codeCell; this.tableBounds = this.sheet.getScreenRectangle(codeCell.x, codeCell.y, codeCell.w - 1, codeCell.h - 1); this.headingHeight = this.sheet.offsets.getRowHeight(codeCell.y); @@ -71,6 +75,7 @@ export class Table extends Container { this.position.set(this.headingBounds.x, this.headingBounds.y); this.addChild(this.headingContainer); + this.headingContainer.removeChildren(); // draw heading background const background = this.headingContainer.addChild(new Graphics()); @@ -105,6 +110,7 @@ export class Table extends Container { // draw outline around entire table this.addChild(this.outline); + this.outline.clear(); this.outline.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); this.outline.drawShape(new Rectangle(0, 0, this.tableBounds.width, this.tableBounds.height)); this.outline.visible = false; @@ -113,6 +119,7 @@ export class Table extends Container { if (sheets.sheet.id === this.sheet.id) { pixiApp.overHeadings.addChild(this.tableName); } + this.tableName.removeChildren(); this.tableName.position.set(this.tableBounds.x, this.tableBounds.y); const nameBackground = this.tableName.addChild(new Graphics()); this.tableName.visible = false; diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index d9891adb55..a49ec6c21a 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -300,16 +300,13 @@ impl GridController { }; let sheet = self.try_sheet_result(current_sheet_pos.sheet_id)?; - - // todo: this should be true sometimes... - let show_header = false; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), &sheet.next_data_table_name(), value, false, - false, - show_header, + true, + true, ); self.finalize_code_run(&mut transaction, current_sheet_pos, Some(data_table), None); diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 189e480e31..51b7fe25c1 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -117,7 +117,7 @@ impl DataTable { DataTableKind::Import(_) => false, }; - let data_table = DataTable { + let mut data_table = DataTable { kind, name: name.into(), header_is_first_row, @@ -131,9 +131,9 @@ impl DataTable { last_modified: Utc::now(), }; - // if has_header { - // data_table.apply_header_from_first_row(); - // } + if header_is_first_row { + data_table.apply_first_row_as_header(); + } data_table } From f53b6b17ed7b43ce6202b10e4d0283ac8add083b Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 18 Oct 2024 13:15:06 -0600 Subject: [PATCH 064/373] Make headers great again --- quadratic-core/src/bin/export_types.rs | 9 +- .../src/controller/operations/import.rs | 2 +- .../src/controller/user_actions/import.rs | 18 ---- quadratic-core/src/grid/data_table.rs | 92 +++++++++++++------ .../src/grid/file/serialize/data_table.rs | 12 ++- quadratic-core/src/grid/file/v1_8/schema.rs | 2 +- quadratic-core/src/grid/js_types.rs | 21 ++++- quadratic-core/src/grid/sheet.rs | 2 +- quadratic-core/src/grid/sheet/rendering.rs | 17 ++-- quadratic-rust-shared/src/auto_gen_path.rs | 4 +- 10 files changed, 112 insertions(+), 67 deletions(-) diff --git a/quadratic-core/src/bin/export_types.rs b/quadratic-core/src/bin/export_types.rs index d40a07e836..1fcdaa55b6 100644 --- a/quadratic-core/src/bin/export_types.rs +++ b/quadratic-core/src/bin/export_types.rs @@ -5,8 +5,8 @@ use controller::operations::clipboard::PasteSpecial; use formulas::{CellRef, CellRefCoord, RangeRef}; use grid::formats::format::Format; use grid::js_types::{ - CellFormatSummary, JsCellValue, JsClipboard, JsOffset, JsPos, JsRenderFill, JsRowHeight, - JsSheetFill, JsValidationWarning, + CellFormatSummary, JsCellValue, JsClipboard, JsDataTableColumn, JsOffset, JsPos, JsRenderFill, + JsRowHeight, JsSheetFill, JsValidationWarning, }; use grid::sheet::borders::{BorderStyleCell, BorderStyleTimestamp}; use grid::sheet::validations::validation::{ @@ -28,8 +28,7 @@ use grid::sheet::validations::validation_rules::validation_text::{ }; use grid::sheet::validations::validation_rules::ValidationRule; use grid::{ - CellAlign, CellVerticalAlign, CellWrap, DataTableColumn, GridBounds, NumericFormat, - NumericFormatKind, SheetId, + CellAlign, CellVerticalAlign, CellWrap, GridBounds, NumericFormat, NumericFormatKind, SheetId, }; use quadratic_core::color::Rgba; use quadratic_core::controller::active_transactions::transaction_name::TransactionName; @@ -83,7 +82,6 @@ fn main() { CodeCellLanguage, ColumnRow, ConnectionKind, - DataTableColumn, DateTimeRange, Duration, Format, @@ -96,6 +94,7 @@ fn main() { JsClipboard, JsCodeCell, JsCodeResult, + JsDataTableColumn, JsGetCellResponse, JsHtmlOutput, JsNumber, diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index e8da1c1bba..5e5e9e1419 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -387,7 +387,7 @@ impl GridController { .ok_or_else(|| anyhow!("Sheet {sheet_id} not found"))?; let sheet_pos = SheetPos::from((insert_at, sheet_id)); let mut data_table = DataTable::from((import.to_owned(), cell_values, sheet)); - data_table.header_is_first_row = true; + data_table.apply_first_row_as_header(); // this operation must be before the SetCodeRun operations ops.push(Operation::SetCellValues { diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index 8748af4f54..a27e0de49b 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -170,9 +170,7 @@ pub(crate) mod tests { fn import_large_csv() { clear_js_calls(); let mut gc = GridController::test(); - let sheet_id = gc.grid.sheets()[0].id; let mut csv = String::new(); - let file_name = "large.csv"; for _ in 0..10000 { for x in 0..10 { @@ -190,10 +188,6 @@ pub(crate) mod tests { ) .unwrap(); - let import = Import::new(file_name.into()); - let cell_value = CellValue::Import(import); - assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); - expect_js_call_count("jsImportProgress", 1, true); } @@ -361,10 +355,6 @@ pub(crate) mod tests { let file: Vec = std::fs::read(PARQUET_FILE).expect("Failed to read file"); let _result = grid_controller.import_parquet(sheet_id, file, file_name, pos, None); - let import = Import::new(file_name.into()); - let cell_value = CellValue::Import(import); - assert_display_cell_value(&grid_controller, sheet_id, 0, 0, &cell_value.to_string()); - assert_data_table_cell_value_row( &grid_controller, sheet_id, @@ -495,10 +485,6 @@ pub(crate) mod tests { print_data_table(&gc, sheet_id, Rect::new_span(pos, Pos { x: 3, y: 4 })); - let import = Import::new(file_name.into()); - let cell_value = CellValue::Import(import); - assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); - assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, vec!["Sample report ", "", ""]); assert_data_table_cell_value_row( &gc, @@ -526,10 +512,6 @@ pub(crate) mod tests { print_table(&gc, sheet_id, Rect::new_span(pos, Pos { x: 2, y: 3 })); - let import = Import::new(file_name.into()); - let cell_value = CellValue::Import(import); - assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); - assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, vec!["issue", " test", " value"]); assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 1, vec!["0", " 1", " Invalid"]); assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 2, vec!["0", " 2", " Valid"]); diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 189e480e31..33cdd14658 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -7,6 +7,7 @@ use std::fmt::{Display, Formatter}; use crate::cellvalue::Import; +use crate::grid::js_types::JsDataTableColumn; use crate::grid::CodeRun; use crate::{ Array, ArraySize, CellValue, Pos, Rect, RunError, RunErrorMsg, SheetPos, SheetRect, Value, @@ -19,13 +20,12 @@ use tabled::{ builder::Builder, settings::{Color, Modify, Style}, }; -use ts_rs::TS; use super::Sheet; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct DataTableColumn { - pub name: String, + pub name: CellValue, pub display: bool, pub value_index: u32, } @@ -39,7 +39,7 @@ pub enum DataTableKind { impl DataTableColumn { pub fn new(name: String, display: bool, value_index: u32) -> Self { DataTableColumn { - name, + name: CellValue::Text(name), display, value_index, } @@ -117,7 +117,7 @@ impl DataTable { DataTableKind::Import(_) => false, }; - let data_table = DataTable { + let mut data_table = DataTable { kind, name: name.into(), header_is_first_row, @@ -131,9 +131,11 @@ impl DataTable { last_modified: Utc::now(), }; - // if has_header { - // data_table.apply_header_from_first_row(); - // } + if header_is_first_row { + data_table.apply_first_row_as_header(); + } + + // data_table.toggle_first_row_as_header(header_is_first_row); data_table } @@ -194,7 +196,7 @@ impl DataTable { match first_row_as_header { true => self.apply_first_row_as_header(), - false => self.columns = None, + false => self.apply_default_header(), } } @@ -244,7 +246,7 @@ impl DataTable { .as_mut() .and_then(|columns| columns.get_mut(index)) .map(|column| { - column.name = name; + column.name = CellValue::Text(name); column.display = display; }); @@ -265,13 +267,20 @@ impl DataTable { Ok(()) } + fn adjust_for_header(&self, index: usize) -> usize { + if self.header_is_first_row { + index + 1 + } else { + index + } + } + pub fn sort_column( &mut self, column_index: usize, direction: SortDirection, ) -> Result> { let old = self.prepend_sort(column_index, direction.clone()); - let increment = |i| if self.header_is_first_row { i + 1 } else { i }; // TODO(ddimaria): skip this if SortDirection::None if let Some(ref mut sort) = self.sort.to_owned() { @@ -280,14 +289,14 @@ impl DataTable { .display_value()? .into_array()? .col(sort.column_index) - .skip(increment(0)) + .skip(self.adjust_for_header(0)) .enumerate() - .sorted_by(|a, b| match direction { + .sorted_by(|a, b| match sort.direction { SortDirection::Ascending => a.1.total_cmp(b.1), SortDirection::Descending => b.1.total_cmp(a.1), SortDirection::None => std::cmp::Ordering::Equal, }) - .map(|(i, _)| increment(i) as u64) + .map(|(i, _)| self.adjust_for_header(i) as u64) .collect::>(); if self.header_is_first_row { @@ -366,6 +375,21 @@ impl DataTable { } pub fn display_value_at(&self, pos: Pos) -> Result<&CellValue> { + // println!("pos: {:?}", pos); + // println!("self.columns: {:?}", self.columns); + + if pos.y == 0 { + if let Some(columns) = &self.columns { + // println!("columns: {:?}", columns); + if let Some(column) = columns.get(pos.x as usize) { + // println!("column: {:?}", column); + return Ok(column.name.as_ref()); + } + } + } + + // pos.y = self.adjust_for_header(pos.y as usize) as i64; + match self.display_buffer { Some(ref display_buffer) => self.display_value_from_buffer_at(display_buffer, pos), None => Ok(self.value.get(pos.x as u32, pos.y as u32)?), @@ -403,7 +427,7 @@ impl DataTable { if self.spill_error { Some(CellValue::Blank) } else { - self.cell_value_ref_at(x, y).cloned() + self.display_value_at((x, y).into()).ok().cloned() } } @@ -539,14 +563,18 @@ impl DataTable { /// Prepares the columns to be sent to the client. If no columns are set, it /// will create default columns. - pub fn send_columns(&self) -> Vec { + pub fn send_columns(&self) -> Vec { match self.columns.as_ref() { - Some(columns) => columns.clone(), + Some(columns) => columns + .iter() + .map(|column| JsDataTableColumn::from(column.to_owned())) + .collect(), + // TODO(ddimaria): refacor this to use the default columns None => { let size = self.output_size(); (0..size.w.get()) - .map(|i| DataTableColumn::new(format!("Column {}", i + 1), true, i)) - .collect::>() + .map(|i| DataTableColumn::new(format!("Column {}", i + 1), true, i).into()) + .collect::>() } } } @@ -651,7 +679,10 @@ pub mod test { // test setting header at index data_table.set_header_at(0, "new".into(), true).unwrap(); - assert_eq!(data_table.columns.as_ref().unwrap()[0].name, "new"); + assert_eq!( + data_table.columns.as_ref().unwrap()[0].name, + CellValue::Text("new".into()) + ); // test setting header display at index data_table.set_header_display_at(0, false).unwrap(); @@ -669,7 +700,6 @@ pub mod test { // sort by population city ascending data_table.sort_column(0, SortDirection::Ascending).unwrap(); - println!("{:?}", data_table.sort); pretty_print_data_table(&data_table, Some("Sorted by City"), None); assert_data_table_row(&data_table, 1, values[2].clone()); assert_data_table_row(&data_table, 2, values[3].clone()); @@ -679,7 +709,6 @@ pub mod test { data_table .sort_column(3, SortDirection::Descending) .unwrap(); - println!("{:?}", data_table.sort); pretty_print_data_table(&data_table, Some("Sorted by Population Descending"), None); assert_data_table_row(&data_table, 1, values[2].clone()); assert_data_table_row(&data_table, 2, values[1].clone()); @@ -798,7 +827,8 @@ pub mod test { fn test_headers_y() { let mut sheet = Sheet::test(); let array = Array::from_str_vec(vec![vec!["first", "second"]], true).unwrap(); - let mut t = DataTable { + let pos = Pos { x: 1, y: 1 }; + let t = DataTable { kind: DataTableKind::Import(Import::new("test.csv".to_string())), name: "Table 1".into(), columns: None, @@ -812,15 +842,21 @@ pub mod test { header_is_first_row: true, }; sheet.set_cell_value( - Pos { x: 1, y: 1 }, + pos, Some(CellValue::Import(Import::new("test.csv".to_string()))), ); - sheet.set_data_table(Pos { x: 1, y: 1 }, Some(t.clone())); + sheet.set_data_table(pos, Some(t.clone())); assert_eq!( - sheet.display_value(Pos { x: 1, y: 1 }), + sheet.display_value(pos), Some(CellValue::Text("first".into())) ); - t.toggle_first_row_as_header(false); - assert_eq!(sheet.display_value(Pos { x: 1, y: 1 }), None); + + let data_table = sheet.data_table_mut((1, 1).into()).unwrap(); + data_table.toggle_first_row_as_header(false); + + assert_eq!( + sheet.display_value(pos), + Some(CellValue::Text("Column 1".into())) + ); } } diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 341288a8f6..19414728e8 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -173,8 +173,14 @@ pub(crate) fn import_data_table_builder( columns: data_table.columns.map(|columns| { columns .into_iter() - .map(|column| { - DataTableColumn::new(column.name, column.display, column.value_index) + .enumerate() + .map(|(index, column)| { + let column_name = match column.name { + current::CellValueSchema::Text(text) => text, + _ => format!("Column {}", index + 1), + }; + + DataTableColumn::new(column_name, column.display, column.value_index) }) .collect() }), @@ -348,7 +354,7 @@ pub(crate) fn export_data_tables( columns .into_iter() .map(|column| current::DataTableColumnSchema { - name: column.name, + name: current::CellValueSchema::Text(column.name.to_string()), display: column.display, value_index: column.value_index, }) diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index 7904ab537f..3eabbe914e 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -95,7 +95,7 @@ pub struct CodeRunSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DataTableColumnSchema { - pub name: String, + pub name: CellValueSchema, pub display: bool, pub value_index: u32, } diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index 6538f395e2..01e53ecc32 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -193,7 +193,7 @@ pub struct JsRenderCodeCell { pub state: JsRenderCodeCellState, pub spill_error: Option>, pub name: String, - pub column_names: Vec, + pub column_names: Vec, pub first_row_header: bool, pub show_header: bool, } @@ -236,6 +236,25 @@ pub struct JsValidationSheet { errors: Vec<(Pos, String)>, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "js", derive(ts_rs::TS))] +#[serde(rename_all = "camelCase")] +pub struct JsDataTableColumn { + pub name: String, + pub display: bool, + pub value_index: u32, +} + +impl From for JsDataTableColumn { + fn from(column: DataTableColumn) -> Self { + JsDataTableColumn { + name: column.name.to_string(), + display: column.display, + value_index: column.value_index, + } + } +} + #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] #[cfg_attr(feature = "js", derive(ts_rs::TS))] #[serde(rename_all = "camelCase")] diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index f0563214c3..45a165289f 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -229,7 +229,7 @@ impl Sheet { CellValue::Code(_) | CellValue::Import(_) => self .data_tables .get(&pos) - .and_then(|run| run.cell_value_at(0, 0)), + .and_then(|data_table| data_table.cell_value_at(0, 0)), CellValue::Blank => self.get_code_cell_value(pos), _ => Some(cell_value.clone()), } diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 6a094e44ec..842b4aca6f 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -645,15 +645,14 @@ mod tests { grid::{ formats::{format::Format, format_update::FormatUpdate, Formats}, js_types::{ - JsHtmlOutput, JsNumber, JsRenderCell, JsRenderCellSpecial, JsRenderCodeCell, - JsSheetFill, JsValidationWarning, + JsDataTableColumn, JsHtmlOutput, JsNumber, JsRenderCell, JsRenderCellSpecial, + JsRenderCodeCell, JsSheetFill, JsValidationWarning, }, sheet::validations::{ validation::{Validation, ValidationStyle}, validation_rules::{validation_logical::ValidationLogical, ValidationRule}, }, - Bold, CellVerticalAlign, CellWrap, CodeRun, DataTableColumn, DataTableKind, Italic, - RenderSize, + Bold, CellVerticalAlign, CellWrap, CodeRun, DataTableKind, Italic, RenderSize, }, selection::Selection, wasm_bindings::js::{clear_js_calls, expect_js_call, expect_js_call_count, hash_test}, @@ -1105,7 +1104,7 @@ mod tests { Value::Single(CellValue::Number(2.into())), false, false, - false, + true, ); sheet.set_data_table(pos, Some(data_table)); sheet.set_cell_value(pos, code); @@ -1121,8 +1120,8 @@ mod tests { state: crate::grid::js_types::JsRenderCodeCellState::Success, spill_error: None, name: "Table 1".to_string(), - column_names: vec![DataTableColumn { - name: "Column 1".to_string(), + column_names: vec![JsDataTableColumn { + name: "Column 1".into(), display: true, value_index: 0, }], @@ -1219,6 +1218,10 @@ mod tests { ]; let sheet = gc.sheet(sheet_id); let expected = sheet.get_render_cells(Rect::new(0, 0, 2, 0)); + + println!("{:?}", expected); + println!("{:?}", cells); + assert_eq!(expected.len(), cells.len()); assert!(expected.iter().all(|cell| cells.contains(cell))); diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index 8df3d2b4d5..a0dea21ae2 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From edfd3bc8f1c633e64160f0cfe7b46a15e4914c5d Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 12:58:32 -0700 Subject: [PATCH 065/373] add more to test_headers_y --- quadratic-core/src/grid/data_table.rs | 7 ++++++- quadratic-rust-shared/src/auto_gen_path.rs | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 33cdd14658..f641b720ec 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -826,7 +826,7 @@ pub mod test { #[parallel] fn test_headers_y() { let mut sheet = Sheet::test(); - let array = Array::from_str_vec(vec![vec!["first", "second"]], true).unwrap(); + let array = Array::from_str_vec(vec![vec!["first"], vec!["second"]], true).unwrap(); let pos = Pos { x: 1, y: 1 }; let t = DataTable { kind: DataTableKind::Import(Import::new("test.csv".to_string())), @@ -854,9 +854,14 @@ pub mod test { let data_table = sheet.data_table_mut((1, 1).into()).unwrap(); data_table.toggle_first_row_as_header(false); + pretty_print_data_table(&data_table, Some("Data Table"), None); assert_eq!( sheet.display_value(pos), Some(CellValue::Text("Column 1".into())) ); + assert_eq!( + sheet.display_value(Pos { x: 1, y: 2 }), + Some(CellValue::Text("first".into())) + ) } } diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index a0dea21ae2..8df3d2b4d5 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From 3bda9e2ed0f7a890271a634e0f30892075032844 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 18 Oct 2024 14:01:17 -0600 Subject: [PATCH 066/373] Checkpoint --- .../src/app/quadratic-core-types/index.d.ts | 4 ++-- quadratic-core/src/grid/data_table.rs | 9 ++++++++- quadratic-core/src/grid/sheet/rendering.rs | 12 ++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 66926a74ba..e4de21e4d5 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -17,7 +17,6 @@ export type CellWrap = "overflow" | "wrap" | "clip"; export type CodeCellLanguage = "Python" | "Formula" | { "Connection": { kind: ConnectionKind, id: string, } } | "Javascript" | "Import"; export interface ColumnRow { column: number, row: number, } export type ConnectionKind = "POSTGRES" | "MYSQL" | "MSSQL" | "SNOWFLAKE"; -export interface DataTableColumn { name: string, display: boolean, value_index: number, } export type DateTimeRange = { "DateRange": [bigint | null, bigint | null] } | { "DateEqual": Array } | { "DateNotEqual": Array } | { "TimeRange": [number | null, number | null] } | { "TimeEqual": Array } | { "TimeNotEqual": Array }; export interface Duration { months: number, seconds: number, } export interface Format { align: CellAlign | null, vertical_align: CellVerticalAlign | null, wrap: CellWrap | null, numeric_format: NumericFormat | null, numeric_decimals: number | null, numeric_commas: boolean | null, bold: boolean | null, italic: boolean | null, text_color: string | null, fill_color: string | null, render_size: RenderSize | null, date_time: string | null, underline: boolean | null, strike_through: boolean | null, } @@ -30,6 +29,7 @@ export interface JsCellValue { value: string, kind: string, } export interface JsClipboard { plainText: string, html: string, } export interface JsCodeCell { x: bigint, y: bigint, code_string: string, language: CodeCellLanguage, std_out: string | null, std_err: string | null, evaluation_result: string | null, spill_error: Array | null, return_info: JsReturnInfo | null, cells_accessed: Array | null, } export interface JsCodeResult { transaction_id: string, success: boolean, std_out: string | null, std_err: string | null, line_number: number | null, output_value: Array | null, output_array: Array>> | null, output_display_type: string | null, cancel_compute: boolean | null, } +export interface JsDataTableColumn { name: string, display: boolean, valueIndex: number, } export interface JsGetCellResponse { x: bigint, y: bigint, value: string, type_name: string, } export interface JsHtmlOutput { sheet_id: string, x: bigint, y: bigint, html: string | null, w: string | null, h: string | null, } export interface JsNumber { decimals: number | null, commas: boolean | null, format: NumericFormat | null, } @@ -37,7 +37,7 @@ export interface JsOffset { column: number | null, row: number | null, size: num export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableHeading"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, show_header: boolean, } +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, show_header: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 33cdd14658..0d693213a9 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -378,7 +378,7 @@ impl DataTable { // println!("pos: {:?}", pos); // println!("self.columns: {:?}", self.columns); - if pos.y == 0 { + if pos.y == 0 && self.show_header { if let Some(columns) = &self.columns { // println!("columns: {:?}", columns); if let Some(column) = columns.get(pos.x as usize) { @@ -647,6 +647,13 @@ pub mod test { assert_eq!(data_table, expected_data_table); assert_eq!(data_table.output_size(), expected_array_size); + pretty_print_data_table(&data_table, None, None); + + println!( + "Data Table: {:?}", + data_table.display_value_at((0, 1).into()).unwrap() + ); + // test default column headings data_table.apply_default_header(); let expected_columns = vec![ diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 842b4aca6f..ace33350fc 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -213,12 +213,12 @@ impl Sheet { let column = self.get_column(x); for y in y_start..=y_end { // We skip rendering the heading row because we render it separately. - if y == code_rect.min.y - && data_table.show_header - && data_table.header_is_first_row - { - continue; - } + // if y == code_rect.min.y + // && data_table.show_header + // && data_table.header_is_first_row + // { + // continue; + // } let value = data_table.cell_value_at( (x - code_rect.min.x) as u32, (y - code_rect.min.y) as u32, From 38445c3baad102cc260a8d29880feb9d5e70e676 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 18 Oct 2024 15:28:16 -0600 Subject: [PATCH 067/373] Handle headers better --- .../active_transactions/pending_transaction.rs | 4 ++-- .../src/controller/execution/control_transaction.rs | 7 +++++-- .../execute_operation/execute_data_table.rs | 9 ++++++++- quadratic-core/src/controller/operations/import.rs | 2 +- quadratic-core/src/controller/user_actions/import.rs | 9 ++++++--- quadratic-core/src/grid/data_table.rs | 12 +++++++++--- quadratic-core/src/grid/sheet/data_table.rs | 7 +++++++ quadratic-core/src/grid/sheet/sheet_test.rs | 4 ++-- quadratic-core/src/test_util.rs | 12 +++++++++++- quadratic-rust-shared/src/auto_gen_path.rs | 4 ++-- 10 files changed, 53 insertions(+), 17 deletions(-) diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index ac37f3e67a..872839fb9b 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -505,7 +505,7 @@ mod tests { Value::Single(CellValue::Html("html".to_string())), false, false, - false, + true, ); transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); @@ -529,7 +529,7 @@ mod tests { Value::Single(CellValue::Image("image".to_string())), false, false, - false, + true, ); transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index a49ec6c21a..d9891adb55 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -300,13 +300,16 @@ impl GridController { }; let sheet = self.try_sheet_result(current_sheet_pos.sheet_id)?; + + // todo: this should be true sometimes... + let show_header = false; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), &sheet.next_data_table_name(), value, false, - true, - true, + false, + show_header, ); self.finalize_code_run(&mut transaction, current_sheet_pos, Some(data_table), None); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index a80fea1e10..c01dd89dd8 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -103,14 +103,21 @@ impl GridController { ) -> Result<()> { if let Operation::SetDataTableAt { sheet_pos, values } = op { let sheet_id = sheet_pos.sheet_id; - let pos = Pos::from(sheet_pos); + let mut pos = Pos::from(sheet_pos); let sheet = self.try_sheet_mut_result(sheet_id)?; + let data_table_pos = sheet.first_data_table_within(pos)?; // TODO(ddimaria): handle multiple values if values.size() != 1 { bail!("Only single values are supported for now"); } + let data_table = sheet.data_table_result(data_table_pos)?; + + if data_table.show_header && !data_table.header_is_first_row { + pos.y -= 1; + } + let value = values.safe_get(0, 0).cloned()?; let old_value = sheet.get_code_cell_value(pos).unwrap_or(CellValue::Blank); diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 5e5e9e1419..0202082f43 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -526,7 +526,7 @@ mod test { }; assert_eq!(sheet_pos.x, 1); assert_eq!( - data_table.cell_value_ref_at(0, 0), + data_table.cell_value_ref_at(0, 1), Some(&CellValue::Text("city0".into())) ); } diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index a27e0de49b..ad892d4bd6 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -78,11 +78,9 @@ pub(crate) mod tests { use std::str::FromStr; use crate::{ - cellvalue::Import, grid::CodeCellLanguage, test_util::{ - assert_cell_value_row, assert_data_table_cell_value_row, assert_display_cell_value, - print_data_table, print_table, + assert_cell_value_row, assert_data_table_cell_value_row, print_data_table, print_table, }, wasm_bindings::js::clear_js_calls, CellValue, CodeCellValue, Rect, RunError, RunErrorMsg, Span, @@ -120,6 +118,11 @@ pub(crate) mod tests { gc.import_csv(sheet_id, csv_file.as_slice().to_vec(), file_name, pos, None) .unwrap(); + let sheet = gc.sheet_mut(sheet_id); + let data_table_pos = sheet.first_data_table_within(pos).unwrap(); + let data_table = sheet.data_table_mut(data_table_pos).unwrap(); + data_table.apply_first_row_as_header(); + (gc, sheet_id, pos, file_name) } diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index eac31fdbc0..281b6d6641 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -133,6 +133,8 @@ impl DataTable { if header_is_first_row { data_table.apply_first_row_as_header(); + } else if show_header { + data_table.apply_default_header(); } // data_table.toggle_first_row_as_header(header_is_first_row); @@ -374,7 +376,7 @@ impl DataTable { } } - pub fn display_value_at(&self, pos: Pos) -> Result<&CellValue> { + pub fn display_value_at(&self, mut pos: Pos) -> Result<&CellValue> { // println!("pos: {:?}", pos); // println!("self.columns: {:?}", self.columns); @@ -388,7 +390,9 @@ impl DataTable { } } - // pos.y = self.adjust_for_header(pos.y as usize) as i64; + if !self.header_is_first_row && self.show_header { + pos.y = pos.y - 1; + } match self.display_buffer { Some(ref display_buffer) => self.display_value_from_buffer_at(display_buffer, pos), @@ -861,7 +865,9 @@ pub mod test { let data_table = sheet.data_table_mut((1, 1).into()).unwrap(); data_table.toggle_first_row_as_header(false); - pretty_print_data_table(&data_table, Some("Data Table"), None); + println!("data_table: {:?}", data_table); + + pretty_print_data_table(&data_table, None, None); assert_eq!( sheet.display_value(pos), Some(CellValue::Text("Column 1".into())) diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index 93523a5f2d..9791a97fbe 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -51,6 +51,13 @@ impl Sheet { self.data_tables.get(&pos) } + /// Returns a DatatTable at a Pos + pub fn data_table_result(&self, pos: Pos) -> Result<&DataTable> { + self.data_tables + .get(&pos) + .ok_or_else(|| anyhow!("Data table not found at {:?}", pos)) + } + /// Returns a mutable DatatTable at a Pos pub fn data_table_mut(&mut self, pos: Pos) -> Result<&mut DataTable> { self.data_tables diff --git a/quadratic-core/src/grid/sheet/sheet_test.rs b/quadratic-core/src/grid/sheet/sheet_test.rs index 6c18eb7be7..b8c896e929 100644 --- a/quadratic-core/src/grid/sheet/sheet_test.rs +++ b/quadratic-core/src/grid/sheet/sheet_test.rs @@ -134,7 +134,7 @@ impl Sheet { Value::Array(array), false, false, - true, + false, )), ); } @@ -180,7 +180,7 @@ impl Sheet { Value::Array(array), false, false, - true, + false, )), ); } diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index 754ebb7a65..2ece690fb7 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -69,8 +69,18 @@ pub fn assert_data_table_cell_value( value: &str, ) { let sheet = grid_controller.sheet(sheet_id); + let mut pos = Pos { x, y }; + let data_table_pos = sheet.first_data_table_within(pos).unwrap(); + let data_table = sheet.data_table_result(data_table_pos).unwrap(); + + println!("Data table at {:?}", data_table); + + if data_table.show_header && !data_table.header_is_first_row { + pos.y += 1; + } + let cell_value = sheet - .get_code_cell_value(Pos { x, y }) + .get_code_cell_value(pos) .map_or_else(|| CellValue::Blank, |v| CellValue::Text(v.to_string())); let expected_text_or_blank = |v: &CellValue| v == &CellValue::Text(value.into()) || v == &CellValue::Blank; diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index 8df3d2b4d5..a0dea21ae2 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From 55685ff10135522846c99c0e6d6594b5a11b860a Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 18 Oct 2024 14:31:51 -0700 Subject: [PATCH 068/373] update test --- quadratic-core/src/grid/data_table.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 281b6d6641..584d5157a1 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -875,6 +875,10 @@ pub mod test { assert_eq!( sheet.display_value(Pos { x: 1, y: 2 }), Some(CellValue::Text("first".into())) - ) + ); + assert_eq!( + sheet.display_value(Pos { x: 1, y: 3 }), + Some(CellValue::Text("second".into())) + ); } } From 66b81bee04061804afb0f4a6b4dc52719c0ca4a3 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 04:42:42 -0700 Subject: [PATCH 069/373] fix bug with output_size --- .../src/controller/execution/run_code/mod.rs | 12 +++++++++--- .../controller/execution/run_code/run_formula.rs | 6 +++--- quadratic-core/src/grid/data_table.rs | 13 ++++++++----- quadratic-rust-shared/src/auto_gen_path.rs | 4 ++-- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index b9bbdf107e..1287ea33b4 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -226,9 +226,15 @@ impl GridController { cells_accessed: transaction.cells_accessed.clone(), }, }; + let table_name = match code_cell_value.language { + CodeCellLanguage::Formula => "Formula 1", + CodeCellLanguage::Javascript => "JavaScript 1", + CodeCellLanguage::Python => "Python 1", + _ => "Table 1", + }; let new_data_table = DataTable::new( DataTableKind::CodeRun(new_code_run), - "Table 1", + table_name, Value::Single(CellValue::Blank), false, false, @@ -269,7 +275,7 @@ impl GridController { let show_header = false; return DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1", + "JavaScript 1", Value::Single(CellValue::Blank), // TODO(ddimaria): this will eventually be an empty vec false, false, @@ -338,7 +344,7 @@ impl GridController { let show_header = false; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1", + "JavaScript 1", value, false, false, diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index 2e397c1adf..7530ea5eff 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -34,7 +34,7 @@ impl GridController { }; let new_data_table = DataTable::new( DataTableKind::CodeRun(new_code_run), - "Table 1", + "Formula 1", output.inner, false, false, @@ -265,7 +265,7 @@ mod test { result, DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1", + "JavaScript 1", Value::Single(CellValue::Number(12.into())), false, false, @@ -337,7 +337,7 @@ mod test { result, DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1", + "JavaScript 1", Value::Array(array), false, false, diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 584d5157a1..c1623fa327 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -5,6 +5,7 @@ //! performed yet). use std::fmt::{Display, Formatter}; +use std::num::NonZeroU32; use crate::cellvalue::Import; use crate::grid::js_types::JsDataTableColumn; @@ -490,7 +491,13 @@ impl DataTable { /// Note: this does not take spill_error into account. pub fn output_size(&self) -> ArraySize { match &self.value { - Value::Array(a) => a.size(), + Value::Array(a) => { + let mut size = a.size(); + if self.show_header && !self.header_is_first_row { + size.h = NonZeroU32::new(size.h.get() + 1).unwrap(); + } + size + } Value::Single(_) | Value::Tuple(_) => ArraySize::_1X1, } } @@ -864,10 +871,6 @@ pub mod test { let data_table = sheet.data_table_mut((1, 1).into()).unwrap(); data_table.toggle_first_row_as_header(false); - - println!("data_table: {:?}", data_table); - - pretty_print_data_table(&data_table, None, None); assert_eq!( sheet.display_value(pos), Some(CellValue::Text("Column 1".into())) diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index a0dea21ae2..8df3d2b4d5 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From 83f56bf3b5e2bc512372953a05711f41a9aedbb0 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 04:49:46 -0700 Subject: [PATCH 070/373] fix tests --- quadratic-core/src/grid/data_table.rs | 12 ++++++------ quadratic-core/src/grid/sheet/code.rs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index c1623fa327..65f41c3de6 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -654,7 +654,7 @@ pub mod test { let expected_data_table = DataTable::new(kind.clone(), "Table 1", expected_values, false, false, true) .with_last_modified(data_table.last_modified); - let expected_array_size = ArraySize::new(4, 4).unwrap(); + let expected_array_size = ArraySize::new(4, 5).unwrap(); assert_eq!(data_table, expected_data_table); assert_eq!(data_table.output_size(), expected_array_size); @@ -790,7 +790,7 @@ pub mod test { ); assert_eq!(data_table.output_size().w.get(), 10); - assert_eq!(data_table.output_size().h.get(), 11); + assert_eq!(data_table.output_size().h.get(), 12); assert_eq!( data_table.output_sheet_rect( SheetPos { @@ -800,7 +800,7 @@ pub mod test { }, false ), - SheetRect::from_numbers(1, 2, 10, 11, sheet_id) + SheetRect::new(1, 2, 10, 13, sheet_id) ); } @@ -829,14 +829,14 @@ pub mod test { let sheet_pos = SheetPos::from((1, 2, sheet_id)); assert_eq!(data_table.output_size().w.get(), 10); - assert_eq!(data_table.output_size().h.get(), 11); + assert_eq!(data_table.output_size().h.get(), 12); assert_eq!( data_table.output_sheet_rect(sheet_pos, false), - SheetRect::from_numbers(1, 2, 1, 1, sheet_id) + SheetRect::new(1, 2, 1, 2, sheet_id) ); assert_eq!( data_table.output_sheet_rect(sheet_pos, true), - SheetRect::from_numbers(1, 2, 10, 11, sheet_id) + SheetRect::new(1, 2, 10, 13, sheet_id) ); } diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 6e6b44373e..7812b9ba33 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -418,7 +418,7 @@ mod test { Value::Array(Array::from(vec![vec!["1"], vec!["2"], vec!["3"]])), false, false, - true, + false, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); sheet.set_data_table(Pos { x: 1, y: 1 }, Some(data_table.clone())); @@ -455,7 +455,7 @@ mod test { Value::Array(Array::from(vec![vec!["1", "2", "3'"]])), false, false, - true, + false, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); sheet.set_data_table(Pos { x: 1, y: 1 }, Some(data_table.clone())); From a42bb5bb5e14f9a99690d4b2fa533e464f8ea66b Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 04:53:06 -0700 Subject: [PATCH 071/373] import csv uses filename as table name --- quadratic-core/src/controller/operations/import.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 0202082f43..62a3cfcf94 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -117,7 +117,8 @@ impl GridController { let sheet = self .try_sheet(sheet_id) .ok_or_else(|| anyhow!("Sheet {sheet_id} not found"))?; - let data_table = DataTable::from((import.to_owned(), cell_values, sheet)); + let mut data_table = DataTable::from((import.to_owned(), cell_values, sheet)); + data_table.name = file_name.to_string(); let sheet_pos = SheetPos::from((insert_at, sheet_id)); // this operation must be before the SetCodeRun operations @@ -480,7 +481,8 @@ mod test { _ => panic!("Expected SetCodeRun operation"), }; expected_data_table.last_modified = data_table.last_modified; - + expected_data_table.name = file_name.to_string(); + let expected = Operation::SetCodeRun { sheet_pos: SheetPos { x: 0, From 005e0420d61470598ede645adffcf9819dce2416 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 05:02:17 -0700 Subject: [PATCH 072/373] add table to context menu for cells within table --- quadratic-client/src/app/actions/actions.ts | 1 + .../src/app/actions/dataTableSpec.ts | 16 +++++++++++++++- .../HTMLGrid/contextMenus/GridContextMenu.tsx | 12 +++++++++++- .../HTMLGrid/contextMenus/TableContextMenu.tsx | 12 +++--------- .../gridGL/HTMLGrid/contextMenus/TableMenu.tsx | 17 +++++++++++++++++ .../src/app/gridGL/cells/CellsSheet.ts | 4 ++++ .../src/app/gridGL/cells/tables/Table.ts | 9 +++++++++ .../src/app/gridGL/cells/tables/Tables.ts | 4 ++++ .../src/app/gridGL/pixiApp/PixiApp.ts | 8 ++++++++ 9 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index f18cb76da6..1be60241de 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -145,4 +145,5 @@ export enum Action { ToggleFirstRowAsHeaderDataTable = 'toggle_first_row_as_header_data_table', RenameDataTable = 'rename_data_table', ToggleHeaderDataTable = 'toggle_header_data_table', + DeleteDataTable = 'delete_data_table', } diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 8a73ba497d..742f1e204a 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -1,9 +1,11 @@ import { Action } from '@/app/actions/actions'; import { ContextMenuSpecial } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; +import { createSelection } from '@/app/grid/sheet/selection'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -import { FileRenameIcon, TableConvertIcon } from '@/shared/components/Icons'; +import { DeleteIcon, FileRenameIcon, TableConvertIcon } from '@/shared/components/Icons'; +import { Rectangle } from 'pixi.js'; import { sheets } from '../grid/controller/Sheets'; import { ActionSpecRecord } from './actionsSpec'; @@ -14,6 +16,7 @@ type DataTableSpec = Pick< | Action.ToggleFirstRowAsHeaderDataTable | Action.RenameDataTable | Action.ToggleHeaderDataTable + | Action.DeleteDataTable >; export type DataTableActionArgs = { @@ -102,4 +105,15 @@ export const dataTableSpec: DataTableSpec = { // quadraticCore.dataTableShowHeadings(sheets.sheet.id, x, y, sheets.getCursorPosition()); }, }, + [Action.DeleteDataTable]: { + label: 'Delete data table', + Icon: DeleteIcon, + run: async () => { + const table = pixiAppSettings.contextMenu?.table; + if (table) { + const selection = createSelection({ sheetId: sheets.sheet.id, rects: [new Rectangle(table.x, table.y, 1, 1)] }); + quadraticCore.deleteCellValues(selection, sheets.getCursorPosition()); + } + }, + }, }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index 0203c1244f..1f885c45ba 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -5,8 +5,9 @@ import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; +import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { focusGrid } from '@/app/helpers/focusGrid'; -import { ControlledMenu, MenuDivider } from '@szhsin/react-menu'; +import { ControlledMenu, MenuDivider, SubMenu } from '@szhsin/react-menu'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useRecoilState } from 'recoil'; import { pixiApp } from '../../pixiApp/PixiApp'; @@ -35,10 +36,12 @@ export const GridContextMenu = () => { const [columnRowAvailable, setColumnRowAvailable] = useState(false); const [canConvertToDataTable, setCanConvertToDataTable] = useState(false); + const [isDataTable, setIsDataTable] = useState(false); useEffect(() => { const updateCursor = () => { setColumnRowAvailable(sheets.sheet.cursor.hasOneColumnRowSelection(true)); setCanConvertToDataTable(sheets.sheet.cursor.canConvertToDataTable()); + setIsDataTable(pixiApp.cellSheet().isCursorOnDataTable()); }; updateCursor(); @@ -96,6 +99,13 @@ export const GridContextMenu = () => { {canConvertToDataTable && } {canConvertToDataTable && } + + {isDataTable && } + {isDataTable && ( + + + + )} ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 658b1c612b..0a44ceaf2a 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -1,12 +1,11 @@ //! This shows the table context menu. -import { Action } from '@/app/actions/actions'; import { contextMenuAtom, ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; -import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; +import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { focusGrid } from '@/app/helpers/focusGrid'; -import { ControlledMenu, MenuDivider } from '@szhsin/react-menu'; +import { ControlledMenu } from '@szhsin/react-menu'; import { useCallback, useEffect, useRef } from 'react'; import { useRecoilState } from 'recoil'; @@ -53,12 +52,7 @@ export const TableContextMenu = () => { menuStyle={{ padding: '0', color: 'inherit' }} menuClassName="bg-background" > - - - - - - + ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx new file mode 100644 index 0000000000..539330db92 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -0,0 +1,17 @@ +import { Action } from '@/app/actions/actions'; +import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; +import { MenuDivider } from '@szhsin/react-menu'; + +export const TableMenu = () => { + return ( + <> + + + + + + + + + ); +}; diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts index e85e901745..aa9509ece1 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts @@ -134,4 +134,8 @@ export class CellsSheet extends Container { getErrorMarkerValidation(x: number, y: number): boolean { return this.cellsLabels.getErrorMarker(x, y) !== undefined; } + + isCursorOnDataTable(): boolean { + return this.tables.isCursorOnDataTable(); + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index ef5896e6df..5adba7fdce 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -254,4 +254,13 @@ export class Table extends Container { rectangle ); } + + // Checks whether the cursor is on the table + isCursorOnDataTable(): boolean { + const cursor = sheets.sheet.cursor.cursorPosition; + return intersects.rectanglePoint( + new Rectangle(this.codeCell.x, this.codeCell.y, this.codeCell.w - 1, this.codeCell.h - 1), + cursor + ); + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index dd8131431f..6a3508602a 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -204,4 +204,8 @@ export class Tables extends Container
{ intersects(rectangle: Rectangle): boolean { return this.children.some((table) => table.intersects(rectangle)); } + + isCursorOnDataTable(): boolean { + return this.children.some((table) => table.isCursorOnDataTable()); + } } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index a3828210ee..649978e541 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -18,6 +18,7 @@ import { UIValidations } from '@/app/gridGL/UI/UIValidations'; import { BoxCells } from '@/app/gridGL/UI/boxCells'; import { CellHighlights } from '@/app/gridGL/UI/cellHighlights/CellHighlights'; import { GridHeadings } from '@/app/gridGL/UI/gridHeadings/GridHeadings'; +import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; import { CellsSheets } from '@/app/gridGL/cells/CellsSheets'; import { CellsImages } from '@/app/gridGL/cells/cellsImages/CellsImages'; import { Pointer } from '@/app/gridGL/interaction/pointer/Pointer'; @@ -373,6 +374,13 @@ export class PixiApp { }; } } + + cellSheet(): CellsSheet { + if (!this.cellsSheets.current) { + throw new Error('cellSheet not found in pixiApp'); + } + return this.cellsSheets.current; + } } export const pixiApp = new PixiApp(); From 5100866abc0aa7c1bfa2fd9dfb459d55f0fa8231 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 05:14:02 -0700 Subject: [PATCH 073/373] playing with styling of table names --- .../gridGL/HTMLGrid/contextMenus/TableRename.tsx | 6 +++--- .../src/app/gridGL/cells/tables/Table.ts | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx index c61261c256..e128347401 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx @@ -2,7 +2,7 @@ import { contextMenuAtom, ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; -import { TABLE_NAME_PADDING } from '@/app/gridGL/cells/tables/Table'; +import { TABLE_NAME_FONT_SIZE, TABLE_NAME_PADDING } from '@/app/gridGL/cells/tables/Table'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { focusGrid } from '@/app/helpers/focusGrid'; import { FONT_SIZE } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; @@ -105,10 +105,10 @@ export const TableRename = () => { ref={ref} className="pointer-events-auto absolute rounded-none border-none bg-primary px-0 text-primary-foreground outline-none" style={{ - paddingLeft: TABLE_NAME_PADDING, + paddingLeft: TABLE_NAME_PADDING[0], left: position.x, top: position.y, - fontSize: FONT_SIZE, + fontSize: TABLE_NAME_FONT_SIZE, width: position.width, height: position.height, transformOrigin: 'bottom left', diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 5adba7fdce..898d3fdd4c 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -15,7 +15,8 @@ interface Column { } const DROPDOWN_PADDING = 10; -export const TABLE_NAME_PADDING = 4; +export const TABLE_NAME_FONT_SIZE = 12; +export const TABLE_NAME_PADDING = [4, 2]; export class Table extends Container { private sheet: Sheet; @@ -40,8 +41,8 @@ export class Table extends Container { this.sheet = sheet; this.tableName = new Container(); this.tableNameText = new BitmapText(codeCell.name, { - fontName: 'OpenSans', - fontSize: FONT_SIZE, + fontName: 'OpenSans-Bold', + fontSize: TABLE_NAME_FONT_SIZE, tint: getCSSVariableTint('primary-foreground'), }); this.headingContainer = new Container(); @@ -125,11 +126,11 @@ export class Table extends Container { this.tableName.visible = false; const text = this.tableName.addChild(this.tableNameText); this.tableNameText.text = codeCell.name; - text.position.set(OPEN_SANS_FIX.x + TABLE_NAME_PADDING, OPEN_SANS_FIX.y - this.headingBounds.height); + text.position.set(TABLE_NAME_PADDING[0], -this.headingHeight); const dropdown = this.tableName.addChild(this.drawDropdown()); dropdown.position.set( - text.width + OPEN_SANS_FIX.x + DROPDOWN_PADDING + TABLE_NAME_PADDING, + text.width + OPEN_SANS_FIX.x + DROPDOWN_PADDING + TABLE_NAME_PADDING[0], -this.headingHeight / 2 ); @@ -138,7 +139,7 @@ export class Table extends Container { new Rectangle( 0, -this.headingBounds.height, - text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING + TABLE_NAME_PADDING, + text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING + TABLE_NAME_PADDING[0], this.headingBounds.height ) ); @@ -146,7 +147,7 @@ export class Table extends Container { this.tableNameBounds = new Rectangle( this.tableBounds.x, this.tableBounds.y - this.headingHeight, - text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING + TABLE_NAME_PADDING, + text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING + TABLE_NAME_PADDING[0], this.headingBounds.height ); }; From d67e1b58662513aad37d848ddd23b993c77d92a4 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 06:30:01 -0700 Subject: [PATCH 074/373] adding symbols to table name --- quadratic-client/public/images/mssql-icon.svg | 38 +++++++++ quadratic-client/public/images/mysql-icon.svg | 4 + .../public/images/postgres-icon.svg | 3 + .../public/images/snowflake-icon.svg | 32 ++++++++ .../src/app/gridGL/cells/CellsMarkers.ts | 79 +++++++++++++------ .../src/app/gridGL/cells/tables/Table.ts | 41 +++++++++- quadratic-client/src/app/gridGL/loadAssets.ts | 4 + 7 files changed, 175 insertions(+), 26 deletions(-) create mode 100644 quadratic-client/public/images/mssql-icon.svg create mode 100644 quadratic-client/public/images/mysql-icon.svg create mode 100644 quadratic-client/public/images/postgres-icon.svg create mode 100644 quadratic-client/public/images/snowflake-icon.svg diff --git a/quadratic-client/public/images/mssql-icon.svg b/quadratic-client/public/images/mssql-icon.svg new file mode 100644 index 0000000000..25420284a7 --- /dev/null +++ b/quadratic-client/public/images/mssql-icon.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/quadratic-client/public/images/mysql-icon.svg b/quadratic-client/public/images/mysql-icon.svg new file mode 100644 index 0000000000..06898af95a --- /dev/null +++ b/quadratic-client/public/images/mysql-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/quadratic-client/public/images/postgres-icon.svg b/quadratic-client/public/images/postgres-icon.svg new file mode 100644 index 0000000000..8eb1e22759 --- /dev/null +++ b/quadratic-client/public/images/postgres-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/quadratic-client/public/images/snowflake-icon.svg b/quadratic-client/public/images/snowflake-icon.svg new file mode 100644 index 0000000000..d601f8782f --- /dev/null +++ b/quadratic-client/public/images/snowflake-icon.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts b/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts index f1df1ad72e..bcb3f47c76 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts @@ -1,8 +1,9 @@ -import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { convertColorStringToTint } from '@/app/helpers/convertColor'; +import { CodeCellLanguage, JsRenderCodeCell } from '@/app/quadratic-core-types'; import { Container, Point, Rectangle, Sprite, Texture } from 'pixi.js'; import { colors } from '../../theme/colors'; -import { pixiAppSettings } from '../pixiApp/PixiAppSettings'; import { generatedTextures } from '../generateTextures'; +import { pixiAppSettings } from '../pixiApp/PixiAppSettings'; import { ErrorMarker } from './CellsSheet'; const INDICATOR_SIZE = 4; @@ -17,6 +18,48 @@ interface Marker { symbol?: Sprite; } +export const getLanguageSymbol = (language: CodeCellLanguage, isError: boolean): Sprite | undefined => { + const symbol = new Sprite(); + if (language === 'Python') { + symbol.texture = Texture.from('/images/python-icon.png'); + symbol.tint = isError ? 0xffffff : colors.cellColorUserPython; + return symbol; + } else if (language === 'Formula') { + symbol.texture = Texture.from('/images/formula-fx-icon.png'); + symbol.tint = isError ? 0xffffff : colors.cellColorUserFormula; + return symbol; + } else if (language === 'Javascript') { + symbol.texture = Texture.from('/images/javascript-icon.png'); + symbol.tint = isError ? colors.cellColorError : colors.cellColorUserJavascript; + return symbol; + } else if (typeof language === 'object') { + switch (language.Connection?.kind) { + case 'MSSQL': + symbol.texture = Texture.from('/images/mssql-icon.png'); + symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languageMssql); + return symbol; + + case 'POSTGRES': + symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languagePostgres); + symbol.texture = Texture.from('/images/postgres-icon.png'); + return symbol; + + case 'MYSQL': + symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languageMysql); + symbol.texture = Texture.from('/images/mysql-icon.png'); + return symbol; + + case 'SNOWFLAKE': + symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languageSnowflake); + symbol.texture = Texture.from('/images/snowflake-icon.png'); + return symbol; + + default: + console.log(`Unknown connection kind: ${language.Connection?.kind} in getLanguageSymbol`); + } + } +}; + export class CellsMarkers extends Container { private markers: Marker[] = []; @@ -35,33 +78,25 @@ export class CellsMarkers extends Container { triangle.tint = colors.cellColorError; } - let symbol: Sprite | undefined; if (isError || selected || pixiAppSettings.showCellTypeOutlines) { - symbol = this.addChild(new Sprite()); - symbol.height = INDICATOR_SIZE; - symbol.width = INDICATOR_SIZE; - symbol.position.set(box.x + 1.25, box.y + 1.25); - if (codeCell.language === 'Python') { - symbol.texture = Texture.from('/images/python-icon.png'); - symbol.tint = isError ? 0xffffff : colors.cellColorUserPython; - } else if (codeCell.language === 'Formula') { - symbol.texture = Texture.from('/images/formula-fx-icon.png'); - symbol.tint = isError ? 0xffffff : colors.cellColorUserFormula; - } else if (codeCell.language === 'Javascript') { - symbol.texture = Texture.from('/images/javascript-icon.png'); - symbol.tint = isError ? colors.cellColorError : colors.cellColorUserJavascript; + const symbol = getLanguageSymbol(codeCell.language, isError); + if (symbol) { + this.addChild(symbol); + symbol.height = INDICATOR_SIZE; + symbol.width = INDICATOR_SIZE; + symbol.position.set(box.x + 1.25, box.y + 1.25); if (isError) { symbol.x -= 1; symbol.y -= 1; } + this.markers.push({ + bounds: new Rectangle(box.x, box.y, box.width, box.height), + codeCell, + triangle, + symbol, + }); } } - this.markers.push({ - bounds: new Rectangle(box.x, box.y, box.width, box.height), - codeCell, - triangle, - symbol, - }); } intersectsCodeInfo(point: Point): JsRenderCodeCell | undefined { diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 898d3fdd4c..627fd9b7a4 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -1,6 +1,7 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; import { DROPDOWN_SIZE } from '@/app/gridGL/cells/cellsLabel/drawSpecial'; +import { getLanguageSymbol } from '@/app/gridGL/cells/CellsMarkers'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; @@ -15,6 +16,9 @@ interface Column { } const DROPDOWN_PADDING = 10; +const SYMBOL_SCALE = 0.5; +const SYMBOL_PADDING = 5; + export const TABLE_NAME_FONT_SIZE = 12; export const TABLE_NAME_PADDING = [4, 2]; @@ -124,13 +128,32 @@ export class Table extends Container { this.tableName.position.set(this.tableBounds.x, this.tableBounds.y); const nameBackground = this.tableName.addChild(new Graphics()); this.tableName.visible = false; + let symbol: Sprite | undefined; + if (codeCell.language !== 'Import') { + symbol = getLanguageSymbol(codeCell.language, false); + if (symbol) { + this.tableName.addChild(symbol); + symbol.width = this.headingBounds.height * SYMBOL_SCALE; + symbol.scale.y = symbol.scale.x; + symbol.anchor.set(0, 0.5); + symbol.y = -this.headingHeight / 2; + symbol.x = SYMBOL_PADDING; + if (codeCell.language === 'Formula' || codeCell.language === 'Python') { + symbol.tint = 0xffffff; + } + } + } const text = this.tableName.addChild(this.tableNameText); this.tableNameText.text = codeCell.name; - text.position.set(TABLE_NAME_PADDING[0], -this.headingHeight); + text.position.set(TABLE_NAME_PADDING[0] + (symbol ? SYMBOL_PADDING + symbol.width : 0), -this.headingHeight); const dropdown = this.tableName.addChild(this.drawDropdown()); dropdown.position.set( - text.width + OPEN_SANS_FIX.x + DROPDOWN_PADDING + TABLE_NAME_PADDING[0], + text.width + + OPEN_SANS_FIX.x + + DROPDOWN_PADDING + + TABLE_NAME_PADDING[0] + + (symbol ? SYMBOL_PADDING + symbol.width : 0), -this.headingHeight / 2 ); @@ -139,7 +162,12 @@ export class Table extends Container { new Rectangle( 0, -this.headingBounds.height, - text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING + TABLE_NAME_PADDING[0], + text.width + + OPEN_SANS_FIX.x + + dropdown.width + + DROPDOWN_PADDING + + TABLE_NAME_PADDING[0] + + (symbol ? SYMBOL_PADDING + symbol.width : 0), this.headingBounds.height ) ); @@ -147,7 +175,12 @@ export class Table extends Container { this.tableNameBounds = new Rectangle( this.tableBounds.x, this.tableBounds.y - this.headingHeight, - text.width + OPEN_SANS_FIX.x + dropdown.width + DROPDOWN_PADDING + TABLE_NAME_PADDING[0], + text.width + + OPEN_SANS_FIX.x + + dropdown.width + + DROPDOWN_PADDING + + TABLE_NAME_PADDING[0] + + (symbol ? SYMBOL_PADDING + symbol.width : 0), this.headingBounds.height ); }; diff --git a/quadratic-client/src/app/gridGL/loadAssets.ts b/quadratic-client/src/app/gridGL/loadAssets.ts index fd2487895d..f2dcf9b302 100644 --- a/quadratic-client/src/app/gridGL/loadAssets.ts +++ b/quadratic-client/src/app/gridGL/loadAssets.ts @@ -49,6 +49,10 @@ export function loadAssets(): Promise { addResourceOnce('checkbox-checked-icon', '/images/checkbox-checked.png'); addResourceOnce('dropdown-icon', '/images/dropdown.png'); addResourceOnce('dropdown-white-icon', '/images/dropdown-white.png'); + addResourceOnce('mssql-icon', '/images/mssql-icon.svg'); + addResourceOnce('postgres-icon', '/images/postgres-icon.svg'); + addResourceOnce('mysql-icon', '/images/mysql-icon.svg'); + addResourceOnce('snowflake-icon', '/images/snowflake-icon.svg'); // Wait until pixi fonts are loaded before resolving Loader.shared.load(() => { From e2f239d8ba39edf15fc13d44c06c19aa3136d25a Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 06:33:02 -0700 Subject: [PATCH 075/373] change name to table (so it includes code) --- quadratic-client/src/app/actions/dataTableSpec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 742f1e204a..e01eb850b6 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -33,7 +33,7 @@ const isHeadingShowing = (): boolean => { export const dataTableSpec: DataTableSpec = { [Action.FlattenDataTable]: { - label: 'Flatten data table', + label: 'Flatten table', Icon: TableConvertIcon, run: async () => { const table = pixiAppSettings.contextMenu?.table; @@ -43,7 +43,7 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.GridToDataTable]: { - label: 'Convert values to data table', + label: 'Convert values to table', Icon: TableConvertIcon, run: async () => { quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); @@ -84,7 +84,7 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.RenameDataTable]: { - label: 'Rename data table', + label: 'Rename table', defaultOption: true, Icon: FileRenameIcon, run: async () => { @@ -106,7 +106,7 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.DeleteDataTable]: { - label: 'Delete data table', + label: 'Delete table', Icon: DeleteIcon, run: async () => { const table = pixiAppSettings.contextMenu?.table; From 9585e83a7093afd91a6b45c7d702099939065345 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 06:39:43 -0700 Subject: [PATCH 076/373] python tables have proper names --- .../src/controller/execution/run_code/mod.rs | 12 ++++++++++-- .../controller/execution/run_code/run_formula.rs | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 1287ea33b4..aeaad7327d 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -141,6 +141,7 @@ impl GridController { transaction, result, current_sheet_pos, + waiting_for_async.clone(), ); transaction.waiting_for_async = None; @@ -252,7 +253,14 @@ impl GridController { transaction: &mut PendingTransaction, js_code_result: JsCodeResult, start: SheetPos, + language: CodeCellLanguage, ) -> DataTable { + let table_name = match language { + CodeCellLanguage::Formula => "Formula 1", + CodeCellLanguage::Javascript => "JavaScript 1", + CodeCellLanguage::Python => "Python 1", + _ => "Table 1", + }; let Some(sheet) = self.try_sheet_mut(start.sheet_id) else { // todo: this is probably not the best place to handle this // sheet may have been deleted before the async operation completed @@ -275,7 +283,7 @@ impl GridController { let show_header = false; return DataTable::new( DataTableKind::CodeRun(code_run), - "JavaScript 1", + table_name, Value::Single(CellValue::Blank), // TODO(ddimaria): this will eventually be an empty vec false, false, @@ -344,7 +352,7 @@ impl GridController { let show_header = false; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), - "JavaScript 1", + table_name, value, false, false, diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index 7530ea5eff..325f89b742 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -250,7 +250,12 @@ mod test { }; // need the result to ensure last_modified is the same - let result = gc.js_code_result_to_code_cell_value(&mut transaction, result, sheet_pos); + let result = gc.js_code_result_to_code_cell_value( + &mut transaction, + result, + sheet_pos, + CodeCellLanguage::Javascript, + ); let code_run = CodeRun { std_out: None, std_err: None, @@ -322,7 +327,12 @@ mod test { let _ = array.set(0, 1, CellValue::Number(BigDecimal::from_str("3").unwrap())); let _ = array.set(1, 1, CellValue::Text("Hello".into())); - let result = gc.js_code_result_to_code_cell_value(&mut transaction, result, sheet_pos); + let result = gc.js_code_result_to_code_cell_value( + &mut transaction, + result, + sheet_pos, + CodeCellLanguage::Javascript, + ); let code_run = CodeRun { std_out: None, std_err: None, From d0d2c06bc5f950e815d69227739012191683da81 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 06:45:00 -0700 Subject: [PATCH 077/373] don't check hover over table when not over canvas --- quadratic-client/src/app/gridGL/cells/tables/Tables.ts | 6 +++++- .../src/app/gridGL/interaction/pointer/Pointer.ts | 2 +- .../src/app/gridGL/interaction/pointer/pointerCursor.ts | 8 ++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 6a3508602a..f17f3b566d 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -119,7 +119,11 @@ export class Tables extends Container
{ }; // Checks if the mouse cursor is hovering over a table or table heading. - checkHover(world: Point) { + checkHover(world: Point, event: PointerEvent) { + // only allow hover when the mouse is over the canvas (and not menus) + if (event.target !== pixiApp.canvas) { + return; + } const hover = this.children.find((table) => table.checkHover(world)); // if we already have the active table open, then don't show hover if (hover && (hover === this.contextMenuTable || hover === this.activeTable || hover === this.renameDataTable)) { diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts index 682ca65924..3e9ad91474 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts @@ -128,7 +128,7 @@ export class Pointer { this.pointerHeading.pointerMove(world) || this.pointerAutoComplete.pointerMove(world) || this.pointerDown.pointerMove(world, event) || - this.pointerCursor.pointerMove(world) || + this.pointerCursor.pointerMove(world, event) || this.pointerLink.pointerMove(world, event) || this.pointerTable.pointerMove(); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts b/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts index 27d45819e8..401eb5b0b5 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/pointerCursor.ts @@ -10,7 +10,7 @@ import { Point } from 'pixi.js'; export class PointerCursor { private lastInfo?: JsRenderCodeCell | EditingCell | ErrorValidation; - private checkHoverCell(world: Point) { + private checkHoverCell(world: Point, event: PointerEvent) { if (!pixiApp.cellsSheets.current) throw new Error('Expected cellsSheets.current to be defined in PointerCursor'); const cell = sheets.sheet.getColumnRow(world.x, world.y); const editingCell = multiplayer.cellIsBeingEdited(cell.x, cell.y, sheets.sheet.id); @@ -31,7 +31,7 @@ export class PointerCursor { foundCodeCell = true; } - pixiApp.cellsSheets.current.tables.checkHover(world); + pixiApp.cellsSheets.current.tables.checkHover(world, event); let foundValidation = false; const validation = pixiApp.cellsSheets.current.cellsLabels.intersectsErrorMarkerValidation(world); @@ -49,10 +49,10 @@ export class PointerCursor { } } - pointerMove(world: Point): void { + pointerMove(world: Point, event: PointerEvent): void { const cursor = pixiApp.pointer.pointerHeading.cursor ?? pixiApp.pointer.pointerAutoComplete.cursor; pixiApp.canvas.style.cursor = cursor ?? 'unset'; multiplayer.sendMouseMove(world.x, world.y); - this.checkHoverCell(world); + this.checkHoverCell(world, event); } } From f465308be707a4a2a4a6621ecab53f982704dbaf Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 15:41:05 -0700 Subject: [PATCH 078/373] refactor table UI --- .../HTMLGrid/contextMenus/TableRename.tsx | 2 +- .../src/app/gridGL/cells/tables/Table.ts | 228 +++--------------- .../gridGL/cells/tables/TableColumnHeader.ts | 24 ++ .../gridGL/cells/tables/TableColumnHeaders.ts | 56 +++++ .../src/app/gridGL/cells/tables/TableName.ts | 132 ++++++++++ .../app/gridGL/cells/tables/TableOutline.ts | 19 ++ .../src/app/gridGL/cells/tables/Tables.ts | 9 +- 7 files changed, 273 insertions(+), 197 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts create mode 100644 quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts create mode 100644 quadratic-client/src/app/gridGL/cells/tables/TableName.ts create mode 100644 quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx index e128347401..8fcae5af1a 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx @@ -2,7 +2,7 @@ import { contextMenuAtom, ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; -import { TABLE_NAME_FONT_SIZE, TABLE_NAME_PADDING } from '@/app/gridGL/cells/tables/Table'; +import { TABLE_NAME_FONT_SIZE, TABLE_NAME_PADDING } from '@/app/gridGL/cells/tables/TableName'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { focusGrid } from '@/app/helpers/focusGrid'; import { FONT_SIZE } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 627fd9b7a4..35eb682e06 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -1,198 +1,50 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; -import { DROPDOWN_SIZE } from '@/app/gridGL/cells/cellsLabel/drawSpecial'; -import { getLanguageSymbol } from '@/app/gridGL/cells/CellsMarkers'; +import { TableColumnHeaders } from '@/app/gridGL/cells/tables/TableColumnHeaders'; +import { TableName } from '@/app/gridGL/cells/tables/TableName'; +import { TableOutline } from '@/app/gridGL/cells/tables/TableOutline'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; -import { colors } from '@/app/theme/colors'; -import { FONT_SIZE, OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; -import { BitmapText, Container, Graphics, Point, Rectangle, Sprite, Texture } from 'pixi.js'; - -interface Column { - heading: Container; - bounds: Rectangle; -} - -const DROPDOWN_PADDING = 10; -const SYMBOL_SCALE = 0.5; -const SYMBOL_PADDING = 5; - -export const TABLE_NAME_FONT_SIZE = 12; -export const TABLE_NAME_PADDING = [4, 2]; +import { Container, Point, Rectangle } from 'pixi.js'; export class Table extends Container { - private sheet: Sheet; - private headingHeight = 0; - private tableName: Container; - private tableNameText: BitmapText; + private tableName: TableName; + private outline: TableOutline; + private columnHeaders: TableColumnHeaders; - // holds all headings - private headingContainer: Container; - - private outline: Graphics; - private tableBounds: Rectangle; - private headingBounds: Rectangle; - private columns: Column[]; - - tableNameBounds: Rectangle; + sheet: Sheet; + tableBounds: Rectangle; codeCell: JsRenderCodeCell; constructor(sheet: Sheet, codeCell: JsRenderCodeCell) { super(); this.codeCell = codeCell; this.sheet = sheet; - this.tableName = new Container(); - this.tableNameText = new BitmapText(codeCell.name, { - fontName: 'OpenSans-Bold', - fontSize: TABLE_NAME_FONT_SIZE, - tint: getCSSVariableTint('primary-foreground'), - }); - this.headingContainer = new Container(); - this.outline = new Graphics(); + this.tableName = new TableName(this); + this.columnHeaders = this.addChild(new TableColumnHeaders(this)); + this.outline = this.addChild(new TableOutline(this)); this.tableBounds = new Rectangle(); - this.headingBounds = new Rectangle(); - this.tableNameBounds = new Rectangle(); - this.columns = []; this.updateCodeCell(codeCell); } - redraw() { - this.removeChildren(); - this.updateCodeCell(this.codeCell); - } - - updateCodeCell = (codeCell: JsRenderCodeCell) => { - this.removeChildren(); - pixiApp.overHeadings.removeChild(this.tableName); - this.tableName.removeChildren(); - - this.codeCell = codeCell; - this.tableBounds = this.sheet.getScreenRectangle(codeCell.x, codeCell.y, codeCell.w - 1, codeCell.h - 1); - this.headingHeight = this.sheet.offsets.getRowHeight(codeCell.y); - this.headingBounds = new Rectangle( - this.tableBounds.x, - this.tableBounds.y, - this.tableBounds.width, - this.headingHeight - ); - this.position.set(this.headingBounds.x, this.headingBounds.y); - - this.addChild(this.headingContainer); - this.headingContainer.removeChildren(); - - // draw heading background - const background = this.headingContainer.addChild(new Graphics()); - background.beginFill(colors.tableHeadingBackground); - background.drawShape(new Rectangle(0, 0, this.headingBounds.width, this.headingBounds.height)); - background.endFill(); - - // create column headings - if (codeCell.show_header) { - this.headingContainer.visible = true; - let x = 0; - this.columns = codeCell.column_names.map((column, index) => { - const width = this.sheet.offsets.getColumnWidth(codeCell.x + index); - const bounds = new Rectangle(x, this.headingBounds.y, width, this.headingBounds.height); - const heading = this.headingContainer.addChild(new Container()); - heading.position.set(x + OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); - heading.addChild( - new BitmapText(column.name, { - fontName: 'OpenSans-Bold', - fontSize: FONT_SIZE, - tint: colors.tableHeadingForeground, - }) - ); - - x += width; - return { heading, bounds }; - }); - } else { - this.columns = []; - this.headingContainer.visible = false; + updateCodeCell = (codeCell?: JsRenderCodeCell) => { + if (codeCell) { + this.codeCell = codeCell; } - - // draw outline around entire table - this.addChild(this.outline); - this.outline.clear(); - this.outline.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); - this.outline.drawShape(new Rectangle(0, 0, this.tableBounds.width, this.tableBounds.height)); - this.outline.visible = false; - - // draw table name - if (sheets.sheet.id === this.sheet.id) { - pixiApp.overHeadings.addChild(this.tableName); - } - this.tableName.removeChildren(); - this.tableName.position.set(this.tableBounds.x, this.tableBounds.y); - const nameBackground = this.tableName.addChild(new Graphics()); - this.tableName.visible = false; - let symbol: Sprite | undefined; - if (codeCell.language !== 'Import') { - symbol = getLanguageSymbol(codeCell.language, false); - if (symbol) { - this.tableName.addChild(symbol); - symbol.width = this.headingBounds.height * SYMBOL_SCALE; - symbol.scale.y = symbol.scale.x; - symbol.anchor.set(0, 0.5); - symbol.y = -this.headingHeight / 2; - symbol.x = SYMBOL_PADDING; - if (codeCell.language === 'Formula' || codeCell.language === 'Python') { - symbol.tint = 0xffffff; - } - } - } - const text = this.tableName.addChild(this.tableNameText); - this.tableNameText.text = codeCell.name; - text.position.set(TABLE_NAME_PADDING[0] + (symbol ? SYMBOL_PADDING + symbol.width : 0), -this.headingHeight); - - const dropdown = this.tableName.addChild(this.drawDropdown()); - dropdown.position.set( - text.width + - OPEN_SANS_FIX.x + - DROPDOWN_PADDING + - TABLE_NAME_PADDING[0] + - (symbol ? SYMBOL_PADDING + symbol.width : 0), - -this.headingHeight / 2 + this.tableBounds = this.sheet.getScreenRectangle( + this.codeCell.x, + this.codeCell.y, + this.codeCell.w - 1, + this.codeCell.h - 1 ); + this.position.set(this.tableBounds.x, this.tableBounds.y); - nameBackground.beginFill(getCSSVariableTint('primary')); - nameBackground.drawShape( - new Rectangle( - 0, - -this.headingBounds.height, - text.width + - OPEN_SANS_FIX.x + - dropdown.width + - DROPDOWN_PADDING + - TABLE_NAME_PADDING[0] + - (symbol ? SYMBOL_PADDING + symbol.width : 0), - this.headingBounds.height - ) - ); - nameBackground.endFill(); - this.tableNameBounds = new Rectangle( - this.tableBounds.x, - this.tableBounds.y - this.headingHeight, - text.width + - OPEN_SANS_FIX.x + - dropdown.width + - DROPDOWN_PADDING + - TABLE_NAME_PADDING[0] + - (symbol ? SYMBOL_PADDING + symbol.width : 0), - this.headingBounds.height - ); + this.tableName.update(); + this.columnHeaders.update(); + this.outline.update(); }; - private drawDropdown() { - const dropdown = new Sprite(Texture.from('/images/dropdown-white.png')); - dropdown.width = DROPDOWN_SIZE[0]; - dropdown.height = DROPDOWN_SIZE[1]; - dropdown.anchor.set(0.5); - return dropdown; - } - private tableNamePosition = (bounds: Rectangle, gridHeading: number) => { if (this.visible) { if (this.tableBounds.y < bounds.top + gridHeading) { @@ -205,43 +57,36 @@ export class Table extends Container { private headingPosition = (bounds: Rectangle, gridHeading: number) => { if (this.visible) { - if (this.headingBounds.top < bounds.top + gridHeading) { - this.headingContainer.y = bounds.top + gridHeading - this.headingBounds.top; + if (this.tableBounds.top < bounds.top + gridHeading) { + this.columnHeaders.y = bounds.top + gridHeading - this.tableBounds.top; } else { - this.headingContainer.y = 0; + this.columnHeaders.y = 0; } } }; intersectsCursor(x: number, y: number) { const rect = new Rectangle(this.codeCell.x, this.codeCell.y, this.codeCell.w - 1, this.codeCell.h - 1); - if (intersects.rectanglePoint(rect, { x, y }) || intersects.rectangleRectangle(rect, this.headingBounds)) { + if ( + intersects.rectanglePoint(rect, { x, y }) || + intersects.rectangleRectangle(rect, this.tableName.tableNameBounds) + ) { this.showActive(); return true; } return false; } - // Returns the table name bounds scaled to the viewport. - private getScaledTableNameBounds() { - const scaled = this.tableNameBounds.clone(); - scaled.width /= pixiApp.viewport.scaled; - scaled.height /= pixiApp.viewport.scaled; - scaled.y -= scaled.height - this.tableNameBounds.height; - return scaled; - } - // Checks whether the mouse cursor is hovering over the table or the table name checkHover(world: Point): boolean { return ( - intersects.rectanglePoint(this.tableBounds, world) || - intersects.rectanglePoint(this.getScaledTableNameBounds(), world) + intersects.rectanglePoint(this.tableBounds, world) || intersects.rectanglePoint(this.tableName.getScaled(), world) ); } intersectsTableName(world: Point): { table: JsRenderCodeCell; nameOrDropdown: 'name' | 'dropdown' } | undefined { - if (intersects.rectanglePoint(this.getScaledTableNameBounds(), world)) { - if (world.x <= this.tableNameBounds.x + this.tableNameText.width / pixiApp.viewport.scaled) { + if (intersects.rectanglePoint(this.tableName.getScaled(), world)) { + if (world.x <= this.tableName.x + this.tableName.getScaledTextWidth()) { return { table: this.codeCell, nameOrDropdown: 'name' }; } return { table: this.codeCell, nameOrDropdown: 'dropdown' }; @@ -297,4 +142,9 @@ export class Table extends Container { cursor ); } + + // Gets the table name bounds + getTableNameBounds(): Rectangle { + return this.tableName.tableNameBounds; + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts new file mode 100644 index 0000000000..38585e7651 --- /dev/null +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -0,0 +1,24 @@ +//! Holds a column header within a table. + +import { colors } from '@/app/theme/colors'; +import { FONT_SIZE, OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; +import { BitmapText, Container } from 'pixi.js'; + +export class TableColumnHeader extends Container { + private text: BitmapText; + + columnHeaderBounds: { x0: number; x1: number }; + + constructor(x: number, width: number, name: string) { + super(); + this.columnHeaderBounds = { x0: x, x1: x + width }; + this.text = this.addChild( + new BitmapText(name, { + fontName: 'OpenSans-Bold', + fontSize: FONT_SIZE, + tint: colors.tableHeadingForeground, + }) + ); + this.text.position.set(x + OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); + } +} diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts new file mode 100644 index 0000000000..3d971b4b58 --- /dev/null +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -0,0 +1,56 @@ +//! Holds the column headers for a table + +import { Table } from '@/app/gridGL/cells/tables/Table'; +import { TableColumnHeader } from '@/app/gridGL/cells/tables/TableColumnHeader'; +import { colors } from '@/app/theme/colors'; +import { Container, Graphics, Rectangle } from 'pixi.js'; + +export class TableColumnHeaders extends Container { + private table: Table; + private background: Graphics; + private columns: Container; + private headerHeight = 0; + + constructor(table: Table) { + super(); + this.table = table; + this.background = this.addChild(new Graphics()); + this.columns = this.addChild(new Container()); + } + + private drawBackground() { + this.background.clear(); + this.background.beginFill(colors.tableHeadingBackground); + this.background.drawShape(new Rectangle(0, 0, this.table.tableBounds.width, this.headerHeight)); + this.background.endFill(); + } + + private createColumns() { + this.columns.removeChildren(); + if (!this.table.codeCell.show_header) { + this.columns.visible = false; + return; + } + this.columns.visible = true; + let x = 0; + const codeCell = this.table.codeCell; + codeCell.column_names.forEach((column, index) => { + const width = this.table.sheet.offsets.getColumnWidth(codeCell.x + index); + this.columns.addChild(new TableColumnHeader(x, width, column.name)); + x += width; + }); + } + + // update when there is an updated code cell + update() { + if (this.table.codeCell.show_header) { + this.visible = true; + this.headerHeight = this.table.sheet.offsets.getRowHeight(this.table.codeCell.y); + console.log(this.headerHeight); + this.drawBackground(); + this.createColumns(); + } else { + this.visible = false; + } + } +} diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts new file mode 100644 index 0000000000..f5f420bec9 --- /dev/null +++ b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts @@ -0,0 +1,132 @@ +import { sheets } from '@/app/grid/controller/Sheets'; +import { DROPDOWN_SIZE } from '@/app/gridGL/cells/cellsLabel/drawSpecial'; +import { getLanguageSymbol } from '@/app/gridGL/cells/CellsMarkers'; +import { Table } from '@/app/gridGL/cells/tables/Table'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; +import { OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; +import { BitmapText, Container, Graphics, Rectangle, Sprite, Texture } from 'pixi.js'; + +export const TABLE_NAME_FONT_SIZE = 12; +export const TABLE_NAME_PADDING = [4, 2]; + +const TABLE_NAME_HEIGHT = 20; +const DROPDOWN_PADDING = 10; +const SYMBOL_SCALE = 0.5; +const SYMBOL_PADDING = 5; + +export class TableName extends Container { + private table: Table; + private background: Graphics; + private symbol: Sprite | undefined; + private text: BitmapText; + private dropdown: Sprite; + + tableNameBounds: Rectangle; + + constructor(table: Table) { + super(); + this.table = table; + this.tableNameBounds = new Rectangle(0, 0, 0, TABLE_NAME_HEIGHT); + this.background = this.addChild(new Graphics()); + this.text = this.addChild(new BitmapText('', { fontSize: TABLE_NAME_FONT_SIZE, fontName: 'OpenSans-Bold' })); + this.symbol = this.addChild(new Sprite()); + this.dropdown = this.addChild(new Sprite(Texture.from('/images/dropdown-white.png'))); + this.dropdown.anchor.set(0.5); + this.dropdown.width = DROPDOWN_SIZE[0]; + this.dropdown.height = DROPDOWN_SIZE[1]; + + // we only add to overHeadings if the sheet is active + if (sheets.sheet.id === this.table.sheet.id) { + pixiApp.overHeadings.addChild(this); + } + } + + private drawBackground() { + const width = + this.text.width + + OPEN_SANS_FIX.x + + this.dropdown.width + + DROPDOWN_PADDING + + TABLE_NAME_PADDING[0] + + (this.symbol ? SYMBOL_PADDING + this.symbol.width : 0); + this.background.clear(); + this.background.beginFill(getCSSVariableTint('primary')); + this.background.drawShape(new Rectangle(0, -TABLE_NAME_HEIGHT, width, TABLE_NAME_HEIGHT)); + this.background.endFill(); + + this.tableNameBounds.width = width; + } + + private drawSymbol() { + if (this.symbol) { + this.removeChild(this.symbol); + this.symbol = undefined; + } + if (this.table.codeCell.language !== 'Import') { + this.symbol = getLanguageSymbol(this.table.codeCell.language, false); + if (this.symbol) { + this.addChild(this.symbol); + this.symbol.width = TABLE_NAME_HEIGHT * SYMBOL_SCALE; + this.symbol.scale.y = this.symbol.scale.x; + this.symbol.anchor.set(0, 0.5); + this.symbol.y = -TABLE_NAME_HEIGHT / 2; + this.symbol.x = SYMBOL_PADDING; + if (this.table.codeCell.language === 'Formula' || this.table.codeCell.language === 'Python') { + this.symbol.tint = 0xffffff; + } + } + } + } + + private drawText() { + this.text.text = this.table.codeCell.name; + this.text.anchor.set(0, 0.5); + this.text.position.set( + TABLE_NAME_PADDING[0] + (this.symbol ? SYMBOL_PADDING + this.symbol.width : 0), + -TABLE_NAME_HEIGHT / 2 + OPEN_SANS_FIX.y + ); + } + + private drawDropdown() { + this.dropdown.position.set( + this.text.width + + OPEN_SANS_FIX.x + + DROPDOWN_PADDING + + TABLE_NAME_PADDING[0] + + (this.symbol ? SYMBOL_PADDING + this.symbol.width : 0), + -this.getTableHeight() / 2 + ); + } + + getTableHeight() { + return TABLE_NAME_HEIGHT / pixiApp.viewport.scaled; + } + + update() { + this.position.set(this.table.tableBounds.x, this.table.tableBounds.y); + this.visible = false; + + this.drawSymbol(); + this.drawText(); + this.drawDropdown(); + this.drawBackground(); + + this.tableNameBounds.x = this.table.tableBounds.x; + this.tableNameBounds.y = this.table.tableBounds.y - TABLE_NAME_HEIGHT; + } + + // Returns the table name bounds scaled to the viewport. + getScaled() { + const scaled = this.tableNameBounds.clone(); + scaled.width /= pixiApp.viewport.scaled; + scaled.height /= pixiApp.viewport.scaled; + scaled.y -= scaled.height - this.tableNameBounds.height; + return scaled; + } + + // Returns the width of the table name text scaled to the viewport. + getScaledTextWidth() { + return this.text.width / pixiApp.viewport.scaled; + } +} diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts new file mode 100644 index 0000000000..dd1e953d5f --- /dev/null +++ b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts @@ -0,0 +1,19 @@ +import { Table } from '@/app/gridGL/cells/tables/Table'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; +import { Graphics, Rectangle } from 'pixi.js'; + +export class TableOutline extends Graphics { + private table: Table; + + constructor(table: Table) { + super(); + this.table = table; + } + + update() { + this.clear(); + this.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); + this.drawShape(new Rectangle(0, 0, this.table.tableBounds.width, this.table.tableBounds.height)); + this.visible = false; + } +} diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index f17f3b566d..b499e42fd6 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -102,7 +102,7 @@ export class Tables extends Container
{ // Redraw the headings if the offsets change. sheetOffsets = (sheetId: string) => { if (sheetId === this.sheet.id) { - this.children.map((table) => table.redraw()); + this.children.map((table) => table.updateCodeCell()); } }; @@ -196,12 +196,7 @@ export class Tables extends Container
{ if (!table) { return { x: 0, y: 0, width: 0, height: 0 }; } - return { - x: table.tableNameBounds.x, - y: table.tableNameBounds.y, - width: table.tableNameBounds.width, - height: table.tableNameBounds.height, - }; + return table.getTableNameBounds(); } // Intersects a column/row rectangle From 70f7a244775711fdd8d98fffdec47b92d59bddc0 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 19 Oct 2024 15:45:15 -0700 Subject: [PATCH 079/373] send sort to client --- quadratic-client/src/app/quadratic-core-types/index.d.ts | 2 ++ quadratic-core/src/bin/export_types.rs | 5 ++++- quadratic-core/src/grid/data_table.rs | 5 +++-- quadratic-core/src/grid/js_types.rs | 3 ++- quadratic-core/src/grid/sheet/rendering.rs | 3 +++ 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index e4de21e4d5..48be36f8d9 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -63,6 +63,8 @@ export interface SheetId { id: string, } export interface SheetInfo { sheet_id: string, name: string, order: string, color: string | null, offsets: string, bounds: GridBounds, bounds_without_formatting: GridBounds, } export interface SheetPos { x: bigint, y: bigint, sheet_id: SheetId, } export interface SheetRect { min: Pos, max: Pos, sheet_id: SheetId, } +export type SortDirection = "Ascending" | "Descending" | "None"; +export interface DataTableSort { column_index: number, direction: SortDirection, } export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; diff --git a/quadratic-core/src/bin/export_types.rs b/quadratic-core/src/bin/export_types.rs index 1fcdaa55b6..65bf7dd224 100644 --- a/quadratic-core/src/bin/export_types.rs +++ b/quadratic-core/src/bin/export_types.rs @@ -28,7 +28,8 @@ use grid::sheet::validations::validation_rules::validation_text::{ }; use grid::sheet::validations::validation_rules::ValidationRule; use grid::{ - CellAlign, CellVerticalAlign, CellWrap, GridBounds, NumericFormat, NumericFormatKind, SheetId, + CellAlign, CellVerticalAlign, CellWrap, DataTableSort, GridBounds, NumericFormat, + NumericFormatKind, SheetId, SortDirection, }; use quadratic_core::color::Rgba; use quadratic_core::controller::active_transactions::transaction_name::TransactionName; @@ -128,6 +129,8 @@ fn main() { SheetInfo, SheetPos, SheetRect, + SortDirection, + DataTableSort, Span, SummarizeSelectionResult, TextCase, diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 65f41c3de6..0e85cf17ca 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -21,6 +21,7 @@ use tabled::{ builder::Builder, settings::{Color, Modify, Style}, }; +use ts_rs::TS; use super::Sheet; @@ -47,7 +48,7 @@ impl DataTableColumn { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub enum SortDirection { Ascending, Descending, @@ -65,7 +66,7 @@ impl Display for SortDirection { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub struct DataTableSort { pub column_index: usize, pub direction: SortDirection, diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index 01e53ecc32..84aee9a724 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use super::formats::format::Format; use super::formatting::{CellAlign, CellVerticalAlign, CellWrap}; use super::sheet::validations::validation::ValidationStyle; -use super::{CodeCellLanguage, DataTableColumn, NumericFormat}; +use super::{CodeCellLanguage, DataTableColumn, DataTableSort, NumericFormat}; use crate::{Pos, SheetRect}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)] @@ -196,6 +196,7 @@ pub struct JsRenderCodeCell { pub column_names: Vec, pub first_row_header: bool, pub show_header: bool, + pub sort: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index ace33350fc..b038225ee7 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -448,6 +448,7 @@ impl Sheet { column_names: data_table.send_columns(), first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, + sort: data_table.sort.clone(), }) } @@ -494,6 +495,7 @@ impl Sheet { column_names: data_table.send_columns(), first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, + sort: data_table.sort.clone(), }) } _ => None, // this should not happen. A CodeRun should always have a CellValue::Code. @@ -1127,6 +1129,7 @@ mod tests { }], first_row_header: false, show_header: true, + sort: None, }) ); } From 83f2a00a4d6cdf3cdf2b654d8ea97bd238ddd250 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 20 Oct 2024 05:03:46 -0700 Subject: [PATCH 080/373] abstracted PixiRename --- .../HTMLGrid/contextMenus/PixiRename.tsx | 118 ++++++++++++++++++ .../HTMLGrid/contextMenus/TableRename.tsx | 107 +++------------- .../gridGL/cells/tables/TableColumnHeaders.ts | 1 - .../src/app/gridGL/cells/tables/Tables.ts | 8 +- 4 files changed, 138 insertions(+), 96 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx new file mode 100644 index 0000000000..32f4b1453f --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx @@ -0,0 +1,118 @@ +import { events } from '@/app/events/events'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { focusGrid } from '@/app/helpers/focusGrid'; +import { Input } from '@/shared/shadcn/ui/input'; +import { cn } from '@/shared/shadcn/utils'; +import { Rectangle } from 'pixi.js'; +import { KeyboardEvent, useCallback, useEffect, useRef } from 'react'; + +interface Props { + position?: Rectangle; + defaultValue?: string; + + // the class name of the input + className?: string; + styles?: React.CSSProperties; + onClose: () => void; + onSave: (value: string) => void; + + // if true, the input will be the same scale as the app; otherwise it will + // scale with the viewport + noScale?: boolean; +} + +export const PixiRename = (props: Props) => { + const { position, defaultValue, className, styles, onClose, onSave, noScale } = props; + + // ensure we can wait a tick for the rename to close to avoid a conflict + // between Escape and Blur + const closed = useRef(false); + + const close = useCallback(() => { + closed.current = true; + onClose(); + focusGrid(); + }, [onClose]); + + const saveAndClose = useCallback(() => { + if (closed.current === true) return; + onClose(); + onSave(ref.current?.value ?? ''); + }, [onClose, onSave]); + + const ref = useRef(null); + + // focus the input after the position is set + useEffect(() => { + if (position) { + setTimeout(() => { + if (ref.current) { + ref.current.select(); + ref.current.focus(); + } + }, 0); + } + }, [position]); + + useEffect(() => { + const viewportChanged = () => { + if (ref.current) { + ref.current.style.transform = `scale(${1 / pixiApp.viewport.scaled})`; + } + }; + if (noScale) { + viewportChanged(); + events.on('viewportChanged', viewportChanged); + } + }, [noScale]); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + close(); + e.stopPropagation(); + e.preventDefault(); + } else if (e.key === 'Enter') { + saveAndClose(); + e.stopPropagation(); + e.preventDefault(); + } + }, + [close, saveAndClose] + ); + + const onChange = useCallback(() => { + if (ref.current) { + // need to calculate the width of the input using a span with the same css as the input + const span = document.createElement('span'); + span.className = className ?? ''; + span.style.visibility = 'hidden'; + span.style.whiteSpace = 'pre'; + span.innerText = ref.current.value; + document.body.appendChild(span); + ref.current.style.width = `${span.offsetWidth}px`; + document.body.removeChild(span); + } + }, [className]); + + if (!position) return null; + + return ( + + ); +}; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx index 8fcae5af1a..8df97c3035 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx @@ -1,97 +1,25 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - import { contextMenuAtom, ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { TABLE_NAME_FONT_SIZE, TABLE_NAME_PADDING } from '@/app/gridGL/cells/tables/TableName'; +import { PixiRename } from '@/app/gridGL/HTMLGrid/contextMenus/PixiRename'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { focusGrid } from '@/app/helpers/focusGrid'; -import { FONT_SIZE } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; -import { Input } from '@/shared/shadcn/ui/input'; -import { KeyboardEvent, useCallback, useEffect, useMemo, useRef } from 'react'; +import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; export const TableRename = () => { const contextMenu = useRecoilValue(contextMenuAtom); - const close = useCallback(() => { - events.emit('contextMenu', {}); - focusGrid(); - }, []); - - const saveAndClose = useCallback(() => { - if (contextMenu.table) { - // quadraticCore.renameDataTable(contextMenu.table.id, contextMenu.table.name); - } - close(); - }, [contextMenu.table, close]); - - const ref = useRef(null); const position = useMemo(() => { if ( contextMenu.type !== ContextMenuType.Table || contextMenu.special !== ContextMenuSpecial.rename || !contextMenu.table ) { - return { x: 0, y: 0, width: 0, height: 0 }; + return; } - const position = pixiApp.cellsSheets.current?.tables.getTableNamePosition(contextMenu.table.x, contextMenu.table.y); - if (!position) { - return { x: 0, y: 0, width: 0, height: 0 }; - } - return position; + return pixiApp.cellsSheets.current?.tables.getTableNamePosition(contextMenu.table.x, contextMenu.table.y); }, [contextMenu]); - // focus the input after the position is set - useEffect(() => { - if (position.height !== 0) { - setTimeout(() => { - if (ref.current) { - ref.current.select(); - ref.current.focus(); - } - }, 0); - } - }, [position]); - - useEffect(() => { - const viewportChanged = () => { - if (ref.current) { - ref.current.style.transform = `scale(${1 / pixiApp.viewport.scaled})`; - } - }; - events.on('viewportChanged', viewportChanged); - }, []); - - const onKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === 'Escape') { - close(); - e.stopPropagation(); - e.preventDefault(); - } else if (e.key === 'Enter') { - saveAndClose(); - e.stopPropagation(); - e.preventDefault(); - } - }, - [close, saveAndClose] - ); - - const onChange = useCallback(() => { - if (ref.current) { - // need to calculate the width of the input using a span with the same css as the input - const span = document.createElement('span'); - span.className = 'text-sm px-3 w-full'; - span.style.fontSize = FONT_SIZE.toString(); - span.style.visibility = 'hidden'; - span.style.whiteSpace = 'pre'; - span.innerText = ref.current.value; - document.body.appendChild(span); - ref.current.style.width = `${span.offsetWidth}px`; - document.body.removeChild(span); - } - }, []); - if ( contextMenu.type !== ContextMenuType.Table || contextMenu.special !== ContextMenuSpecial.rename || @@ -101,23 +29,18 @@ export const TableRename = () => { } return ( - { + if (contextMenu.table) { + console.log('TODO: rename table'); + // quadraticCore.renameDataTable(contextMenu.table.id, contextMenu.table.name); + } + }} + onClose={() => events.emit('contextMenu', {})} /> ); }; diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 3d971b4b58..4cd7fb2425 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -46,7 +46,6 @@ export class TableColumnHeaders extends Container { if (this.table.codeCell.show_header) { this.visible = true; this.headerHeight = this.table.sheet.offsets.getRowHeight(this.table.codeCell.y); - console.log(this.headerHeight); this.drawBackground(); this.createColumns(); } else { diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index b499e42fd6..cbdd6debbd 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -157,7 +157,9 @@ export class Tables extends Container
{ // until the cursor moves again. if (this.renameDataTable) { this.renameDataTable.showTableName(); - this.hoverTable = this.renameDataTable; + if (this.activeTable !== this.renameDataTable) { + this.renameDataTable.hideActive(); + } this.renameDataTable = undefined; } if (this.contextMenuTable) { @@ -191,10 +193,10 @@ export class Tables extends Container
{ pixiApp.setViewportDirty(); }; - getTableNamePosition(x: number, y: number): { x: number; y: number; width: number; height: number } { + getTableNamePosition(x: number, y: number): Rectangle | undefined { const table = this.children.find((table) => table.codeCell.x === x && table.codeCell.y === y); if (!table) { - return { x: 0, y: 0, width: 0, height: 0 }; + return; } return table.getTableNameBounds(); } From e929a0350ff67b9538058427428af21aaf0091ab Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 20 Oct 2024 06:58:55 -0700 Subject: [PATCH 081/373] sort UI --- quadratic-client/public/images/arrow-down.svg | 1 + quadratic-client/public/images/arrow-up.svg | 1 + .../HTMLGrid/contextMenus/TableRename.tsx | 1 + .../src/app/gridGL/cells/tables/Table.ts | 18 +++ .../gridGL/cells/tables/TableColumnHeader.ts | 108 ++++++++++++++++-- .../gridGL/cells/tables/TableColumnHeaders.ts | 68 ++++++++++- .../src/app/gridGL/cells/tables/TableName.ts | 6 +- .../src/app/gridGL/cells/tables/Tables.ts | 20 +++- .../interaction/keyboard/keyboardCell.ts | 9 +- .../app/gridGL/interaction/pointer/Pointer.ts | 5 +- .../interaction/pointer/PointerTable.ts | 14 ++- quadratic-client/src/app/gridGL/loadAssets.ts | 2 + .../src/app/quadratic-core-types/index.d.ts | 2 +- quadratic-client/src/app/theme/colors.ts | 2 +- .../execute_operation/execute_data_table.rs | 7 ++ quadratic-core/src/grid/sheet/rendering.rs | 12 +- 16 files changed, 242 insertions(+), 34 deletions(-) create mode 100644 quadratic-client/public/images/arrow-down.svg create mode 100644 quadratic-client/public/images/arrow-up.svg diff --git a/quadratic-client/public/images/arrow-down.svg b/quadratic-client/public/images/arrow-down.svg new file mode 100644 index 0000000000..dcdb14ec5f --- /dev/null +++ b/quadratic-client/public/images/arrow-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/quadratic-client/public/images/arrow-up.svg b/quadratic-client/public/images/arrow-up.svg new file mode 100644 index 0000000000..7929437eef --- /dev/null +++ b/quadratic-client/public/images/arrow-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx index 8df97c3035..33f5322be8 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx @@ -41,6 +41,7 @@ export const TableRename = () => { } }} onClose={() => events.emit('contextMenu', {})} + noScale /> ); }; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 35eb682e06..f20bad25a1 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -16,6 +16,7 @@ export class Table extends Container { sheet: Sheet; tableBounds: Rectangle; codeCell: JsRenderCodeCell; + tableCursor: string | undefined; constructor(sheet: Sheet, codeCell: JsRenderCodeCell) { super(); @@ -147,4 +148,21 @@ export class Table extends Container { getTableNameBounds(): Rectangle { return this.tableName.tableNameBounds; } + + pointerMove(world: Point): boolean { + const result = this.columnHeaders.pointerMove(world); + if (result) { + this.tableCursor = this.columnHeaders.tableCursor; + } else { + this.tableCursor = undefined; + } + return result; + } + + pointerDown(world: Point): boolean { + if (intersects.rectanglePoint(this.tableName.getScaled(), world)) { + return true; + } + return this.columnHeaders.pointerDown(world); + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 38585e7651..c663477917 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -1,24 +1,118 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ //! Holds a column header within a table. +import { Table } from '@/app/gridGL/cells/tables/Table'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { DataTableSort } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; import { FONT_SIZE, OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; -import { BitmapText, Container } from 'pixi.js'; +import { BitmapText, Container, Graphics, Point, Rectangle, Sprite, Texture } from 'pixi.js'; + +const SORT_BACKGROUND_ALPHA = 0.1; +const SORT_BUTTON_RADIUS = 7; +const SORT_ICON_SIZE = 12; +const SORT_BUTTON_PADDING = 3; export class TableColumnHeader extends Container { - private text: BitmapText; + private columnName: BitmapText; + private sortButton?: Graphics; + private sortIcon?: Sprite; + + private sortButtonStart = 0; + private columnHeaderBounds: Rectangle; - columnHeaderBounds: { x0: number; x1: number }; + private onSortPressed: Function; - constructor(x: number, width: number, name: string) { + tableCursor: string | undefined; + + constructor(options: { + table: Table; + x: number; + width: number; + height: number; + name: string; + sort?: DataTableSort; + onSortPressed: Function; + }) { super(); - this.columnHeaderBounds = { x0: x, x1: x + width }; - this.text = this.addChild( + const { table, x, width, height, name, sort, onSortPressed } = options; + this.onSortPressed = onSortPressed; + this.columnHeaderBounds = new Rectangle(table.tableBounds.x + x, table.tableBounds.y, width, height); + this.position.set(x, 0); + + this.columnName = this.addChild( new BitmapText(name, { fontName: 'OpenSans-Bold', fontSize: FONT_SIZE, tint: colors.tableHeadingForeground, }) ); - this.text.position.set(x + OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); + this.clipName(name, width); + this.drawSortButton(width, height, sort); + this.columnName.position.set(OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); + } + + // tests the width of the text and clips it if it is too wide + private clipName(name: string, width: number) { + let clippedName = name; + while (clippedName.length > 0 && this.columnName.width > width) { + clippedName = clippedName.slice(0, -1); + this.columnName.text = clippedName; + } + } + + private drawSortButton(width: number, height: number, sort?: DataTableSort) { + this.sortButtonStart = this.columnHeaderBounds.x + width - SORT_BUTTON_RADIUS * 2 - SORT_BUTTON_PADDING; + this.sortButton = this.addChild(new Graphics()); + this.sortButton.beginFill(0, SORT_BACKGROUND_ALPHA); + this.sortButton.drawCircle(0, 0, SORT_BUTTON_RADIUS); + this.sortButton.endFill(); + this.sortButton.position.set(width - SORT_BUTTON_RADIUS - SORT_BUTTON_PADDING, height / 2); + this.sortButton.visible = false; + + if (sort) { + let texture: Texture; + if (sort.direction === 'Descending') { + texture = Texture.from('arrow-up'); + } else if (sort.direction === 'Ascending') { + texture = Texture.from('arrow-down'); + } else { + texture = Texture.EMPTY; + } + + this.sortIcon = this.addChild(new Sprite(texture)); + this.sortIcon.anchor.set(0.5); + this.sortIcon.position = this.sortButton.position; + this.sortIcon.width = SORT_ICON_SIZE; + this.sortIcon.scale.y = this.sortIcon.scale.x; + } + } + + pointerMove(world: Point): boolean { + if (!this.sortButton) return false; + if (intersects.rectanglePoint(this.columnHeaderBounds, world)) { + if (!this.sortButton.visible) { + this.sortButton.visible = true; + pixiApp.setViewportDirty(); + } + this.tableCursor = world.x > this.sortButtonStart ? 'pointer' : undefined; + return true; + } + if (this.sortButton.visible) { + this.sortButton.visible = false; + this.tableCursor = undefined; + pixiApp.setViewportDirty(); + } + return false; + } + + pointerDown(world: Point): boolean { + if (!this.sortButton) return false; + if (intersects.rectanglePoint(this.columnHeaderBounds, world) && world.x > this.sortButtonStart) { + this.onSortPressed(); + return true; + } + return false; } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 4cd7fb2425..c550e6525e 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -1,9 +1,12 @@ //! Holds the column headers for a table +import { sheets } from '@/app/grid/controller/Sheets'; import { Table } from '@/app/gridGL/cells/tables/Table'; import { TableColumnHeader } from '@/app/gridGL/cells/tables/TableColumnHeader'; +import { SortDirection } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; -import { Container, Graphics, Rectangle } from 'pixi.js'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { Container, Graphics, Point, Rectangle } from 'pixi.js'; export class TableColumnHeaders extends Container { private table: Table; @@ -11,6 +14,8 @@ export class TableColumnHeaders extends Container { private columns: Container; private headerHeight = 0; + tableCursor: string | undefined; + constructor(table: Table) { super(); this.table = table; @@ -25,7 +30,7 @@ export class TableColumnHeaders extends Container { this.background.endFill(); } - private createColumns() { + private createColumnHeaders() { this.columns.removeChildren(); if (!this.table.codeCell.show_header) { this.columns.visible = false; @@ -36,7 +41,48 @@ export class TableColumnHeaders extends Container { const codeCell = this.table.codeCell; codeCell.column_names.forEach((column, index) => { const width = this.table.sheet.offsets.getColumnWidth(codeCell.x + index); - this.columns.addChild(new TableColumnHeader(x, width, column.name)); + this.columns.addChild( + new TableColumnHeader({ + table: this.table, + x, + width, + height: this.headerHeight, + name: column.name, + sort: codeCell.sort?.find((s) => s.column_index === column.valueIndex), + onSortPressed: () => { + // todo: once Rust is fixed, this should be the SortDirection enum + const sortOrder: SortDirection | undefined = codeCell.sort?.find( + (s) => s.column_index === column.valueIndex + )?.direction; + let newOrder: 'asc' | 'desc' | 'none' = 'none'; + switch (sortOrder) { + case undefined: + case 'None': + newOrder = 'asc'; + break; + case 'Ascending': + newOrder = 'desc'; + break; + case 'Descending': + newOrder = 'none'; + break; + } + if (!newOrder) { + throw new Error('Unknown sort order in onSortPressed'); + } + console.log(sortOrder, newOrder); + const table = this.table.codeCell; + quadraticCore.sortDataTable( + sheets.sheet.id, + table.x, + table.y, + column.valueIndex, + newOrder, + sheets.getCursorPosition() + ); + }, + }) + ); x += width; }); } @@ -47,9 +93,23 @@ export class TableColumnHeaders extends Container { this.visible = true; this.headerHeight = this.table.sheet.offsets.getRowHeight(this.table.codeCell.y); this.drawBackground(); - this.createColumns(); + this.createColumnHeaders(); } else { this.visible = false; } } + + pointerMove(world: Point): boolean { + const found = this.columns.children.find((column) => column.pointerMove(world)); + if (!found) { + this.tableCursor = undefined; + } else { + this.tableCursor = found.tableCursor; + } + return !!found; + } + + pointerDown(world: Point): boolean { + return !!this.columns.children.find((column) => column.pointerDown(world)); + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts index f5f420bec9..05fd987a31 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts @@ -95,14 +95,10 @@ export class TableName extends Container { DROPDOWN_PADDING + TABLE_NAME_PADDING[0] + (this.symbol ? SYMBOL_PADDING + this.symbol.width : 0), - -this.getTableHeight() / 2 + -TABLE_NAME_HEIGHT / 2 ); } - getTableHeight() { - return TABLE_NAME_HEIGHT / pixiApp.viewport.scaled; - } - update() { this.position.set(this.table.tableBounds.x, this.table.tableBounds.y); this.visible = false; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index cbdd6debbd..5a4cbd4e9c 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -19,6 +19,8 @@ export class Tables extends Container
{ private contextMenuTable: Table | undefined; private renameDataTable: Table | undefined; + tableCursor: string | undefined; + constructor(cellsSheet: CellsSheet) { super(); this.cellsSheet = cellsSheet; @@ -140,13 +142,29 @@ export class Tables extends Container
{ } } - pointerDown(world: Point): { table: JsRenderCodeCell; nameOrDropdown: 'name' | 'dropdown' } | undefined { + // Returns true if the pointer down as handled (eg, a column header was + // clicked). Otherwise it handles TableName. + pointerDown(world: Point): { table: JsRenderCodeCell; nameOrDropdown: 'name' | 'dropdown' } | boolean { for (const table of this.children) { const result = table.intersectsTableName(world); if (result) { return result; } + if (table.pointerDown(world)) { + return true; + } + } + return false; + } + + pointerMove(world: Point): boolean { + for (const table of this.children) { + if (table.pointerMove(world)) { + this.tableCursor = table.tableCursor; + return true; + } } + return false; } // track and activate a table whose context menu is open (this handles the diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts index 628feeb51f..04447bb300 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts @@ -133,8 +133,13 @@ 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) => { - // 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)) { + // open code cell unless this is the actual code cell (but not an import, + // which is editable). In this case we can overwrite it + if ( + code && + code.language !== 'Import' && + (Number(code.x) !== cursorPosition.x || Number(code.y) !== cursorPosition.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/Pointer.ts b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts index 3e9ad91474..8ede5029d6 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts @@ -130,7 +130,7 @@ export class Pointer { this.pointerDown.pointerMove(world, event) || this.pointerCursor.pointerMove(world, event) || this.pointerLink.pointerMove(world, event) || - this.pointerTable.pointerMove(); + this.pointerTable.pointerMove(world); this.updateCursor(); }; @@ -143,7 +143,8 @@ export class Pointer { this.pointerImages.cursor ?? this.pointerHeading.cursor ?? this.pointerAutoComplete.cursor ?? - this.pointerLink.cursor; + this.pointerLink.cursor ?? + this.pointerTable.cursor; pixiApp.canvas.style.cursor = cursor ?? 'unset'; } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index dcd10a55cb..da7f91dc6c 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -10,12 +10,14 @@ import { Point } from 'pixi.js'; // todo: dragging on double click export class PointerTable { + cursor: string | undefined; + private doubleClickTimeout: number | undefined; pointerDown(world: Point, event: PointerEvent): boolean { - const result = pixiApp.cellsSheets.current?.tables.pointerDown(world); - if (!result) { - return false; + const result = pixiApp.cellSheet().tables.pointerDown(world); + if (typeof result === 'boolean') { + return result; } if (event.button === 2 || (isMac && event.button === 0 && event.ctrlKey)) { events.emit('contextMenu', { @@ -54,11 +56,13 @@ export class PointerTable { return true; } - pointerMove(): boolean { + pointerMove(world: Point): boolean { if (this.doubleClickTimeout) { clearTimeout(this.doubleClickTimeout); this.doubleClickTimeout = undefined; } - return false; + const result = pixiApp.cellSheet().tables.pointerMove(world); + this.cursor = pixiApp.cellSheet().tables.tableCursor; + return result; } } diff --git a/quadratic-client/src/app/gridGL/loadAssets.ts b/quadratic-client/src/app/gridGL/loadAssets.ts index f2dcf9b302..5222b4fe74 100644 --- a/quadratic-client/src/app/gridGL/loadAssets.ts +++ b/quadratic-client/src/app/gridGL/loadAssets.ts @@ -53,6 +53,8 @@ export function loadAssets(): Promise { addResourceOnce('postgres-icon', '/images/postgres-icon.svg'); addResourceOnce('mysql-icon', '/images/mysql-icon.svg'); addResourceOnce('snowflake-icon', '/images/snowflake-icon.svg'); + addResourceOnce('arrow-up', '/images/arrow-up.svg'); + addResourceOnce('arrow-down', '/images/arrow-down.svg'); // Wait until pixi fonts are loaded before resolving Loader.shared.load(() => { diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 48be36f8d9..ca7f2c9024 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -37,7 +37,7 @@ export interface JsOffset { column: number | null, row: number | null, size: num export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableHeading"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, show_header: boolean, } +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index 71408d3b08..cf456b3d31 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -25,7 +25,7 @@ export const colors = { gridBackground: 0xffffff, - // table headings + // table column headings tableHeadingForeground: 0, tableHeadingBackground: 0xe3eafc, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index c01dd89dd8..91565e1330 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -274,6 +274,12 @@ impl GridController { let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; let data_table = sheet.data_table_mut(data_table_pos)?; + // DSF: this would be better if we used the enum directly. TS will + // send it as a string (using the export_types definition) and it's + // easy to parse. Additionally, we probably don't need the "None" + // value as we should use Option so we can set the + // entire table's sort value to None if there are no remaining sort + // orders. let sort_order_enum = match sort_order.as_str() { "asc" => SortDirection::Ascending, "desc" => SortDirection::Descending, @@ -284,6 +290,7 @@ impl GridController { let old_value = data_table.sort_column(column_index as usize, sort_order_enum)?; self.send_to_wasm(transaction, &sheet_rect)?; + transaction.add_code_cell(sheet_id, data_table_pos.into()); let forward_operations = vec![op]; diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index b038225ee7..ff8b42e6ee 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -213,12 +213,12 @@ impl Sheet { let column = self.get_column(x); for y in y_start..=y_end { // We skip rendering the heading row because we render it separately. - // if y == code_rect.min.y - // && data_table.show_header - // && data_table.header_is_first_row - // { - // continue; - // } + if y == code_rect.min.y + && data_table.show_header + && data_table.header_is_first_row + { + continue; + } let value = data_table.cell_value_at( (x - code_rect.min.x) as u32, (y - code_rect.min.y) as u32, From 17d80b1be9b462275a671de704f17070fd8e6a04 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 20 Oct 2024 07:40:05 -0700 Subject: [PATCH 082/373] added code for optimistically updating the sort icon --- .../gridGL/cells/tables/TableColumnHeaders.ts | 89 ++++++++++++------- .../execution/control_transaction.rs | 24 ++--- 2 files changed, 68 insertions(+), 45 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index c550e6525e..cdf0e34d26 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -3,7 +3,7 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Table } from '@/app/gridGL/cells/tables/Table'; import { TableColumnHeader } from '@/app/gridGL/cells/tables/TableColumnHeader'; -import { SortDirection } from '@/app/quadratic-core-types'; +import { JsDataTableColumn, SortDirection } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Container, Graphics, Point, Rectangle } from 'pixi.js'; @@ -30,6 +30,60 @@ export class TableColumnHeaders extends Container { this.background.endFill(); } + private onSortPressed(column: JsDataTableColumn) { + // todo: once Rust is fixed, this should be the SortDirection enum + const sortOrder: SortDirection | undefined = this.table.codeCell.sort?.find( + (s) => s.column_index === column.valueIndex + )?.direction; + let newOrder: 'asc' | 'desc' | 'none' = 'none'; + switch (sortOrder) { + case undefined: + case 'None': + newOrder = 'asc'; + break; + case 'Ascending': + newOrder = 'desc'; + break; + case 'Descending': + newOrder = 'none'; + break; + } + if (!newOrder) { + throw new Error('Unknown sort order in onSortPressed'); + } + const table = this.table.codeCell; + quadraticCore.sortDataTable( + sheets.sheet.id, + table.x, + table.y, + column.valueIndex, + newOrder, + sheets.getCursorPosition() + ); + + // todo: once Rust is fixed, this should be the SortDirection enum + // todo: not sure if this is worthwhile + // let newOrderRust: SortDirection; + // switch (newOrder) { + // case 'asc': + // newOrderRust = 'Ascending'; + // break; + // case 'desc': + // newOrderRust = 'Descending'; + // break; + // case 'none': + // newOrderRust = 'None'; + // break; + // } + + // // we optimistically update the Sort array while we wait for core to finish the sort + // table.sort = table.sort + // ? table.sort.map((s) => (s.column_index === column.valueIndex ? { ...s, direction: newOrderRust } : s)) + // : null; + // this.createColumnHeaders(); + // pixiApp.setViewportDirty(); + } + private createColumnHeaders() { this.columns.removeChildren(); if (!this.table.codeCell.show_header) { @@ -49,38 +103,7 @@ export class TableColumnHeaders extends Container { height: this.headerHeight, name: column.name, sort: codeCell.sort?.find((s) => s.column_index === column.valueIndex), - onSortPressed: () => { - // todo: once Rust is fixed, this should be the SortDirection enum - const sortOrder: SortDirection | undefined = codeCell.sort?.find( - (s) => s.column_index === column.valueIndex - )?.direction; - let newOrder: 'asc' | 'desc' | 'none' = 'none'; - switch (sortOrder) { - case undefined: - case 'None': - newOrder = 'asc'; - break; - case 'Ascending': - newOrder = 'desc'; - break; - case 'Descending': - newOrder = 'none'; - break; - } - if (!newOrder) { - throw new Error('Unknown sort order in onSortPressed'); - } - console.log(sortOrder, newOrder); - const table = this.table.codeCell; - quadraticCore.sortDataTable( - sheets.sheet.id, - table.x, - table.y, - column.valueIndex, - newOrder, - sheets.getCursorPosition() - ); - }, + onSortPressed: () => this.onSortPressed(column), }) ); x += width; diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index d9891adb55..53d31787cb 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -137,6 +137,18 @@ impl GridController { self.send_offsets_modified(*sheet_id, offsets); }); + // todo: this can be sent in less calls + transaction + .code_cells + .iter() + .for_each(|(sheet_id, positions)| { + if let Some(sheet) = self.try_sheet(*sheet_id) { + positions.iter().for_each(|pos| { + sheet.send_code_cell(*pos); + }); + } + }); + self.process_visible_dirty_hashes(&mut transaction); self.process_remaining_dirty_hashes(&mut transaction); @@ -153,18 +165,6 @@ impl GridController { } }); - // todo: this can be sent in less calls - transaction - .code_cells - .iter() - .for_each(|(sheet_id, positions)| { - if let Some(sheet) = self.try_sheet(*sheet_id) { - positions.iter().for_each(|pos| { - sheet.send_code_cell(*pos); - }); - } - }); - // todo: this can be sent in less calls transaction .html_cells From 35b37783acff0d2becd38b2ce6de05bd055d1093 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 05:29:33 -0700 Subject: [PATCH 083/373] UI tweaks; ensure difference between Code Table and Data Table --- quadratic-client/src/app/actions/actions.ts | 1 + .../src/app/actions/dataTableSpec.ts | 60 +++++++++---------- .../HTMLGrid/contextMenus/GridContextMenu.tsx | 9 ++- .../contextMenus/TableContextMenu.tsx | 2 +- .../HTMLGrid/contextMenus/TableMenu.tsx | 12 +++- .../HTMLGrid/contextMenus/contextMenu.tsx | 6 +- .../src/app/gridGL/cells/CellsSheet.ts | 7 ++- .../src/app/gridGL/cells/tables/Tables.ts | 5 +- .../src/shared/components/Icons.tsx | 8 +++ 9 files changed, 66 insertions(+), 44 deletions(-) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 1be60241de..4fd1bee648 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -146,4 +146,5 @@ export enum Action { RenameDataTable = 'rename_data_table', ToggleHeaderDataTable = 'toggle_header_data_table', DeleteDataTable = 'delete_data_table', + CodeToDataTable = 'code_to_data_table', } diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index e01eb850b6..d625a78972 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -1,10 +1,12 @@ import { Action } from '@/app/actions/actions'; -import { ContextMenuSpecial } from '@/app/atoms/contextMenuAtom'; +import { ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { createSelection } from '@/app/grid/sheet/selection'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; +import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -import { DeleteIcon, FileRenameIcon, TableConvertIcon } from '@/shared/components/Icons'; +import { DeleteIcon, FileRenameIcon, FlattenTableIcon, TableConvertIcon, TableIcon } from '@/shared/components/Icons'; import { Rectangle } from 'pixi.js'; import { sheets } from '../grid/controller/Sheets'; import { ActionSpecRecord } from './actionsSpec'; @@ -17,12 +19,9 @@ type DataTableSpec = Pick< | Action.RenameDataTable | Action.ToggleHeaderDataTable | Action.DeleteDataTable + | Action.CodeToDataTable >; -export type DataTableActionArgs = { - [Action.FlattenDataTable]: { name: string }; -}; - const isFirstRowHeader = (): boolean => { return !!pixiAppSettings.contextMenu?.table?.first_row_header; }; @@ -31,12 +30,16 @@ const isHeadingShowing = (): boolean => { return !!pixiAppSettings.contextMenu?.table?.show_header; }; +const getTable = (): JsRenderCodeCell | undefined => { + return pixiAppSettings.contextMenu?.table ?? pixiApp.cellSheet().cursorOnDataTable(); +}; + export const dataTableSpec: DataTableSpec = { [Action.FlattenDataTable]: { label: 'Flatten table', - Icon: TableConvertIcon, + Icon: FlattenTableIcon, run: async () => { - const table = pixiAppSettings.contextMenu?.table; + const table = getTable(); if (table) { quadraticCore.flattenDataTable(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); } @@ -49,29 +52,11 @@ export const dataTableSpec: DataTableSpec = { quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); }, }, - // [Action.SortDataTableFirstColAsc]: { - // label: 'Sort Data Table - First Column Ascending', - // Icon: PersonAddIcon, - // isAvailable: () => isDataTable(), - // run: async () => { - // const { x, y } = sheets.sheet.cursor.cursorPosition; - // quadraticCore.sortDataTable(sheets.sheet.id, x, y, 0, 'asc', sheets.getCursorPosition()); - // }, - // }, - // [Action.SortDataTableFirstColDesc]: { - // label: 'Sort Data Table - First Column Descending', - // Icon: PersonAddIcon, - // isAvailable: () => isDataTable(), - // run: async () => { - // const { x, y } = sheets.sheet.cursor.cursorPosition; - // quadraticCore.sortDataTable(sheets.sheet.id, x, y, 0, 'desc', sheets.getCursorPosition()); - // }, - // }, [Action.ToggleFirstRowAsHeaderDataTable]: { label: 'First row as column headings', checkbox: isFirstRowHeader, run: () => { - const table = pixiAppSettings.contextMenu?.table; + const table = getTable(); if (table) { quadraticCore.dataTableFirstRowAsHeader( sheets.sheet.id, @@ -88,11 +73,12 @@ export const dataTableSpec: DataTableSpec = { defaultOption: true, Icon: FileRenameIcon, run: async () => { + const table = getTable(); const contextMenu = pixiAppSettings.contextMenu; if (contextMenu) { setTimeout(() => { - pixiAppSettings.setContextMenu?.({ ...contextMenu, special: ContextMenuSpecial.rename }); - events.emit('contextMenu', { ...contextMenu, special: ContextMenuSpecial.rename }); + pixiAppSettings.setContextMenu?.({ type: ContextMenuType.Table, special: ContextMenuSpecial.rename, table }); + events.emit('contextMenu', { type: ContextMenuType.Table, special: ContextMenuSpecial.rename, table }); }, 0); } }, @@ -101,19 +87,29 @@ export const dataTableSpec: DataTableSpec = { label: 'Show column headings', checkbox: isHeadingShowing, run: async () => { - // const { x, y } = sheets.sheet.cursor.cursorPosition; - // quadraticCore.dataTableShowHeadings(sheets.sheet.id, x, y, sheets.getCursorPosition()); + // const table = getTable(); + // quadraticCore.dataTableShowHeadings(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); }, }, [Action.DeleteDataTable]: { label: 'Delete table', Icon: DeleteIcon, run: async () => { - const table = pixiAppSettings.contextMenu?.table; + const table = getTable(); if (table) { const selection = createSelection({ sheetId: sheets.sheet.id, rects: [new Rectangle(table.x, table.y, 1, 1)] }); quadraticCore.deleteCellValues(selection, sheets.getCursorPosition()); } }, }, + [Action.CodeToDataTable]: { + label: 'Convert to data table', + Icon: TableIcon, + run: async () => { + const table = getTable(); + if (table) { + // quadraticCore.codeToDataTable(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); + } + }, + }, }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index 1f885c45ba..c617d63d5c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -37,11 +37,14 @@ export const GridContextMenu = () => { const [columnRowAvailable, setColumnRowAvailable] = useState(false); const [canConvertToDataTable, setCanConvertToDataTable] = useState(false); const [isDataTable, setIsDataTable] = useState(false); + const [tableType, setTableType] = useState(''); useEffect(() => { const updateCursor = () => { setColumnRowAvailable(sheets.sheet.cursor.hasOneColumnRowSelection(true)); setCanConvertToDataTable(sheets.sheet.cursor.canConvertToDataTable()); - setIsDataTable(pixiApp.cellSheet().isCursorOnDataTable()); + const codeCell = pixiApp.cellSheet().cursorOnDataTable(); + setIsDataTable(!!codeCell); + setTableType(codeCell?.language === 'Import' ? 'Data' : 'Code'); }; updateCursor(); @@ -102,8 +105,8 @@ export const GridContextMenu = () => { {isDataTable && } {isDataTable && ( - - + + )} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 0a44ceaf2a..a7ccbbe9ad 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -52,7 +52,7 @@ export const TableContextMenu = () => { menuStyle={{ padding: '0', color: 'inherit' }} menuClassName="bg-background" > - + ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index 539330db92..e2152c6b1d 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -2,14 +2,22 @@ import { Action } from '@/app/actions/actions'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { MenuDivider } from '@szhsin/react-menu'; -export const TableMenu = () => { +interface Props { + isCodeTable: boolean; + defaultRename?: boolean; +} + +export const TableMenu = (props: Props) => { + const { isCodeTable, defaultRename } = props; + return ( <> - + + {isCodeTable && } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx index a4d30ff918..66d6df78e7 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx @@ -7,9 +7,13 @@ import { MenuItem } from '@szhsin/react-menu'; interface Props { action: Action; + + // allows overriding of the default option (which sets the menu item to bold) + overrideDefaultOption?: boolean; } export const MenuItemAction = (props: Props): JSX.Element | null => { + const { overrideDefaultOption } = props; const { label, Icon, run, isAvailable, checkbox, defaultOption } = defaultActionSpec[props.action]; const isAvailableArgs = useIsAvailableArgs(); const keyboardShortcut = keyboardShortcutEnumToDisplay(props.action); @@ -20,7 +24,7 @@ export const MenuItemAction = (props: Props): JSX.Element | null => { return ( - {label} + {label} ); }; diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts index aa9509ece1..dd1260fd3b 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts @@ -1,6 +1,6 @@ import { events } from '@/app/events/events'; import { Tables } from '@/app/gridGL/cells/tables/Tables'; -import { JsValidationWarning } from '@/app/quadratic-core-types'; +import { JsRenderCodeCell, JsValidationWarning } from '@/app/quadratic-core-types'; import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; import { Container, Rectangle, Sprite } from 'pixi.js'; import { pixiApp } from '../pixiApp/PixiApp'; @@ -135,7 +135,8 @@ export class CellsSheet extends Container { return this.cellsLabels.getErrorMarker(x, y) !== undefined; } - isCursorOnDataTable(): boolean { - return this.tables.isCursorOnDataTable(); + // Returns the table that the cursor is on, or undefined if the cursor is not on a table. + cursorOnDataTable(): JsRenderCodeCell | undefined { + return this.tables.cursorOnDataTable(); } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 5a4cbd4e9c..e61035f31b 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -224,7 +224,8 @@ export class Tables extends Container
{ return this.children.some((table) => table.intersects(rectangle)); } - isCursorOnDataTable(): boolean { - return this.children.some((table) => table.isCursorOnDataTable()); + // Returns the table that the cursor is on, or undefined if the cursor is not on a table. + cursorOnDataTable(): JsRenderCodeCell | undefined { + return this.children.find((table) => table.isCursorOnDataTable())?.codeCell; } } diff --git a/quadratic-client/src/shared/components/Icons.tsx b/quadratic-client/src/shared/components/Icons.tsx index fad4a409b9..ea779a84e4 100644 --- a/quadratic-client/src/shared/components/Icons.tsx +++ b/quadratic-client/src/shared/components/Icons.tsx @@ -438,3 +438,11 @@ export const TableEditIcon: IconComponent = (props) => { export const TableConvertIcon: IconComponent = (props) => { return table_convert; }; + +export const FlattenTableIcon: IconComponent = (props) => { + return view_compact; +}; + +export const TableIcon: IconComponent = (props) => { + return table; +}; From 79044768790a836fad4612c76c06d99e6dd04ca2 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 07:12:10 -0700 Subject: [PATCH 084/373] cleaning up --- quadratic-client/src/app/actions/actions.ts | 11 ++- .../src/app/actions/dataTableSpec.ts | 44 ++++++--- .../src/app/atoms/contextMenuAtom.ts | 12 +-- quadratic-client/src/app/events/events.ts | 11 +-- .../HTMLGrid/contextMenus/PixiRename.tsx | 6 +- .../contextMenus/TableColumnContextMenu.tsx | 59 +++++++++++ .../contextMenus/TableColumnHeaderRename.tsx | 60 ++++++++++++ .../contextMenus/TableContextMenu.tsx | 8 +- .../HTMLGrid/contextMenus/TableMenu.tsx | 9 +- .../HTMLGrid/contextMenus/TableRename.tsx | 13 +-- .../src/app/gridGL/cells/tables/Table.ts | 16 ++- .../gridGL/cells/tables/TableColumnHeader.ts | 28 ++++-- .../gridGL/cells/tables/TableColumnHeaders.ts | 20 +++- .../src/app/gridGL/cells/tables/Tables.ts | 28 ++++-- .../interaction/pointer/PointerTable.ts | 97 +++++++++++++------ .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 3 +- .../src/app/quadratic-core-types/index.d.ts | 2 +- quadratic-client/src/app/theme/colors.ts | 4 +- .../worker/cellsLabel/CellLabel.ts | 4 +- .../src/shared/components/Icons.tsx | 4 + .../execute_operation/execute_data_table.rs | 4 +- quadratic-core/src/grid/js_types.rs | 2 +- quadratic-core/src/grid/sheet/rendering.rs | 2 +- 23 files changed, 333 insertions(+), 114 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 4fd1bee648..3eeeb00242 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -140,11 +140,12 @@ export enum Action { DeleteRow = 'delete_row', DeleteColumn = 'delete_column', - FlattenDataTable = 'flatten_data_table', + FlattenTable = 'flatten_table', GridToDataTable = 'grid_to_data_table', - ToggleFirstRowAsHeaderDataTable = 'toggle_first_row_as_header_data_table', - RenameDataTable = 'rename_data_table', - ToggleHeaderDataTable = 'toggle_header_data_table', - DeleteDataTable = 'delete_data_table', + ToggleFirstRowAsHeaderTable = 'toggle_first_row_as_header_table', + RenameTable = 'rename_table', + ToggleHeaderTable = 'toggle_header_table', + DeleteDataTable = 'delete_table', CodeToDataTable = 'code_to_data_table', + SortTable = 'table_sort', } diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index d625a78972..4eec946fd2 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -1,25 +1,33 @@ import { Action } from '@/app/actions/actions'; -import { ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { createSelection } from '@/app/grid/sheet/selection'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -import { DeleteIcon, FileRenameIcon, FlattenTableIcon, TableConvertIcon, TableIcon } from '@/shared/components/Icons'; +import { + DeleteIcon, + FileRenameIcon, + FlattenTableIcon, + SortIcon, + TableConvertIcon, + TableIcon, +} from '@/shared/components/Icons'; import { Rectangle } from 'pixi.js'; import { sheets } from '../grid/controller/Sheets'; import { ActionSpecRecord } from './actionsSpec'; type DataTableSpec = Pick< ActionSpecRecord, - | Action.FlattenDataTable + | Action.FlattenTable | Action.GridToDataTable - | Action.ToggleFirstRowAsHeaderDataTable - | Action.RenameDataTable - | Action.ToggleHeaderDataTable + | Action.ToggleFirstRowAsHeaderTable + | Action.RenameTable + | Action.ToggleHeaderTable | Action.DeleteDataTable | Action.CodeToDataTable + | Action.SortTable >; const isFirstRowHeader = (): boolean => { @@ -35,8 +43,8 @@ const getTable = (): JsRenderCodeCell | undefined => { }; export const dataTableSpec: DataTableSpec = { - [Action.FlattenDataTable]: { - label: 'Flatten table', + [Action.FlattenTable]: { + label: 'Flatten table to grid', Icon: FlattenTableIcon, run: async () => { const table = getTable(); @@ -46,13 +54,13 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.GridToDataTable]: { - label: 'Convert values to table', + label: 'Convert values to data table', Icon: TableConvertIcon, run: async () => { quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); }, }, - [Action.ToggleFirstRowAsHeaderDataTable]: { + [Action.ToggleFirstRowAsHeaderTable]: { label: 'First row as column headings', checkbox: isFirstRowHeader, run: () => { @@ -68,7 +76,7 @@ export const dataTableSpec: DataTableSpec = { } }, }, - [Action.RenameDataTable]: { + [Action.RenameTable]: { label: 'Rename table', defaultOption: true, Icon: FileRenameIcon, @@ -77,13 +85,14 @@ export const dataTableSpec: DataTableSpec = { const contextMenu = pixiAppSettings.contextMenu; if (contextMenu) { setTimeout(() => { - pixiAppSettings.setContextMenu?.({ type: ContextMenuType.Table, special: ContextMenuSpecial.rename, table }); - events.emit('contextMenu', { type: ContextMenuType.Table, special: ContextMenuSpecial.rename, table }); + const newContextMenu = { type: ContextMenuType.Table, rename: true, table }; + pixiAppSettings.setContextMenu?.(newContextMenu); + events.emit('contextMenu', newContextMenu); }, 0); } }, }, - [Action.ToggleHeaderDataTable]: { + [Action.ToggleHeaderTable]: { label: 'Show column headings', checkbox: isHeadingShowing, run: async () => { @@ -112,4 +121,11 @@ export const dataTableSpec: DataTableSpec = { } }, }, + [Action.SortTable]: { + label: 'Sort table (coming soon)', + Icon: SortIcon, + run: async () => { + // open table sort dialog... + }, + }, }; diff --git a/quadratic-client/src/app/atoms/contextMenuAtom.ts b/quadratic-client/src/app/atoms/contextMenuAtom.ts index 31a4068852..dead4d62cd 100644 --- a/quadratic-client/src/app/atoms/contextMenuAtom.ts +++ b/quadratic-client/src/app/atoms/contextMenuAtom.ts @@ -8,10 +8,6 @@ export enum ContextMenuType { Table = 'table', } -export enum ContextMenuSpecial { - rename = 'rename', -} - export interface ContextMenuState { type?: ContextMenuType; world?: Point; @@ -21,7 +17,8 @@ export interface ContextMenuState { // special states we need to track // rename is for tables - special?: ContextMenuSpecial; + rename?: boolean; + selectedColumn?: number; } export const defaultContextMenuState: ContextMenuState = { @@ -29,6 +26,8 @@ export const defaultContextMenuState: ContextMenuState = { column: undefined, row: undefined, table: undefined, + rename: undefined, + selectedColumn: undefined, }; export interface ContextMenuOptions { @@ -37,7 +36,8 @@ export interface ContextMenuOptions { column?: number; row?: number; table?: JsRenderCodeCell; - special?: ContextMenuSpecial; + rename?: boolean; + selectedColumn?: number; } export const contextMenuAtom = atom({ diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 67c3a99a33..5e758277b1 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -1,4 +1,4 @@ -import { ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { ContextMenuOptions } from '@/app/atoms/contextMenuAtom'; import { ErrorValidation } from '@/app/gridGL/cells/CellsSheet'; import { EditingCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; import { SheetPosTS } from '@/app/gridGL/types/size'; @@ -126,14 +126,7 @@ interface EventTypes { validation: (validation: string | boolean) => void; // trigger a context menu - contextMenu: (options: { - type?: ContextMenuType; - world?: Point; - row?: number; - column?: number; - table?: JsRenderCodeCell; - special?: ContextMenuSpecial; - }) => void; + contextMenu: (options: ContextMenuOptions) => void; contextMenuClose: () => void; suggestionDropdownKeyboard: (key: 'ArrowDown' | 'ArrowUp' | 'Enter' | 'Escape' | 'Tab') => void; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx index 32f4b1453f..92d2bde315 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx @@ -36,9 +36,11 @@ export const PixiRename = (props: Props) => { const saveAndClose = useCallback(() => { if (closed.current === true) return; + if (ref.current?.value !== defaultValue) { + onSave(ref.current?.value ?? ''); + } onClose(); - onSave(ref.current?.value ?? ''); - }, [onClose, onSave]); + }, [defaultValue, onClose, onSave]); const ref = useRef(null); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx new file mode 100644 index 0000000000..22c0139465 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -0,0 +1,59 @@ +//! This shows the table column's header context menu. + +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { events } from '@/app/events/events'; +import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { focusGrid } from '@/app/helpers/focusGrid'; +import { ControlledMenu } from '@szhsin/react-menu'; +import { useCallback, useEffect, useRef } from 'react'; +import { useRecoilState } from 'recoil'; + +export const TableContextMenu = () => { + const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); + + const onClose = useCallback(() => { + setContextMenu({}); + events.emit('contextMenuClose'); + focusGrid(); + }, [setContextMenu]); + + useEffect(() => { + pixiApp.viewport.on('moved', onClose); + pixiApp.viewport.on('zoomed', onClose); + + return () => { + pixiApp.viewport.off('moved', onClose); + pixiApp.viewport.off('zoomed', onClose); + }; + }, [onClose]); + + const ref = useRef(null); + + return ( +
+ + + +
+ ); +}; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx new file mode 100644 index 0000000000..65a7726452 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -0,0 +1,60 @@ +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { events } from '@/app/events/events'; +import { TABLE_NAME_FONT_SIZE, TABLE_NAME_PADDING } from '@/app/gridGL/cells/tables/TableName'; +import { PixiRename } from '@/app/gridGL/HTMLGrid/contextMenus/PixiRename'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; + +export const TableColumnHeaderRename = () => { + const contextMenu = useRecoilValue(contextMenuAtom); + + const position = useMemo(() => { + if ( + contextMenu.type !== ContextMenuType.Table || + !contextMenu.rename || + !contextMenu.table || + contextMenu.selectedColumn === undefined + ) { + return; + } + return pixiApp.cellsSheets.current?.tables.getTableColumnHeaderPosition( + contextMenu.table.x, + contextMenu.table.y, + contextMenu.selectedColumn + ); + }, [contextMenu]); + + const originalHeaderName = useMemo(() => { + if (!contextMenu.table || contextMenu.selectedColumn === undefined) { + return; + } + return contextMenu.table.column_names[contextMenu.selectedColumn].name; + }, [contextMenu.selectedColumn, contextMenu.table]); + + if ( + contextMenu.type !== ContextMenuType.Table || + !contextMenu.rename || + !contextMenu.table || + contextMenu.selectedColumn === undefined + ) { + return null; + } + + return ( + { + if (contextMenu.table) { + console.log('TODO: rename table'); + // quadraticCore.renameDataTable(contextMenu.table.id, contextMenu.table.name); + } + }} + onClose={() => events.emit('contextMenu', {})} + noScale + /> + ); +}; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index a7ccbbe9ad..5064792e5e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -1,6 +1,6 @@ //! This shows the table context menu. -import { contextMenuAtom, ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; @@ -13,13 +13,13 @@ export const TableContextMenu = () => { const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { - if (contextMenu.type === ContextMenuType.Table && contextMenu.special === ContextMenuSpecial.rename) { + if (contextMenu.type === ContextMenuType.Table && contextMenu.rename) { return; } setContextMenu({}); events.emit('contextMenuClose'); focusGrid(); - }, [contextMenu.special, contextMenu.type, setContextMenu]); + }, [contextMenu.rename, contextMenu.type, setContextMenu]); useEffect(() => { pixiApp.viewport.on('moved', onClose); @@ -42,7 +42,7 @@ export const TableContextMenu = () => { top: contextMenu.world?.y ?? 0, transform: `scale(${1 / pixiApp.viewport.scale.x})`, pointerEvents: 'auto', - display: contextMenu.type === ContextMenuType.Table && !contextMenu.special ? 'block' : 'none', + display: contextMenu.type === ContextMenuType.Table && !contextMenu.rename ? 'block' : 'none', }} > { return ( <> - + + - - + + {isCodeTable && } - + ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx index 33f5322be8..2c4c39f0a8 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx @@ -1,4 +1,4 @@ -import { contextMenuAtom, ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { TABLE_NAME_FONT_SIZE, TABLE_NAME_PADDING } from '@/app/gridGL/cells/tables/TableName'; import { PixiRename } from '@/app/gridGL/HTMLGrid/contextMenus/PixiRename'; @@ -12,19 +12,16 @@ export const TableRename = () => { const position = useMemo(() => { if ( contextMenu.type !== ContextMenuType.Table || - contextMenu.special !== ContextMenuSpecial.rename || - !contextMenu.table + !contextMenu.rename || + !contextMenu.table || + contextMenu.selectedColumn !== undefined ) { return; } return pixiApp.cellsSheets.current?.tables.getTableNamePosition(contextMenu.table.x, contextMenu.table.y); }, [contextMenu]); - if ( - contextMenu.type !== ContextMenuType.Table || - contextMenu.special !== ContextMenuSpecial.rename || - !contextMenu.table - ) { + if (contextMenu.type !== ContextMenuType.Table || !contextMenu.rename || !contextMenu.table) { return null; } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index f20bad25a1..57c104f3ab 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -3,6 +3,7 @@ import { Sheet } from '@/app/grid/sheet/Sheet'; import { TableColumnHeaders } from '@/app/gridGL/cells/tables/TableColumnHeaders'; import { TableName } from '@/app/gridGL/cells/tables/TableName'; import { TableOutline } from '@/app/gridGL/cells/tables/TableOutline'; +import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; @@ -85,12 +86,12 @@ export class Table extends Container { ); } - intersectsTableName(world: Point): { table: JsRenderCodeCell; nameOrDropdown: 'name' | 'dropdown' } | undefined { + intersectsTableName(world: Point): TablePointerDownResult | undefined { if (intersects.rectanglePoint(this.tableName.getScaled(), world)) { if (world.x <= this.tableName.x + this.tableName.getScaledTextWidth()) { - return { table: this.codeCell, nameOrDropdown: 'name' }; + return { table: this.codeCell, type: 'table-name' }; } - return { table: this.codeCell, nameOrDropdown: 'dropdown' }; + return { table: this.codeCell, type: 'dropdown' }; } } @@ -149,6 +150,11 @@ export class Table extends Container { return this.tableName.tableNameBounds; } + // Gets the column header bounds + getColumnHeaderBounds(index: number): Rectangle { + return this.columnHeaders.getColumnHeaderBounds(index); + } + pointerMove(world: Point): boolean { const result = this.columnHeaders.pointerMove(world); if (result) { @@ -159,9 +165,9 @@ export class Table extends Container { return result; } - pointerDown(world: Point): boolean { + pointerDown(world: Point): TablePointerDownResult | undefined { if (intersects.rectanglePoint(this.tableName.getScaled(), world)) { - return true; + return { table: this.codeCell, type: 'table-name' }; } return this.columnHeaders.pointerDown(world); } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index c663477917..cddac4677a 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -2,6 +2,7 @@ //! Holds a column header within a table. import { Table } from '@/app/gridGL/cells/tables/Table'; +import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { DataTableSort } from '@/app/quadratic-core-types'; @@ -15,12 +16,15 @@ const SORT_ICON_SIZE = 12; const SORT_BUTTON_PADDING = 3; export class TableColumnHeader extends Container { + private table: Table; + private index: number; + private columnName: BitmapText; private sortButton?: Graphics; private sortIcon?: Sprite; private sortButtonStart = 0; - private columnHeaderBounds: Rectangle; + columnHeaderBounds: Rectangle; private onSortPressed: Function; @@ -28,6 +32,7 @@ export class TableColumnHeader extends Container { constructor(options: { table: Table; + index: number; x: number; width: number; height: number; @@ -36,7 +41,9 @@ export class TableColumnHeader extends Container { onSortPressed: Function; }) { super(); - const { table, x, width, height, name, sort, onSortPressed } = options; + const { table, index, x, width, height, name, sort, onSortPressed } = options; + this.table = table; + this.index = index; this.onSortPressed = onSortPressed; this.columnHeaderBounds = new Rectangle(table.tableBounds.x + x, table.tableBounds.y, width, height); this.position.set(x, 0); @@ -45,7 +52,7 @@ export class TableColumnHeader extends Container { new BitmapText(name, { fontName: 'OpenSans-Bold', fontSize: FONT_SIZE, - tint: colors.tableHeadingForeground, + tint: colors.tableColumnHeaderForeground, }) ); this.clipName(name, width); @@ -107,12 +114,15 @@ export class TableColumnHeader extends Container { return false; } - pointerDown(world: Point): boolean { - if (!this.sortButton) return false; - if (intersects.rectanglePoint(this.columnHeaderBounds, world) && world.x > this.sortButtonStart) { - this.onSortPressed(); - return true; + pointerDown(world: Point): TablePointerDownResult | undefined { + if (!this.sortButton) return; + if (intersects.rectanglePoint(this.columnHeaderBounds, world)) { + if (world.x > this.sortButtonStart) { + this.onSortPressed(); + return { table: this.table.codeCell, type: 'sort' }; + } else { + return { table: this.table.codeCell, type: 'column-name', column: this.index }; + } } - return false; } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index cdf0e34d26..35e36cc993 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -3,6 +3,7 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Table } from '@/app/gridGL/cells/tables/Table'; import { TableColumnHeader } from '@/app/gridGL/cells/tables/TableColumnHeader'; +import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; import { JsDataTableColumn, SortDirection } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; @@ -25,7 +26,7 @@ export class TableColumnHeaders extends Container { private drawBackground() { this.background.clear(); - this.background.beginFill(colors.tableHeadingBackground); + this.background.beginFill(colors.tableColumnHeaderBackground); this.background.drawShape(new Rectangle(0, 0, this.table.tableBounds.width, this.headerHeight)); this.background.endFill(); } @@ -98,6 +99,7 @@ export class TableColumnHeaders extends Container { this.columns.addChild( new TableColumnHeader({ table: this.table, + index, x, width, height: this.headerHeight, @@ -132,7 +134,19 @@ export class TableColumnHeaders extends Container { return !!found; } - pointerDown(world: Point): boolean { - return !!this.columns.children.find((column) => column.pointerDown(world)); + pointerDown(world: Point): TablePointerDownResult | undefined { + for (const column of this.columns.children) { + const result = column.pointerDown(world); + if (result) { + return result; + } + } + } + + getColumnHeaderBounds(index: number): Rectangle { + if (index < 0 || index >= this.columns.children.length) { + throw new Error('Invalid column header index in getColumnHeaderBounds'); + } + return this.columns.children[index]?.columnHeaderBounds; } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index e61035f31b..17c8aabf99 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -1,7 +1,7 @@ //! Tables renders all pixi-based UI elements for tables. Right now that's the //! headings. -import { ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { ContextMenuOptions, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; @@ -11,6 +11,12 @@ import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { JsCodeCell, JsRenderCodeCell } from '@/app/quadratic-core-types'; import { Container, Point, Rectangle } from 'pixi.js'; +export interface TablePointerDownResult { + table: JsRenderCodeCell; + type: 'table-name' | 'dropdown' | 'column-name' | 'sort'; + column?: number; +} + export class Tables extends Container
{ private cellsSheet: CellsSheet; @@ -144,17 +150,17 @@ export class Tables extends Container
{ // Returns true if the pointer down as handled (eg, a column header was // clicked). Otherwise it handles TableName. - pointerDown(world: Point): { table: JsRenderCodeCell; nameOrDropdown: 'name' | 'dropdown' } | boolean { + pointerDown(world: Point): TablePointerDownResult | undefined { for (const table of this.children) { const result = table.intersectsTableName(world); if (result) { return result; } - if (table.pointerDown(world)) { - return true; + const columnName = table.pointerDown(world); + if (columnName) { + return columnName; } } - return false; } pointerMove(world: Point): boolean { @@ -170,7 +176,7 @@ export class Tables extends Container
{ // track and activate a table whose context menu is open (this handles the // case where you hover a table and open the context menu; we want to keep // that table active while the context menu is open) - contextMenu = (options?: { type?: ContextMenuType; table?: JsRenderCodeCell; special?: ContextMenuSpecial }) => { + contextMenu = (options?: ContextMenuOptions) => { // we keep the former context menu table active after the rename finishes // until the cursor moves again. if (this.renameDataTable) { @@ -191,7 +197,7 @@ export class Tables extends Container
{ return; } if (options.type === ContextMenuType.Table && options.table) { - if (options.special === ContextMenuSpecial.rename) { + if (options.rename) { this.renameDataTable = this.children.find((table) => table.codeCell === options.table); if (this.renameDataTable) { this.renameDataTable.showActive(); @@ -219,6 +225,14 @@ export class Tables extends Container
{ return table.getTableNameBounds(); } + getTableColumnHeaderPosition(x: number, y: number, index: number): Rectangle | undefined { + const table = this.children.find((table) => table.codeCell.x === x && table.codeCell.y === y); + if (!table) { + return; + } + return table.getColumnHeaderBounds(index); + } + // Intersects a column/row rectangle intersects(rectangle: Rectangle): boolean { return this.children.some((table) => table.intersects(rectangle)); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index da7f91dc6c..094f65e2f0 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -1,7 +1,9 @@ //! Handles pointer events for data tables. -import { ContextMenuSpecial, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; import { DOUBLE_CLICK_TIME } from '@/app/gridGL/interaction/pointer/pointerUtils'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { isMac } from '@/shared/utils/isMac'; @@ -14,45 +16,84 @@ export class PointerTable { private doubleClickTimeout: number | undefined; - pointerDown(world: Point, event: PointerEvent): boolean { - const result = pixiApp.cellSheet().tables.pointerDown(world); - if (typeof result === 'boolean') { - return result; + private pointerDownTableName(world: Point, tableDown: TablePointerDownResult) { + if (this.doubleClickTimeout) { + events.emit('contextMenu', { + type: ContextMenuType.Table, + world, + column: tableDown.table.x, + row: tableDown.table.y, + table: tableDown.table, + rename: true, + }); + } else { + this.doubleClickTimeout = window.setTimeout(() => { + this.doubleClickTimeout = undefined; + }, DOUBLE_CLICK_TIME); } - if (event.button === 2 || (isMac && event.button === 0 && event.ctrlKey)) { + } + + private pointerDownDropdown(world: Point, tableDown: TablePointerDownResult) { + events.emit('contextMenu', { + type: ContextMenuType.Table, + world, + column: tableDown.table.x, + row: tableDown.table.y, + table: tableDown.table, + }); + } + + private pointerDownColumnName(world: Point, tableDown: TablePointerDownResult) { + if (tableDown.column === undefined) { + throw new Error('Expected column to be defined in pointerTable'); + } + if (this.doubleClickTimeout) { events.emit('contextMenu', { type: ContextMenuType.Table, world, - column: result.table.x, - row: result.table.y, - table: result.table, + column: tableDown.table.x, + row: tableDown.table.y, + table: tableDown.table, + rename: true, + selectedColumn: tableDown.column, + }); + } else { + // move cursor to column header + sheets.sheet.cursor.changePosition({ + cursorPosition: { x: tableDown.table.x + tableDown.column, y: tableDown.table.y }, }); + + // select entire column? + + this.doubleClickTimeout = window.setTimeout(() => { + this.doubleClickTimeout = undefined; + }, DOUBLE_CLICK_TIME); } + } - if (result.nameOrDropdown === 'name') { - if (this.doubleClickTimeout) { - events.emit('contextMenu', { - type: ContextMenuType.Table, - world, - column: result.table.x, - row: result.table.y, - table: result.table, - special: ContextMenuSpecial.rename, - }); - } else { - this.doubleClickTimeout = window.setTimeout(() => { - this.doubleClickTimeout = undefined; - }, DOUBLE_CLICK_TIME); - } - } else if (result.nameOrDropdown === 'dropdown') { + pointerDown(world: Point, event: PointerEvent): boolean { + const tableDown = pixiApp.cellSheet().tables.pointerDown(world); + if (!tableDown?.table) return false; + + if (event.button === 2 || (isMac && event.button === 0 && event.ctrlKey)) { events.emit('contextMenu', { type: ContextMenuType.Table, - column: result.table.x, - row: result.table.y, world, - table: result.table, + column: tableDown.table.x, + row: tableDown.table.y, + table: tableDown.table, }); } + + if (tableDown.type === 'table-name') { + this.pointerDownTableName(world, tableDown); + } else if (tableDown.type === 'dropdown') { + this.pointerDownDropdown(world, tableDown); + } else if (tableDown.type === 'sort') { + // tables doesn't have to do anything with sort; it's handled in TableColumnHeader + } else if (tableDown.type === 'column-name') { + this.pointerDownColumnName(world, tableDown); + } return true; } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index 884b1991ba..fc3db56c1f 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -1,7 +1,6 @@ import { CodeEditorState, defaultCodeEditorState } from '@/app/atoms/codeEditorAtom'; import { ContextMenuOptions, - ContextMenuSpecial, ContextMenuState, ContextMenuType, defaultContextMenuState, @@ -238,7 +237,7 @@ class PixiAppSettings { }; isRenamingTable(): boolean { - return this.contextMenu.type === ContextMenuType.Table && this.contextMenu.special === ContextMenuSpecial.rename; + return !!(this.contextMenu.type === ContextMenuType.Table && this.contextMenu.rename); } } diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index ca7f2c9024..96e189a200 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -36,7 +36,7 @@ export interface JsNumber { decimals: number | null, commas: boolean | null, for export interface JsOffset { column: number | null, row: number | null, size: number, } export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } -export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableHeading"; +export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index cf456b3d31..e0940a6fb1 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -26,8 +26,8 @@ export const colors = { gridBackground: 0xffffff, // table column headings - tableHeadingForeground: 0, - tableHeadingBackground: 0xe3eafc, + tableColumnHeaderForeground: 0, + tableColumnHeaderBackground: 0xe3eafc, independence: 0x5d576b, headerBackgroundColor: 0xffffff, diff --git a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts index 90a2aa7c29..e5c23f33ad 100644 --- a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts +++ b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts @@ -165,8 +165,8 @@ export class CellLabel { this.tint = convertColorStringToTint(cell.textColor); } else if (this.link) { this.tint = convertColorStringToTint(colors.link); - } else if (cell.special === 'TableHeading') { - this.tint = colors.tableHeadingForeground; + } else if (cell.special === 'TableColumnHeader') { + this.tint = colors.tableColumnHeaderForeground; } else { this.tint = 0; } diff --git a/quadratic-client/src/shared/components/Icons.tsx b/quadratic-client/src/shared/components/Icons.tsx index ea779a84e4..a98f798d5b 100644 --- a/quadratic-client/src/shared/components/Icons.tsx +++ b/quadratic-client/src/shared/components/Icons.tsx @@ -446,3 +446,7 @@ export const FlattenTableIcon: IconComponent = (props) => { export const TableIcon: IconComponent = (props) => { return table; }; + +export const SortIcon: IconComponent = (props) => { + return sort; +}; diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 91565e1330..49fad94782 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -270,9 +270,11 @@ impl GridController { { let sheet_id = sheet_pos.sheet_id; let sheet = self.try_sheet_mut_result(sheet_id)?; - let sheet_rect = SheetRect::single_sheet_pos(sheet_pos); let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; let data_table = sheet.data_table_mut(data_table_pos)?; + let sheet_rect = data_table + .output_rect(sheet_pos.into(), true) + .to_sheet_rect(sheet_id); // DSF: this would be better if we used the enum directly. TS will // send it as a string (using the export_types definition) and it's diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index 84aee9a724..0c046e4ced 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -18,7 +18,7 @@ pub enum JsRenderCellSpecial { Logical, Checkbox, List, - TableHeading, + TableColumnHeader, } #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)] diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index ff8b42e6ee..0b233c1cb4 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -230,7 +230,7 @@ impl Sheet { None }; let special = if y == code_rect.min.y && data_table.show_header { - Some(JsRenderCellSpecial::TableHeading) + Some(JsRenderCellSpecial::TableColumnHeader) } else { None }; From cd4cc1fb826a3e401824ae7a66ac5406d5873084 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 21 Oct 2024 08:15:01 -0600 Subject: [PATCH 085/373] Skip SortDirection::None --- quadratic-core/src/grid/data_table.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 0e85cf17ca..4e8b095535 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -288,7 +288,15 @@ impl DataTable { // TODO(ddimaria): skip this if SortDirection::None if let Some(ref mut sort) = self.sort.to_owned() { - for sort in sort.iter().rev() { + for sort in sort + .iter() + .rev() + .filter(|s| s.direction != SortDirection::None) + { + dbgjs!(format!( + "sorting index {} {}", + sort.column_index, sort.direction + )); let mut display_buffer = self .display_value()? .into_array()? @@ -311,6 +319,8 @@ impl DataTable { } } + dbgjs!(format!("sorts: {:?}", self.sort)); + Ok(old) } From 41dc4254be018174ada5ab4f5519b40f8f04211b Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 21 Oct 2024 08:18:52 -0600 Subject: [PATCH 086/373] Resort display buffer for when sorting --- quadratic-core/src/grid/data_table.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 4e8b095535..96096cbe09 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -285,6 +285,7 @@ impl DataTable { direction: SortDirection, ) -> Result> { let old = self.prepend_sort(column_index, direction.clone()); + self.display_buffer = None; // TODO(ddimaria): skip this if SortDirection::None if let Some(ref mut sort) = self.sort.to_owned() { From 88176e8ae1dcf73e7ceb5cb3870bd8c40daa8d5c Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 08:36:03 -0700 Subject: [PATCH 087/373] double click to edit table column name --- .../app/gridGL/HTMLGrid/HTMLGridContainer.tsx | 2 ++ .../contextMenus/TableColumnHeaderRename.tsx | 13 +++++++++---- .../src/app/gridGL/cells/tables/Table.ts | 8 ++++++++ .../gridGL/cells/tables/TableColumnHeaders.ts | 16 ++++++++++++++++ .../src/app/gridGL/cells/tables/Tables.ts | 7 ++++++- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index db40ca88fb..bc4da503da 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -3,6 +3,7 @@ import { Annotations } from '@/app/gridGL/HTMLGrid/annotations/Annotations'; import { CodeHint } from '@/app/gridGL/HTMLGrid/CodeHint'; import { CodeRunning } from '@/app/gridGL/HTMLGrid/codeRunning/CodeRunning'; import { GridContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/GridContextMenu'; +import { TableColumnHeaderRename } from '@/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename'; import { TableContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableContextMenu'; import { TableRename } from '@/app/gridGL/HTMLGrid/contextMenus/TableRename'; import { HoverCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; @@ -135,6 +136,7 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { + ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx index 65a7726452..0fac6a224e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -1,8 +1,10 @@ import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; -import { TABLE_NAME_FONT_SIZE, TABLE_NAME_PADDING } from '@/app/gridGL/cells/tables/TableName'; import { PixiRename } from '@/app/gridGL/HTMLGrid/contextMenus/PixiRename'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { convertTintToHex } from '@/app/helpers/convertColor'; +import { colors } from '@/app/theme/colors'; +import { FONT_SIZE } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; @@ -45,8 +47,12 @@ export const TableColumnHeaderRename = () => { { if (contextMenu.table) { console.log('TODO: rename table'); @@ -54,7 +60,6 @@ export const TableColumnHeaderRename = () => { } }} onClose={() => events.emit('contextMenu', {})} - noScale /> ); }; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 57c104f3ab..77204d58b5 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -124,6 +124,14 @@ export class Table extends Container { pixiApp.overHeadings.removeChild(this.tableName); } + hideColumnHeaders(index: number) { + this.columnHeaders.hide(index); + } + + showColumnHeaders() { + this.columnHeaders.show(); + } + private isShowingTableName(): boolean { return this.tableName.parent !== undefined; } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 35e36cc993..3776a33631 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -149,4 +149,20 @@ export class TableColumnHeaders extends Container { } return this.columns.children[index]?.columnHeaderBounds; } + + // Hides a column header + hide(index: number) { + if (index < 0 || index >= this.columns.children.length) { + throw new Error('Invalid column header index in hide'); + } + const column = this.columns.children[index]; + if (column) { + column.visible = false; + } + } + + // Shows all column headers + show() { + this.columns.children.forEach((column) => (column.visible = true)); + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 17c8aabf99..5413e0402f 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -181,6 +181,7 @@ export class Tables extends Container
{ // until the cursor moves again. if (this.renameDataTable) { this.renameDataTable.showTableName(); + this.renameDataTable.showColumnHeaders(); if (this.activeTable !== this.renameDataTable) { this.renameDataTable.hideActive(); } @@ -201,7 +202,11 @@ export class Tables extends Container
{ this.renameDataTable = this.children.find((table) => table.codeCell === options.table); if (this.renameDataTable) { this.renameDataTable.showActive(); - this.renameDataTable.hideTableName(); + if (options.selectedColumn === undefined) { + this.renameDataTable.hideTableName(); + } else { + this.renameDataTable.hideColumnHeaders(options.selectedColumn); + } this.hoverTable = undefined; } } else { From 076e4109e2dfdb1eaeb1ddb0d84ff70786253921 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 08:39:49 -0700 Subject: [PATCH 088/373] adding DataTable.alternating_colors --- quadratic-core/src/grid/data_table.rs | 4 ++++ quadratic-core/src/grid/file/serialize/data_table.rs | 2 ++ quadratic-core/src/grid/file/v1_7/file.rs | 1 + quadratic-core/src/grid/file/v1_8/schema.rs | 1 + 4 files changed, 8 insertions(+) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 96096cbe09..66424d9013 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -85,6 +85,7 @@ pub struct DataTable { pub readonly: bool, pub spill_error: bool, pub last_modified: DateTime, + pub alternating_colors: bool, } impl From<(Import, Array, &Sheet)> for DataTable { @@ -130,6 +131,7 @@ impl DataTable { value, readonly, spill_error, + alternating_colors: true, last_modified: Utc::now(), }; @@ -169,6 +171,7 @@ impl DataTable { readonly, spill_error, last_modified: Utc::now(), + alternating_colors: true, } } @@ -870,6 +873,7 @@ pub mod test { last_modified: Utc::now(), show_header: true, header_is_first_row: true, + alternating_colors: true, }; sheet.set_cell_value( pos, diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 19414728e8..ff579696cf 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -197,6 +197,7 @@ pub(crate) fn import_data_table_builder( .collect() }), display_buffer: data_table.display_buffer, + alternating_colors: data_table.alternating_colors, }; new_data_tables.insert(Pos { x: pos.x, y: pos.y }, data_table); @@ -398,6 +399,7 @@ pub(crate) fn export_data_tables( last_modified: Some(data_table.last_modified), spill_error: data_table.spill_error, value, + alternating_colors: data_table.alternating_colors, }; (current::PosSchema::from(pos), data_table) diff --git a/quadratic-core/src/grid/file/v1_7/file.rs b/quadratic-core/src/grid/file/v1_7/file.rs index f0dd5aa37d..b0495cde67 100644 --- a/quadratic-core/src/grid/file/v1_7/file.rs +++ b/quadratic-core/src/grid/file/v1_7/file.rs @@ -57,6 +57,7 @@ fn upgrade_code_runs( readonly: true, spill_error: code_run.spill_error, last_modified: code_run.last_modified, + alternating_colors: true, }; Ok((v1_8::PosSchema::from(pos), new_data_table)) }) diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index 3eabbe914e..b479d02f7d 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -132,6 +132,7 @@ pub struct DataTableSchema { pub readonly: bool, pub spill_error: bool, pub last_modified: Option>, + pub alternating_colors: bool, } impl From for AxisSchema { From 46ffa95e1418ab79e5f9b407bfa7b059c7fa5710 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 08:50:30 -0700 Subject: [PATCH 089/373] ensure compute and spills are recalculated --- quadratic-client/src/app/actions/dataTableSpec.ts | 2 +- .../execution/execute_operation/execute_data_table.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 4eec946fd2..b05b497750 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -61,7 +61,7 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.ToggleFirstRowAsHeaderTable]: { - label: 'First row as column headings', + label: 'First row as column headers', checkbox: isFirstRowHeader, run: () => { const table = getTable(); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 49fad94782..d426263c77 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -21,6 +21,9 @@ impl GridController { transaction.add_dirty_hashes_from_sheet_rect(*sheet_rect); if transaction.is_user() { + self.add_compute_operations(transaction, &sheet_rect, None); + self.check_all_spills(transaction, sheet_rect.sheet_id, true); + let sheet = self.try_sheet_result(sheet_rect.sheet_id)?; let rows = sheet.get_rows_with_wrap_in_rect(&(*sheet_rect).into()); From 6b8ced371eea6788a848b10f34c4ae69adf03288 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 08:52:51 -0700 Subject: [PATCH 090/373] tweak placement of compute code --- .../execute_operation/execute_data_table.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index d426263c77..85f8d1c3cf 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -21,9 +21,6 @@ impl GridController { transaction.add_dirty_hashes_from_sheet_rect(*sheet_rect); if transaction.is_user() { - self.add_compute_operations(transaction, &sheet_rect, None); - self.check_all_spills(transaction, sheet_rect.sheet_id, true); - let sheet = self.try_sheet_result(sheet_rect.sheet_id)?; let rows = sheet.get_rows_with_wrap_in_rect(&(*sheet_rect).into()); @@ -127,7 +124,7 @@ impl GridController { // sen the new value sheet.set_code_cell_value(pos, value.to_owned()); - let sheet_rect = SheetRect::from_numbers( + let data_table_rect = SheetRect::from_numbers( sheet_pos.x, sheet_pos.y, values.w as i64, @@ -135,7 +132,7 @@ impl GridController { sheet_id, ); - self.send_to_wasm(transaction, &sheet_rect)?; + self.send_to_wasm(transaction, &data_table_rect)?; let forward_operations = vec![Operation::SetDataTableAt { sheet_pos, @@ -149,7 +146,7 @@ impl GridController { self.data_table_operations( transaction, - &sheet_rect, + &data_table_rect, forward_operations, reverse_operations, ); @@ -275,7 +272,7 @@ impl GridController { let sheet = self.try_sheet_mut_result(sheet_id)?; let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; let data_table = sheet.data_table_mut(data_table_pos)?; - let sheet_rect = data_table + let data_table_sheet_rect = data_table .output_rect(sheet_pos.into(), true) .to_sheet_rect(sheet_id); @@ -294,7 +291,7 @@ impl GridController { let old_value = data_table.sort_column(column_index as usize, sort_order_enum)?; - self.send_to_wasm(transaction, &sheet_rect)?; + self.send_to_wasm(transaction, &data_table_sheet_rect)?; transaction.add_code_cell(sheet_id, data_table_pos.into()); let forward_operations = vec![op]; @@ -310,7 +307,7 @@ impl GridController { self.data_table_operations( transaction, - &sheet_rect, + &data_table_sheet_rect, forward_operations, reverse_operations, ); @@ -333,7 +330,6 @@ impl GridController { { let sheet_id = sheet_pos.sheet_id; let sheet = self.try_sheet_mut_result(sheet_id)?; - let sheet_rect = SheetRect::single_sheet_pos(sheet_pos); let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; let data_table = sheet.data_table_mut(data_table_pos)?; let data_table_rect = data_table @@ -354,7 +350,7 @@ impl GridController { self.data_table_operations( transaction, - &sheet_rect, + &data_table_rect, forward_operations, reverse_operations, ); From 7f16654e3cce940c0e87e987b6a01b4703b558f7 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 21 Oct 2024 10:11:40 -0600 Subject: [PATCH 091/373] Stabilize multi sorting --- quadratic-core/src/grid/data_table.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 96096cbe09..f39259037e 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -286,41 +286,34 @@ impl DataTable { ) -> Result> { let old = self.prepend_sort(column_index, direction.clone()); self.display_buffer = None; + let value = self.display_value()?.into_array()?; + let mut display_buffer = (0..value.height()).map(|i| i as u64).collect::>(); - // TODO(ddimaria): skip this if SortDirection::None if let Some(ref mut sort) = self.sort.to_owned() { for sort in sort .iter() .rev() .filter(|s| s.direction != SortDirection::None) { - dbgjs!(format!( - "sorting index {} {}", - sort.column_index, sort.direction - )); - let mut display_buffer = self - .display_value()? - .into_array()? - .col(sort.column_index) + display_buffer = display_buffer + .into_iter() .skip(self.adjust_for_header(0)) - .enumerate() + .map(|i| (i, value.get(sort.column_index as u32, i as u32).unwrap())) .sorted_by(|a, b| match sort.direction { SortDirection::Ascending => a.1.total_cmp(b.1), SortDirection::Descending => b.1.total_cmp(a.1), SortDirection::None => std::cmp::Ordering::Equal, }) - .map(|(i, _)| self.adjust_for_header(i) as u64) + .map(|(i, _)| i) .collect::>(); if self.header_is_first_row { display_buffer.insert(0, 0); } - - self.display_buffer = Some(display_buffer); } } - dbgjs!(format!("sorts: {:?}", self.sort)); + self.display_buffer = Some(display_buffer); Ok(old) } From 5f2c5e7d519e0628dad9055c2f224958300584da Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 10:26:28 -0700 Subject: [PATCH 092/373] alternating colors --- quadratic-client/src/app/actions/actions.ts | 1 + .../src/app/actions/dataTableSpec.ts | 13 +++++++ .../HTMLGrid/contextMenus/TableMenu.tsx | 1 + .../src/app/gridGL/cells/CellsFills.ts | 38 ++++++++++++++++++- .../src/app/gridGL/cells/CellsSheet.ts | 2 +- .../src/app/gridGL/cells/CellsSheets.ts | 2 +- .../src/app/gridGL/cells/tables/Table.ts | 9 +++++ .../gridGL/cells/tables/TableColumnHeader.ts | 2 +- .../src/app/quadratic-core-types/index.d.ts | 4 +- quadratic-client/src/app/theme/colors.ts | 3 +- .../execution/control_transaction.rs | 33 +++++++++++----- quadratic-core/src/grid/js_types.rs | 2 + quadratic-core/src/grid/sheet/rendering.rs | 9 ++++- 13 files changed, 102 insertions(+), 17 deletions(-) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 3eeeb00242..1be082e600 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -148,4 +148,5 @@ export enum Action { DeleteDataTable = 'delete_table', CodeToDataTable = 'code_to_data_table', SortTable = 'table_sort', + ToggleTableAlternatingColors = 'toggle_table_alternating_colors', } diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index b05b497750..0aa74949b1 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -28,6 +28,7 @@ type DataTableSpec = Pick< | Action.DeleteDataTable | Action.CodeToDataTable | Action.SortTable + | Action.ToggleTableAlternatingColors >; const isFirstRowHeader = (): boolean => { @@ -42,6 +43,10 @@ const getTable = (): JsRenderCodeCell | undefined => { return pixiAppSettings.contextMenu?.table ?? pixiApp.cellSheet().cursorOnDataTable(); }; +const isAlternatingColorsShowing = (): boolean => { + return !!pixiAppSettings.contextMenu?.table?.alternating_colors; +}; + export const dataTableSpec: DataTableSpec = { [Action.FlattenTable]: { label: 'Flatten table to grid', @@ -128,4 +133,12 @@ export const dataTableSpec: DataTableSpec = { // open table sort dialog... }, }, + [Action.ToggleTableAlternatingColors]: { + label: 'Toggle alternating colors', + checkbox: isAlternatingColorsShowing, + run: async () => { + console.log('TODO: toggle alternating colors'); + // quadraticCore.dataTableToggleAlternatingColors(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); + }, + }, }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index 15a5bfa2a2..1951813d0f 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -17,6 +17,7 @@ export const TableMenu = (props: Props) => { + {isCodeTable && } diff --git a/quadratic-client/src/app/gridGL/cells/CellsFills.ts b/quadratic-client/src/app/gridGL/cells/CellsFills.ts index dd437c4076..06156f601b 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsFills.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsFills.ts @@ -1,6 +1,6 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; -import { JsRenderFill, JsSheetFill } from '@/app/quadratic-core-types'; +import { JsRenderCodeCell, JsRenderFill, JsSheetFill } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; import { Container, Graphics, ParticleContainer, Rectangle, Sprite, Texture } from 'pixi.js'; import { Sheet } from '../../grid/sheet/Sheet'; @@ -24,8 +24,10 @@ export class CellsFills extends Container { private cellsSheet: CellsSheet; private cells: JsRenderFill[] = []; private metaFill?: JsSheetFill; + private alternatingColors: Map = new Map(); private cellsContainer: ParticleContainer; + private alternatingColorsGraphics: Graphics; private meta: Graphics; private dirty = false; @@ -38,6 +40,8 @@ export class CellsFills extends Container { new ParticleContainer(undefined, { vertices: true, tint: true }, undefined, true) ); + this.alternatingColorsGraphics = this.addChild(new Graphics()); + events.on('sheetFills', (sheetId, fills) => { if (sheetId === this.cellsSheet.sheetId) { this.cells = fills; @@ -128,6 +132,7 @@ export class CellsFills extends Container { if (this.dirty) { this.dirty = false; this.drawMeta(); + this.drawAlternatingColors(); } }; @@ -185,4 +190,35 @@ export class CellsFills extends Container { pixiApp.setViewportDirty(); } }; + + // this is called by Table.ts + updateAlternatingColors = (x: number, y: number, table?: JsRenderCodeCell) => { + const key = `${x},${y}`; + if (table) { + this.alternatingColors.set(key, table); + this.setDirty(); + } else { + if (this.alternatingColors.has(key)) { + this.alternatingColors.delete(key); + this.setDirty(); + } + } + }; + + private drawAlternatingColors = () => { + this.alternatingColorsGraphics.clear(); + this.alternatingColors.forEach((table, key) => { + const bounds = this.sheet.getScreenRectangle(table.x, table.y + 1, table.w - 1, table.y); + let yOffset = bounds.y; + for (let y = 0; y < table.h; y++) { + let height = this.sheet.offsets.getRowHeight(y + table.y); + if (y % 2 !== 0) { + this.alternatingColorsGraphics.beginFill(colors.tableAlternatingBackground); + this.alternatingColorsGraphics.drawRect(bounds.x, yOffset, bounds.width, height); + this.alternatingColorsGraphics.endFill(); + } + yOffset += height; + } + }); + }; } diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts index dd1260fd3b..83703784b2 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts @@ -26,8 +26,8 @@ export interface ErrorValidation { } export class CellsSheet extends Container { - private cellsFills: CellsFills; private borders: Borders; + cellsFills: CellsFills; cellsArray: CellsArray; cellsImages: CellsImages; diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts index 47c9687cb7..694fd9359a 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts @@ -79,7 +79,7 @@ export class CellsSheets extends Container { this.current.show(bounds); } - private getById(id: string): CellsSheet | undefined { + getById(id: string): CellsSheet | undefined { return this.children.find((search) => search.sheetId === id); } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 77204d58b5..7829f7c65a 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -45,6 +45,15 @@ export class Table extends Container { this.tableName.update(); this.columnHeaders.update(); this.outline.update(); + + const cellsSheet = pixiApp.cellsSheets.getById(this.sheet.id); + if (cellsSheet) { + cellsSheet.cellsFills.updateAlternatingColors( + this.codeCell.x, + this.codeCell.y, + this.codeCell.alternating_colors ? this.codeCell : undefined + ); + } }; private tableNamePosition = (bounds: Rectangle, gridHeading: number) => { diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index cddac4677a..910897554d 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -63,7 +63,7 @@ export class TableColumnHeader extends Container { // tests the width of the text and clips it if it is too wide private clipName(name: string, width: number) { let clippedName = name; - while (clippedName.length > 0 && this.columnName.width > width) { + while (clippedName.length > 0 && this.columnName.width + SORT_BUTTON_RADIUS * 2 + SORT_BUTTON_PADDING > width) { clippedName = clippedName.slice(0, -1); this.columnName.text = clippedName; } diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 96e189a200..2d37865375 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -36,8 +36,8 @@ export interface JsNumber { decimals: number | null, commas: boolean | null, for export interface JsOffset { column: number | null, row: number | null, size: number, } export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } -export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, } +export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader" | "TableAlternatingColor"; +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index e0940a6fb1..8dd264da18 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -25,9 +25,10 @@ export const colors = { gridBackground: 0xffffff, - // table column headings + // table colors tableColumnHeaderForeground: 0, tableColumnHeaderBackground: 0xe3eafc, + tableAlternatingBackground: 0xf8fafe, independence: 0x5d576b, headerBackgroundColor: 0xffffff, diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index 53d31787cb..052e091f2c 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -6,12 +6,12 @@ use crate::controller::active_transactions::transaction_name::TransactionName; use crate::controller::operations::operation::Operation; use crate::controller::transaction::Transaction; use crate::controller::transaction_types::JsCodeResult; -use crate::error_core::Result; +use crate::error_core::{CoreError, Result}; use crate::grid::js_types::JsHtmlOutput; -use crate::grid::{CodeRun, DataTable, DataTableKind}; +use crate::grid::{CodeCellLanguage, CodeRun, ConnectionKind, DataTable, DataTableKind}; use crate::parquet::parquet_to_vec; use crate::renderer_constants::{CELL_SHEET_HEIGHT, CELL_SHEET_WIDTH}; -use crate::{Pos, RunError, RunErrorMsg, Value}; +use crate::{CellValue, Pos, RunError, RunErrorMsg, Value}; impl GridController { // loop compute cycle until complete or an async call is made @@ -299,17 +299,32 @@ impl GridController { Value::Array(array.into()) }; - let sheet = self.try_sheet_result(current_sheet_pos.sheet_id)?; + let Some(sheet) = self.try_sheet(current_sheet_pos.sheet_id) else { + return Err(CoreError::CodeCellSheetError("Sheet not found".to_string())); + }; + let Some(CellValue::Code(code)) = sheet.cell_value_ref(current_sheet_pos.into()) else { + return Err(CoreError::CodeCellSheetError( + "Code cell not found".to_string(), + )); + }; - // todo: this should be true sometimes... - let show_header = false; + let name = match code.language { + CodeCellLanguage::Connection { kind, .. } => match kind { + ConnectionKind::Postgres => "Postgres 1", + ConnectionKind::Mysql => "MySQL 1", + ConnectionKind::Mssql => "MSSQL 1", + ConnectionKind::Snowflake => "Snowflake 1", + }, + // this should not happen + _ => "Connection 1", + }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), - &sheet.next_data_table_name(), + name, value, false, - false, - show_header, + true, + true, ); self.finalize_code_run(&mut transaction, current_sheet_pos, Some(data_table), None); diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index 0c046e4ced..e4be5b2041 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -19,6 +19,7 @@ pub enum JsRenderCellSpecial { Checkbox, List, TableColumnHeader, + TableAlternatingColor, } #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)] @@ -197,6 +198,7 @@ pub struct JsRenderCodeCell { pub first_row_header: bool, pub show_header: bool, pub sort: Option>, + pub alternating_colors: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 0b233c1cb4..1d8e68c781 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -232,7 +232,11 @@ impl Sheet { let special = if y == code_rect.min.y && data_table.show_header { Some(JsRenderCellSpecial::TableColumnHeader) } else { - None + if (y - code_rect.min.y) % 2 == 0 { + Some(JsRenderCellSpecial::TableAlternatingColor) + } else { + None + } }; cells.push( self.get_render_cell(x, y, column, &value, language, special), @@ -449,6 +453,7 @@ impl Sheet { first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, sort: data_table.sort.clone(), + alternating_colors: data_table.alternating_colors, }) } @@ -496,6 +501,7 @@ impl Sheet { first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, sort: data_table.sort.clone(), + alternating_colors: data_table.alternating_colors, }) } _ => None, // this should not happen. A CodeRun should always have a CellValue::Code. @@ -1130,6 +1136,7 @@ mod tests { first_row_header: false, show_header: true, sort: None, + alternating_colors: true, }) ); } From 682518a409bfea3355d74811820a6cf13202d2ee Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 10:26:42 -0700 Subject: [PATCH 093/373] remove comment --- quadratic-client/src/app/theme/colors.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index 8dd264da18..d0b0a0cd34 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -20,7 +20,6 @@ export const colors = { cursorCell: 0x2463eb, searchCell: 0x50c878, - // todo: this is new and should be reviewed movingCells: 0x2463eb, gridBackground: 0xffffff, From 5c12e8468afb80a758149ae6688185c876094d45 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 10:28:52 -0700 Subject: [PATCH 094/373] fix bug with alternating_colors --- quadratic-client/src/app/gridGL/cells/CellsFills.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsFills.ts b/quadratic-client/src/app/gridGL/cells/CellsFills.ts index 06156f601b..5decbefaf8 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsFills.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsFills.ts @@ -207,10 +207,10 @@ export class CellsFills extends Container { private drawAlternatingColors = () => { this.alternatingColorsGraphics.clear(); - this.alternatingColors.forEach((table, key) => { + this.alternatingColors.forEach((table) => { const bounds = this.sheet.getScreenRectangle(table.x, table.y + 1, table.w - 1, table.y); let yOffset = bounds.y; - for (let y = 0; y < table.h; y++) { + for (let y = 0; y < table.h - 1; y++) { let height = this.sheet.offsets.getRowHeight(y + table.y); if (y % 2 !== 0) { this.alternatingColorsGraphics.beginFill(colors.tableAlternatingBackground); From 685c765ced3dd16b7229fe897facd72986e779c0 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 10:45:33 -0700 Subject: [PATCH 095/373] move table colors to styles.css --- .../HTMLGrid/contextMenus/TableColumnHeaderRename.tsx | 6 ++---- quadratic-client/src/app/gridGL/cells/CellsFills.ts | 5 +++-- .../src/app/gridGL/cells/tables/TableColumnHeader.ts | 5 +++-- .../src/app/gridGL/cells/tables/TableColumnHeaders.ts | 5 +++-- quadratic-client/src/app/theme/colors.ts | 5 ----- .../web-workers/renderWebWorker/renderClientMessages.ts | 4 ++++ .../app/web-workers/renderWebWorker/renderWebWorker.ts | 2 ++ .../renderWebWorker/worker/cellsLabel/CellLabel.ts | 3 ++- .../web-workers/renderWebWorker/worker/renderClient.ts | 8 +++++++- quadratic-client/src/shared/shadcn/styles.css | 5 +++++ 10 files changed, 31 insertions(+), 17 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx index 0fac6a224e..6625a70f21 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -2,8 +2,6 @@ import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { PixiRename } from '@/app/gridGL/HTMLGrid/contextMenus/PixiRename'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { convertTintToHex } from '@/app/helpers/convertColor'; -import { colors } from '@/app/theme/colors'; import { FONT_SIZE } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; @@ -50,8 +48,8 @@ export const TableColumnHeaderRename = () => { className="origin-bottom-left border-none p-0 text-sm font-bold text-primary-foreground outline-none" styles={{ fontSize: FONT_SIZE, - color: convertTintToHex(colors.tableColumnHeaderForeground), - backgroundColor: convertTintToHex(colors.tableColumnHeaderBackground), + color: 'var(--table-column-header-foreground)', + backgroundColor: 'var(--table-column-header-background)', }} onSave={() => { if (contextMenu.table) { diff --git a/quadratic-client/src/app/gridGL/cells/CellsFills.ts b/quadratic-client/src/app/gridGL/cells/CellsFills.ts index 5decbefaf8..e9c9f634f2 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsFills.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsFills.ts @@ -4,7 +4,7 @@ import { JsRenderCodeCell, JsRenderFill, JsSheetFill } from '@/app/quadratic-cor import { colors } from '@/app/theme/colors'; import { Container, Graphics, ParticleContainer, Rectangle, Sprite, Texture } from 'pixi.js'; import { Sheet } from '../../grid/sheet/Sheet'; -import { convertColorStringToTint } from '../../helpers/convertColor'; +import { convertColorStringToTint, getCSSVariableTint } from '../../helpers/convertColor'; import { intersects } from '../helpers/intersects'; import { pixiApp } from '../pixiApp/PixiApp'; import { CellsSheet } from './CellsSheet'; @@ -207,13 +207,14 @@ export class CellsFills extends Container { private drawAlternatingColors = () => { this.alternatingColorsGraphics.clear(); + const color = getCSSVariableTint('table-alternating-background'); this.alternatingColors.forEach((table) => { const bounds = this.sheet.getScreenRectangle(table.x, table.y + 1, table.w - 1, table.y); let yOffset = bounds.y; for (let y = 0; y < table.h - 1; y++) { let height = this.sheet.offsets.getRowHeight(y + table.y); if (y % 2 !== 0) { - this.alternatingColorsGraphics.beginFill(colors.tableAlternatingBackground); + this.alternatingColorsGraphics.beginFill(color); this.alternatingColorsGraphics.drawRect(bounds.x, yOffset, bounds.width, height); this.alternatingColorsGraphics.endFill(); } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 910897554d..710ba40488 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -5,8 +5,8 @@ import { Table } from '@/app/gridGL/cells/tables/Table'; import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { DataTableSort } from '@/app/quadratic-core-types'; -import { colors } from '@/app/theme/colors'; import { FONT_SIZE, OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; import { BitmapText, Container, Graphics, Point, Rectangle, Sprite, Texture } from 'pixi.js'; @@ -48,11 +48,12 @@ export class TableColumnHeader extends Container { this.columnHeaderBounds = new Rectangle(table.tableBounds.x + x, table.tableBounds.y, width, height); this.position.set(x, 0); + const tint = getCSSVariableTint('table-column-header-foreground'); this.columnName = this.addChild( new BitmapText(name, { fontName: 'OpenSans-Bold', fontSize: FONT_SIZE, - tint: colors.tableColumnHeaderForeground, + tint, }) ); this.clipName(name, width); diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 3776a33631..7dfda3805d 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -4,8 +4,8 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Table } from '@/app/gridGL/cells/tables/Table'; import { TableColumnHeader } from '@/app/gridGL/cells/tables/TableColumnHeader'; import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { JsDataTableColumn, SortDirection } from '@/app/quadratic-core-types'; -import { colors } from '@/app/theme/colors'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Container, Graphics, Point, Rectangle } from 'pixi.js'; @@ -26,7 +26,8 @@ export class TableColumnHeaders extends Container { private drawBackground() { this.background.clear(); - this.background.beginFill(colors.tableColumnHeaderBackground); + const color = getCSSVariableTint('table-column-header-background'); + this.background.beginFill(color); this.background.drawShape(new Rectangle(0, 0, this.table.tableBounds.width, this.headerHeight)); this.background.endFill(); } diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index d0b0a0cd34..f2ad1d2a3b 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -24,11 +24,6 @@ export const colors = { gridBackground: 0xffffff, - // table colors - tableColumnHeaderForeground: 0, - tableColumnHeaderBackground: 0xe3eafc, - tableAlternatingBackground: 0xf8fafe, - independence: 0x5d576b, headerBackgroundColor: 0xffffff, headerSelectedBackgroundColor: 0x2463eb, diff --git a/quadratic-client/src/app/web-workers/renderWebWorker/renderClientMessages.ts b/quadratic-client/src/app/web-workers/renderWebWorker/renderClientMessages.ts index f5384f9a1d..71ecbb9365 100644 --- a/quadratic-client/src/app/web-workers/renderWebWorker/renderClientMessages.ts +++ b/quadratic-client/src/app/web-workers/renderWebWorker/renderClientMessages.ts @@ -7,6 +7,10 @@ import type { RenderSpecial } from './worker/cellsLabel/CellsTextHashSpecial'; export interface ClientRenderInit { type: 'clientRenderInit'; bitmapFonts: RenderBitmapFonts; + + // this is taken from the CSS variable (which is not accessible in the + // worker): --table-column-header-foreground + tableColumnHeaderForeground: number; } // also includes sending the data as transferable ArrayBuffers diff --git a/quadratic-client/src/app/web-workers/renderWebWorker/renderWebWorker.ts b/quadratic-client/src/app/web-workers/renderWebWorker/renderWebWorker.ts index 08ae040c79..0c1b8d5263 100644 --- a/quadratic-client/src/app/web-workers/renderWebWorker/renderWebWorker.ts +++ b/quadratic-client/src/app/web-workers/renderWebWorker/renderWebWorker.ts @@ -1,5 +1,6 @@ import { debugWebWorkers, debugWebWorkersMessages } from '@/app/debugFlags'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { Rectangle } from 'pixi.js'; import { prepareBitmapFontInformation } from './renderBitmapFonts'; import { @@ -32,6 +33,7 @@ class RenderWebWorker { const message: ClientRenderInit = { type: 'clientRenderInit', bitmapFonts: prepareBitmapFontInformation(), + tableColumnHeaderForeground: getCSSVariableTint('table-column-header-foreground'), }; this.worker.postMessage(message, [coreMessagePort]); if (debugWebWorkers) console.log('[renderWebWorker] initialized.'); diff --git a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts index e5c23f33ad..0ccb5e55e1 100644 --- a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts +++ b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts @@ -23,6 +23,7 @@ import { CellsLabels } from '@/app/web-workers/renderWebWorker/worker/cellsLabel import { convertNumber, reduceDecimals } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/convertNumber'; import { LabelMeshEntry } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/LabelMeshEntry'; import { LabelMeshes } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/LabelMeshes'; +import { renderClient } from '@/app/web-workers/renderWebWorker/worker/renderClient'; import { CELL_HEIGHT, CELL_TEXT_MARGIN_LEFT, MIN_CELL_WIDTH } from '@/shared/constants/gridConstants'; import { removeItems } from '@pixi/utils'; import { Point, Rectangle } from 'pixi.js'; @@ -166,7 +167,7 @@ export class CellLabel { } else if (this.link) { this.tint = convertColorStringToTint(colors.link); } else if (cell.special === 'TableColumnHeader') { - this.tint = colors.tableColumnHeaderForeground; + this.tint = renderClient.tableColumnHeaderForeground; } else { this.tint = 0; } diff --git a/quadratic-client/src/app/web-workers/renderWebWorker/worker/renderClient.ts b/quadratic-client/src/app/web-workers/renderWebWorker/worker/renderClient.ts index 35dbba7c06..dcc2775d1b 100644 --- a/quadratic-client/src/app/web-workers/renderWebWorker/worker/renderClient.ts +++ b/quadratic-client/src/app/web-workers/renderWebWorker/worker/renderClient.ts @@ -22,6 +22,8 @@ import { renderText } from './renderText'; declare var self: WorkerGlobalScope & typeof globalThis; class RenderClient { + tableColumnHeaderForeground = 0; + constructor() { self.onmessage = this.handleMessage; } @@ -35,6 +37,7 @@ class RenderClient { case 'clientRenderInit': renderText.clientInit(e.data.bitmapFonts); renderCore.init(e.ports[0]); + this.tableColumnHeaderForeground = e.data.tableColumnHeaderForeground; return; case 'clientRenderViewport': @@ -68,7 +71,10 @@ class RenderClient { return; default: - console.warn('[renderClient] Unhandled message type', e.data); + // ignore messages from react dev tools + if (!(e.data as any)?.source) { + console.warn('[renderClient] Unhandled message type', e.data); + } } }; diff --git a/quadratic-client/src/shared/shadcn/styles.css b/quadratic-client/src/shared/shadcn/styles.css index 1bb296f725..f9590238bc 100644 --- a/quadratic-client/src/shared/shadcn/styles.css +++ b/quadratic-client/src/shared/shadcn/styles.css @@ -39,6 +39,11 @@ --ring: 221.2 83.2% 53.3%; /* Border radius for card, input and buttons */ --radius: 0.3rem; + + /* Sheet Table Colors */ + --table-column-header-foreground: 0 0% 0%; + --table-column-header-background: 223 81% 94%; + --table-alternating-background: 220 75% 98%; } .dark { From 5411b9766f7316197d0b94a0eeecb7d914cd19dd Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 10:51:13 -0700 Subject: [PATCH 096/373] fix bug with sort button --- .../src/app/gridGL/cells/tables/TableColumnHeader.ts | 3 ++- .../src/app/gridGL/cells/tables/TableColumnHeaders.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 710ba40488..6efab24ba4 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -20,10 +20,11 @@ export class TableColumnHeader extends Container { private index: number; private columnName: BitmapText; - private sortButton?: Graphics; private sortIcon?: Sprite; private sortButtonStart = 0; + + sortButton?: Graphics; columnHeaderBounds: Rectangle; private onSortPressed: Function; diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 7dfda3805d..6f02c81121 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -4,6 +4,7 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Table } from '@/app/gridGL/cells/tables/Table'; import { TableColumnHeader } from '@/app/gridGL/cells/tables/TableColumnHeader'; import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { JsDataTableColumn, SortDirection } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; @@ -132,6 +133,16 @@ export class TableColumnHeaders extends Container { } else { this.tableCursor = found.tableCursor; } + + // ensure we clear the sort button on any other column header + this.columns.children.forEach((column) => { + if (column !== found) { + if (column.sortButton?.visible) { + column.sortButton.visible = false; + pixiApp.setViewportDirty(); + } + } + }); return !!found; } From 06d46164a0f19f5462c0900bd92024ba1025d9b4 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 21 Oct 2024 10:56:10 -0700 Subject: [PATCH 097/373] fix alternating colors --- quadratic-client/src/app/gridGL/cells/CellsFills.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsFills.ts b/quadratic-client/src/app/gridGL/cells/CellsFills.ts index e9c9f634f2..d9f4a6e30e 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsFills.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsFills.ts @@ -211,9 +211,9 @@ export class CellsFills extends Container { this.alternatingColors.forEach((table) => { const bounds = this.sheet.getScreenRectangle(table.x, table.y + 1, table.w - 1, table.y); let yOffset = bounds.y; - for (let y = 0; y < table.h - 1; y++) { + for (let y = table.show_header ? 1 : 0; y < table.h - 1; y++) { let height = this.sheet.offsets.getRowHeight(y + table.y); - if (y % 2 !== 0) { + if (y % 2 === 0) { this.alternatingColorsGraphics.beginFill(color); this.alternatingColorsGraphics.drawRect(bounds.x, yOffset, bounds.width, height); this.alternatingColorsGraphics.endFill(); From da896601cca34065a59e9c61d83905f1d1d6dac1 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 21 Oct 2024 13:54:25 -0600 Subject: [PATCH 098/373] Rename data table in rust and TS --- quadratic-api/src/data/current_blank.grid | Bin 261 -> 260 bytes .../src/app/actions/dataTableSpec.ts | 3 ++ .../contextMenus/TableColumnHeaderRename.tsx | 2 +- .../HTMLGrid/contextMenus/TableRename.tsx | 21 ++++++-- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../quadraticCore/coreClientMessages.ts | 10 ++++ .../quadraticCore/quadraticCore.ts | 11 +++++ .../web-workers/quadraticCore/worker/core.ts | 5 ++ .../quadraticCore/worker/coreClient.ts | 4 ++ .../active_transactions/transaction_name.rs | 1 + .../execute_operation/execute_data_table.rs | 45 ++++++++++++++++++ .../execution/execute_operation/mod.rs | 4 +- .../src/controller/operations/data_table.rs | 9 ++++ .../src/controller/operations/operation.rs | 11 +++++ .../src/controller/user_actions/data_table.rs | 10 ++++ quadratic-core/src/grid/data_table.rs | 45 +++++++++++++++++- quadratic-core/src/grid/sheet/data_table.rs | 6 +++ .../wasm_bindings/controller/data_table.rs | 16 +++++++ .../data/grid/current_blank.grid | Bin 261 -> 260 bytes quadratic-rust-shared/src/auto_gen_path.rs | 4 +- 20 files changed, 199 insertions(+), 10 deletions(-) diff --git a/quadratic-api/src/data/current_blank.grid b/quadratic-api/src/data/current_blank.grid index 3e81b823b6a06e05d63fcc50d19d361d0e863f97..07f4cd25d80246245efef7f59e8ea432bfa9851d 100644 GIT binary patch literal 260 zcmV+f0sH;~000000000nE;uT90aZ}#ZiFBZe3fQ@YlE#C>udCbyt4X)mGL`wUoSVx{@`YOUuMZyD|o$ z0L^y+J$y%zw+aeaP@#cdA4(N~W)(z$GwR^B+0$tli(wqo?6K?UO=RVcPma$@T!jtI z&YhjOQduvT7F5S;F&7O!ug!=iBdq+TY-9!xt|aM8jAs5njr}J#TIyR`r%%l(H!%;% z+=IiPoMA@iaL-JM&1G!l?S2swd1drOVj{XNp>rYC&hZG7d<0qChY;u{4`D2YrUO^+ K9fUu-Z;v0}0CkW6 literal 261 zcmV+g0s8&}000000000nE;uT90aZ}jj)Nc&{gur;>&8pBYyFLVG{&GnOM=b@uuV$* z_YNpc`a;f`%Q=VfU~fV=3vK5?acK7!YT5ALsXXU(QHlK2oT~iNR6MWik`?FX!X^8G zErW&40$TVBB3~tBFrh{R?Xqf}0U9od04u12Qrg|A>$9%!L#EuG6Q+n46e~ zXzsz`f66eTb2ulag>(rUb-f*gMBW)ak&uXPOK4w+wS7FmC?7x)_uID7P42>23QY$t LzZ=+Ij46)=J_UP& diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index b05b497750..a95a5fed59 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -83,10 +83,13 @@ export const dataTableSpec: DataTableSpec = { run: async () => { const table = getTable(); const contextMenu = pixiAppSettings.contextMenu; + console.log('contextMenu', contextMenu); if (contextMenu) { setTimeout(() => { const newContextMenu = { type: ContextMenuType.Table, rename: true, table }; pixiAppSettings.setContextMenu?.(newContextMenu); + console.log('newContextMenu', newContextMenu); + console.log('table', table); events.emit('contextMenu', newContextMenu); }, 0); } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx index 0fac6a224e..d30d023c22 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -55,7 +55,7 @@ export const TableColumnHeaderRename = () => { }} onSave={() => { if (contextMenu.table) { - console.log('TODO: rename table'); + console.log('TODO: rename column heading'); // quadraticCore.renameDataTable(contextMenu.table.id, contextMenu.table.name); } }} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx index 2c4c39f0a8..c43fd105fa 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx @@ -3,6 +3,7 @@ import { events } from '@/app/events/events'; import { TABLE_NAME_FONT_SIZE, TABLE_NAME_PADDING } from '@/app/gridGL/cells/tables/TableName'; import { PixiRename } from '@/app/gridGL/HTMLGrid/contextMenus/PixiRename'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; @@ -31,10 +32,22 @@ export const TableRename = () => { position={position} className="origin-bottom-left bg-primary px-3 text-sm font-bold text-primary-foreground" styles={{ fontSize: TABLE_NAME_FONT_SIZE, paddingLeft: TABLE_NAME_PADDING[0] }} - onSave={() => { - if (contextMenu.table) { - console.log('TODO: rename table'); - // quadraticCore.renameDataTable(contextMenu.table.id, contextMenu.table.name); + onSave={(value: string) => { + if (contextMenu.table && pixiApp.cellsSheets.current) { + console.log( + 'TbleRename.tsx: onSave()', + pixiApp.cellsSheets.current?.sheetId, + contextMenu.table.x, + contextMenu.table.y, + value + ); + quadraticCore.updateDataTableName( + pixiApp.cellsSheets.current?.sheetId, + contextMenu.table.x, + contextMenu.table.y, + value, + '' + ); } }} onClose={() => events.emit('contextMenu', {})} diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 96e189a200..8744321f7a 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -69,7 +69,7 @@ export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; export type TextMatch = { "Exactly": TextCase } | { "Contains": TextCase } | { "NotContains": TextCase } | { "TextLength": { min: number | null, max: number | null, } }; -export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "GridToDataTable" | "DataTableFirstRowAsHeader" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; +export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "GridToDataTable" | "UpdateDataTableName" | "DataTableFirstRowAsHeader" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; export interface TransientResize { row: bigint | null, column: bigint | null, old_size: number, new_size: number, } export interface Validation { id: string, selection: Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } export interface ValidationDateTime { ignore_blank: boolean, require_date: boolean, require_time: boolean, prohibit_date: boolean, prohibit_time: boolean, ranges: Array, } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index e3a887a263..911fe67043 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -1030,6 +1030,15 @@ export interface ClientCoreGridToDataTable { cursor: string; } +export interface ClientCoreUpdateDataTableName { + type: 'clientCoreUpdateDataTableName'; + sheetId: string; + x: number; + y: number; + name: string; + cursor: string; +} + export interface ClientCoreSortDataTable { type: 'clientCoreSortDataTable'; sheetId: string; @@ -1131,6 +1140,7 @@ export type ClientCoreMessage = | ClientCoreInsertRow | ClientCoreFlattenDataTable | ClientCoreGridToDataTable + | ClientCoreUpdateDataTableName | ClientCoreSortDataTable | ClientCoreDataTableFirstRowAsHeader; diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index 0cf4ebc392..41e95ceb1e 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -1199,6 +1199,17 @@ class QuadraticCore { }); } + updateDataTableName(sheetId: string, x: number, y: number, name: string, cursor: string) { + this.send({ + type: 'clientCoreUpdateDataTableName', + sheetId, + x, + y, + name, + cursor, + }); + } + sortDataTable(sheetId: string, x: number, y: number, columnIndex: number, sortOrder: string, cursor: string) { this.send({ type: 'clientCoreSortDataTable', diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index aa71464e2f..4621aa79ee 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1125,6 +1125,11 @@ class Core { this.gridController.gridToDataTable(JSON.stringify(selection, bigIntReplacer), cursor); } + updateDataTableName(sheetId: string, x: number, y: number, name: string, cursor: string) { + if (!this.gridController) throw new Error('Expected gridController to be defined'); + this.gridController.updateDataTableName(sheetId, posToPos(x, y), name, cursor); + } + sortDataTable(sheetId: string, x: number, y: number, column_index: number, sort_order: string, cursor: string) { if (!this.gridController) throw new Error('Expected gridController to be defined'); this.gridController.sortDataTable(sheetId, posToPos(x, y), column_index, sort_order, cursor); diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index 4aae59ef85..8953f42237 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -589,6 +589,10 @@ class CoreClient { core.gridToDataTable(e.data.selection, e.data.cursor); return; + case 'clientCoreUpdateDataTableName': + core.updateDataTableName(e.data.sheetId, e.data.x, e.data.y, e.data.name, e.data.cursor); + return; + case 'clientCoreSortDataTable': core.sortDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.columnIndex, e.data.sortOrder, e.data.cursor); return; diff --git a/quadratic-core/src/controller/active_transactions/transaction_name.rs b/quadratic-core/src/controller/active_transactions/transaction_name.rs index 41c422d5b8..efd61440be 100644 --- a/quadratic-core/src/controller/active_transactions/transaction_name.rs +++ b/quadratic-core/src/controller/active_transactions/transaction_name.rs @@ -18,6 +18,7 @@ pub enum TransactionName { RunCode, FlattenDataTable, GridToDataTable, + UpdateDataTableName, DataTableFirstRowAsHeader, Import, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 85f8d1c3cf..6d41021a47 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -257,6 +257,51 @@ impl GridController { bail!("Expected Operation::GridToDataTable in execute_grid_to_data_table"); } + pub(super) fn execute_update_data_table_name( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::UpdateDataTableName { + sheet_pos, + ref name, + } = op + { + let sheet_id = sheet_pos.sheet_id; + let sheet = self.try_sheet_mut_result(sheet_id)?; + let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; + let data_table = sheet.data_table_mut(data_table_pos)?; + + let old_name = data_table.name.to_owned(); + data_table.name = name.to_owned(); + + let data_table_rect = data_table + .output_rect(sheet_pos.into(), true) + .to_sheet_rect(sheet_id); + + self.send_to_wasm(transaction, &data_table_rect)?; + transaction.add_code_cell(sheet_id, data_table_pos.into()); + + let forward_operations = vec![op]; + + let reverse_operations = vec![Operation::UpdateDataTableName { + sheet_pos, + name: old_name, + }]; + + self.data_table_operations( + transaction, + &data_table_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::UpdateDataTableName in execute_update_data_table_name"); + } + pub(super) fn execute_sort_data_table( &mut self, transaction: &mut PendingTransaction, diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 512363d222..f5283fb57f 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -41,6 +41,9 @@ impl GridController { Operation::GridToDataTable { .. } => Self::handle_execution_operation_result( self.execute_grid_to_data_table(transaction, op), ), + Operation::UpdateDataTableName { .. } => Self::handle_execution_operation_result( + self.execute_update_data_table_name(transaction, op), + ), Operation::SortDataTable { .. } => Self::handle_execution_operation_result( self.execute_sort_data_table(transaction, op), ), @@ -107,7 +110,6 @@ impl GridController { pub fn execute_reverse_operations(gc: &mut GridController, transaction: &PendingTransaction) { let mut undo_transaction = PendingTransaction::default(); undo_transaction.operations = transaction.reverse_operations.clone().into(); - println!("reverse_operations: {:?}", undo_transaction.operations); gc.execute_operation(&mut undo_transaction); } diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index 6901b398c3..1918fc86ad 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -18,6 +18,15 @@ impl GridController { vec![Operation::GridToDataTable { sheet_rect }] } + pub fn update_data_table_name_operations( + &self, + sheet_pos: SheetPos, + name: String, + _cursor: Option, + ) -> Vec { + vec![Operation::UpdateDataTableName { sheet_pos, name }] + } + pub fn sort_data_table_operations( &self, sheet_pos: SheetPos, diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 264ede3d99..06a59403c3 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -51,6 +51,10 @@ pub enum Operation { GridToDataTable { sheet_rect: SheetRect, }, + UpdateDataTableName { + sheet_pos: SheetPos, + name: String, + }, SortDataTable { sheet_pos: SheetPos, column_index: u32, @@ -223,6 +227,13 @@ impl fmt::Display for Operation { Operation::GridToDataTable { sheet_rect } => { write!(fmt, "GridToDataTable {{ sheet_rect: {} }}", sheet_rect) } + Operation::UpdateDataTableName { sheet_pos, name } => { + write!( + fmt, + "UpdateDataTableName {{ sheet_pos: {} name: {} }}", + sheet_pos, name + ) + } Operation::SortDataTable { sheet_pos, column_index, diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index c446da6e51..1a11d1a824 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -35,6 +35,16 @@ impl GridController { self.start_user_transaction(ops, cursor, TransactionName::GridToDataTable); } + pub fn update_data_table_name( + &mut self, + sheet_pos: SheetPos, + name: String, + cursor: Option, + ) { + let ops = self.update_data_table_name_operations(sheet_pos, name, cursor.to_owned()); + self.start_user_transaction(ops, cursor, TransactionName::UpdateDataTableName); + } + pub fn sort_data_table( &mut self, sheet_pos: SheetPos, diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index a88fe762ad..a095e282be 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -16,6 +16,7 @@ use crate::{ use anyhow::{anyhow, Ok, Result}; use chrono::{DateTime, Utc}; use itertools::Itertools; +use regex::Regex; use serde::{Deserialize, Serialize}; use tabled::{ builder::Builder, @@ -23,7 +24,45 @@ use tabled::{ }; use ts_rs::TS; -use super::Sheet; +use super::{Grid, Sheet}; + +impl Grid { + pub fn unique_data_table_name(&mut self, name: &str) -> Result { + let re = Regex::new(r"\d+$").unwrap(); + let base = re.replace(&name, ""); + + let all_names = self + .sheets() + .iter() + .flat_map(|sheet| sheet.data_tables.values().map(|table| &table.name)) + .collect::>(); + + let mut num = 1; + let mut name = String::from(""); + + while name == "" { + let new_name = format!("{}{}", base, num); + + if !all_names.contains(&&new_name) { + name = new_name; + } + + num += 1; + } + + Ok(name) + } + + pub fn update_data_table_name(&mut self, sheet_pos: SheetPos, name: &str) -> Result<()> { + let sheet = self + .try_sheet_mut(sheet_pos.sheet_id) + .ok_or_else(|| anyhow!("Sheet {} not found", sheet_pos.sheet_id))?; + + sheet.update_table_name(sheet_pos.into(), name)?; + + Ok(()) + } +} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct DataTableColumn { @@ -282,6 +321,10 @@ impl DataTable { } } + pub fn update_table_name(&mut self, name: &str) { + self.name = name.into(); + } + pub fn sort_column( &mut self, column_index: usize, diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index 9791a97fbe..a9c714e773 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -46,6 +46,12 @@ impl Sheet { } } + pub fn update_table_name(&mut self, pos: Pos, name: &str) -> Result<()> { + self.data_table_mut(pos)?.update_table_name(name); + + Ok(()) + } + /// Returns a DatatTable at a Pos pub fn data_table(&self, pos: Pos) -> Option<&DataTable> { self.data_tables.get(&pos) diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index 023979a060..87f440bf73 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -67,6 +67,22 @@ impl GridController { cursor, ); + Ok(()) + } + /// Flattens a Data Table + #[wasm_bindgen(js_name = "updateDataTableName")] + pub fn js_update_data_table_name( + &mut self, + sheet_id: String, + pos: String, + name: String, + cursor: Option, + ) -> Result<(), JsValue> { + let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; + let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; + + self.update_data_table_name(pos.to_sheet_pos(sheet_id), name, cursor); + Ok(()) } } diff --git a/quadratic-rust-shared/data/grid/current_blank.grid b/quadratic-rust-shared/data/grid/current_blank.grid index 3e81b823b6a06e05d63fcc50d19d361d0e863f97..07f4cd25d80246245efef7f59e8ea432bfa9851d 100644 GIT binary patch literal 260 zcmV+f0sH;~000000000nE;uT90aZ}#ZiFBZe3fQ@YlE#C>udCbyt4X)mGL`wUoSVx{@`YOUuMZyD|o$ z0L^y+J$y%zw+aeaP@#cdA4(N~W)(z$GwR^B+0$tli(wqo?6K?UO=RVcPma$@T!jtI z&YhjOQduvT7F5S;F&7O!ug!=iBdq+TY-9!xt|aM8jAs5njr}J#TIyR`r%%l(H!%;% z+=IiPoMA@iaL-JM&1G!l?S2swd1drOVj{XNp>rYC&hZG7d<0qChY;u{4`D2YrUO^+ K9fUu-Z;v0}0CkW6 literal 261 zcmV+g0s8&}000000000nE;uT90aZ}jj)Nc&{gur;>&8pBYyFLVG{&GnOM=b@uuV$* z_YNpc`a;f`%Q=VfU~fV=3vK5?acK7!YT5ALsXXU(QHlK2oT~iNR6MWik`?FX!X^8G zErW&40$TVBB3~tBFrh{R?Xqf}0U9od04u12Qrg|A>$9%!L#EuG6Q+n46e~ zXzsz`f66eTb2ulag>(rUb-f*gMBW)ak&uXPOK4w+wS7FmC?7x)_uID7P42>23QY$t LzZ=+Ij46)=J_UP& diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index 8df3d2b4d5..a0dea21ae2 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From cade65ba97b00f687fb262c4bc33bb527f23dd16 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 21 Oct 2024 14:23:40 -0600 Subject: [PATCH 099/373] Unique data table names --- .../HTMLGrid/contextMenus/TableRename.tsx | 7 ---- .../execute_operation/execute_data_table.rs | 3 +- .../src/controller/operations/import.rs | 15 +++---- quadratic-core/src/grid/data_table.rs | 31 ++++++++++----- quadratic-core/src/grid/file/v1_7/file.rs | 2 +- quadratic-core/src/grid/sheet/data_table.rs | 39 +++---------------- 6 files changed, 35 insertions(+), 62 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx index c43fd105fa..9db863eb3e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx @@ -34,13 +34,6 @@ export const TableRename = () => { styles={{ fontSize: TABLE_NAME_FONT_SIZE, paddingLeft: TABLE_NAME_PADDING[0] }} onSave={(value: string) => { if (contextMenu.table && pixiApp.cellsSheets.current) { - console.log( - 'TbleRename.tsx: onSave()', - pixiApp.cellsSheets.current?.sheetId, - contextMenu.table.x, - contextMenu.table.y, - value - ); quadraticCore.updateDataTableName( pixiApp.cellsSheets.current?.sheetId, contextMenu.table.x, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 6d41021a47..3395a056ba 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -223,7 +223,8 @@ impl GridController { let old_values = sheet.cell_values_in_rect(&rect, false)?; let import = Import::new("simple.csv".into()); - let data_table = DataTable::from((import.to_owned(), old_values.to_owned(), sheet)); + let data_table = + DataTable::from((import.to_owned(), old_values.to_owned(), &self.grid)); let cell_value = CellValue::Import(import.to_owned()); let sheet = self.try_sheet_mut_result(sheet_id)?; diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 62a3cfcf94..cbc06557c3 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -114,10 +114,7 @@ impl GridController { } // finally add the final operation - let sheet = self - .try_sheet(sheet_id) - .ok_or_else(|| anyhow!("Sheet {sheet_id} not found"))?; - let mut data_table = DataTable::from((import.to_owned(), cell_values, sheet)); + let mut data_table = DataTable::from((import.to_owned(), cell_values, &self.grid)); data_table.name = file_name.to_string(); let sheet_pos = SheetPos::from((insert_at, sheet_id)); @@ -383,11 +380,9 @@ impl GridController { } } } - let sheet = self - .try_sheet(sheet_id) - .ok_or_else(|| anyhow!("Sheet {sheet_id} not found"))?; + let sheet_pos = SheetPos::from((insert_at, sheet_id)); - let mut data_table = DataTable::from((import.to_owned(), cell_values, sheet)); + let mut data_table = DataTable::from((import.to_owned(), cell_values, &self.grid)); data_table.apply_first_row_as_header(); // this operation must be before the SetCodeRun operations @@ -473,7 +468,7 @@ mod test { let import = Import::new(file_name.into()); let cell_value = CellValue::Import(import.clone()); let sheet = gc.try_sheet(sheet_id).unwrap(); - let mut expected_data_table = DataTable::from((import, values.into(), sheet)); + let mut expected_data_table = DataTable::from((import, values.into(), &gc.grid)); assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); let data_table = match ops[1].clone() { @@ -482,7 +477,7 @@ mod test { }; expected_data_table.last_modified = data_table.last_modified; expected_data_table.name = file_name.to_string(); - + let expected = Operation::SetCodeRun { sheet_pos: SheetPos { x: 0, diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index a095e282be..0f964b4ac9 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -24,10 +24,10 @@ use tabled::{ }; use ts_rs::TS; -use super::{Grid, Sheet}; +use super::Grid; impl Grid { - pub fn unique_data_table_name(&mut self, name: &str) -> Result { + pub fn unique_data_table_name(&self, name: &str) -> String { let re = Regex::new(r"\d+$").unwrap(); let base = re.replace(&name, ""); @@ -50,18 +50,23 @@ impl Grid { num += 1; } - Ok(name) + name } pub fn update_data_table_name(&mut self, sheet_pos: SheetPos, name: &str) -> Result<()> { + let unique_name = self.unique_data_table_name(name); let sheet = self .try_sheet_mut(sheet_pos.sheet_id) .ok_or_else(|| anyhow!("Sheet {} not found", sheet_pos.sheet_id))?; - sheet.update_table_name(sheet_pos.into(), name)?; + sheet.update_table_name(sheet_pos.into(), &unique_name)?; Ok(()) } + + pub fn next_data_table_name(&self) -> String { + self.unique_data_table_name("Table") + } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -127,9 +132,9 @@ pub struct DataTable { pub alternating_colors: bool, } -impl From<(Import, Array, &Sheet)> for DataTable { - fn from((import, cell_values, sheet): (Import, Array, &Sheet)) -> Self { - let name = sheet.next_data_table_name(); +impl From<(Import, Array, &Grid)> for DataTable { + fn from((import, cell_values, grid): (Import, Array, &Grid)) -> Self { + let name = grid.unique_data_table_name(&import.file_name); DataTable::new( DataTableKind::Import(import), @@ -647,7 +652,11 @@ pub mod test { use std::collections::HashSet; use super::*; - use crate::{controller::GridController, grid::SheetId, Array}; + use crate::{ + controller::GridController, + grid::{Sheet, SheetId}, + Array, + }; use serial_test::parallel; pub fn test_csv_values() -> Vec> { @@ -660,12 +669,14 @@ pub mod test { } pub fn new_data_table() -> (Sheet, DataTable) { - let sheet = GridController::test().grid().sheets()[0].clone(); + let gc = GridController::test(); + let grid = gc.grid(); + let sheet = grid.sheets()[0].clone(); let file_name = "test.csv"; let values = test_csv_values(); let import = Import::new(file_name.into()); let array = Array::from_str_vec(values, true).unwrap(); - let data_table = DataTable::from((import.clone(), array, &sheet)); + let data_table = DataTable::from((import.clone(), array, grid)); (sheet, data_table) } diff --git a/quadratic-core/src/grid/file/v1_7/file.rs b/quadratic-core/src/grid/file/v1_7/file.rs index b0495cde67..7e3b506f90 100644 --- a/quadratic-core/src/grid/file/v1_7/file.rs +++ b/quadratic-core/src/grid/file/v1_7/file.rs @@ -47,7 +47,7 @@ fn upgrade_code_runs( }; let new_data_table = v1_8::DataTableSchema { kind: v1_8::DataTableKindSchema::CodeRun(new_code_run), - name: format!("Table {}", i), + name: format!("Table{}", i), header_is_first_row: false, show_header: false, columns: None, diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index a9c714e773..0291afdc2e 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -1,26 +1,9 @@ use super::Sheet; -use crate::{ - grid::{data_table::DataTable, DataTableKind}, - Pos, Value, -}; +use crate::{grid::data_table::DataTable, Pos}; use anyhow::{anyhow, bail, Result}; impl Sheet { - pub fn new_data_table( - &mut self, - pos: Pos, - kind: DataTableKind, - value: Value, - spill_error: bool, - header: bool, - ) -> Option { - let name = self.next_data_table_name(); - let data_table = DataTable::new(kind, &name, value, spill_error, header, true); - - self.set_data_table(pos, Some(data_table)) - } - /// Sets or deletes a data table. /// /// Returns the old value if it was set. @@ -32,20 +15,6 @@ impl Sheet { } } - pub fn next_data_table_name(&self) -> String { - let mut i = self.data_tables.len() + 1; - - loop { - let name = format!("Table {}", i); - - if !self.data_tables.values().any(|table| table.name == name) { - return name; - } - - i += 1; - } - } - pub fn update_table_name(&mut self, pos: Pos, name: &str) -> Result<()> { self.data_table_mut(pos)?.update_table_name(name); @@ -105,7 +74,11 @@ impl Sheet { #[cfg(test)] mod test { use super::*; - use crate::{controller::GridController, grid::CodeRun, CellValue, Value}; + use crate::{ + controller::GridController, + grid::{CodeRun, DataTableKind}, + CellValue, Value, + }; use bigdecimal::BigDecimal; use serial_test::parallel; use std::{collections::HashSet, vec}; From 9b281e8aca3ec63c497854404f8630b5c9f32ef3 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 21 Oct 2024 17:35:19 -0600 Subject: [PATCH 100/373] Code to Data Table checkpoint --- .../src/app/actions/dataTableSpec.ts | 4 +- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../quadraticCore/coreClientMessages.ts | 9 ++ .../quadraticCore/quadraticCore.ts | 10 +++ .../web-workers/quadraticCore/worker/core.ts | 5 ++ .../quadraticCore/worker/coreClient.ts | 4 + .../active_transactions/transaction_name.rs | 1 + .../execute_operation/execute_data_table.rs | 88 +++++++++++++++---- .../execution/execute_operation/mod.rs | 3 + .../src/controller/operations/data_table.rs | 28 +++++- .../src/controller/operations/import.rs | 1 - .../src/controller/operations/operation.rs | 13 ++- .../src/controller/user_actions/data_table.rs | 11 +++ quadratic-core/src/grid/data_table.rs | 20 ++--- .../wasm_bindings/controller/data_table.rs | 19 +++- 15 files changed, 180 insertions(+), 38 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 3ab5d2c67a..6a8f17f85d 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -93,8 +93,6 @@ export const dataTableSpec: DataTableSpec = { setTimeout(() => { const newContextMenu = { type: ContextMenuType.Table, rename: true, table }; pixiAppSettings.setContextMenu?.(newContextMenu); - console.log('newContextMenu', newContextMenu); - console.log('table', table); events.emit('contextMenu', newContextMenu); }, 0); } @@ -125,7 +123,7 @@ export const dataTableSpec: DataTableSpec = { run: async () => { const table = getTable(); if (table) { - // quadraticCore.codeToDataTable(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); + quadraticCore.codeDataTableToDataTable(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); } }, }, diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index a93973ace4..600438d05c 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -69,7 +69,7 @@ export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; export type TextMatch = { "Exactly": TextCase } | { "Contains": TextCase } | { "NotContains": TextCase } | { "TextLength": { min: number | null, max: number | null, } }; -export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "GridToDataTable" | "UpdateDataTableName" | "DataTableFirstRowAsHeader" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; +export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "SwitchDataTableKind" | "GridToDataTable" | "UpdateDataTableName" | "DataTableFirstRowAsHeader" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; export interface TransientResize { row: bigint | null, column: bigint | null, old_size: number, new_size: number, } export interface Validation { id: string, selection: Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } export interface ValidationDateTime { ignore_blank: boolean, require_date: boolean, require_time: boolean, prohibit_date: boolean, prohibit_time: boolean, ranges: Array, } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index 911fe67043..ca76fb8cb6 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -1024,6 +1024,14 @@ export interface ClientCoreFlattenDataTable { cursor: string; } +export interface ClientCoreCodeDataTableToDataTable { + type: 'clientCoreCodeDataTableToDataTable'; + sheetId: string; + x: number; + y: number; + cursor: string; +} + export interface ClientCoreGridToDataTable { type: 'clientCoreGridToDataTable'; selection: Selection; @@ -1139,6 +1147,7 @@ export type ClientCoreMessage = | ClientCoreInsertColumn | ClientCoreInsertRow | ClientCoreFlattenDataTable + | ClientCoreCodeDataTableToDataTable | ClientCoreGridToDataTable | ClientCoreUpdateDataTableName | ClientCoreSortDataTable diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index 41e95ceb1e..167d19cdfd 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -1191,6 +1191,16 @@ class QuadraticCore { }); } + codeDataTableToDataTable(sheetId: string, x: number, y: number, cursor: string) { + this.send({ + type: 'clientCoreCodeDataTableToDataTable', + sheetId, + x, + y, + cursor, + }); + } + gridToDataTable(selection: Selection, cursor: string) { this.send({ type: 'clientCoreGridToDataTable', diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 4621aa79ee..ba6e340489 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1120,6 +1120,11 @@ class Core { this.gridController.flattenDataTable(sheetId, posToPos(x, y), cursor); } + codeDataTableToDataTable(sheetId: string, x: number, y: number, cursor: string) { + if (!this.gridController) throw new Error('Expected gridController to be defined'); + this.gridController.codeDataTableToDataTable(sheetId, posToPos(x, y), cursor); + } + gridToDataTable(selection: Selection, cursor: string) { if (!this.gridController) throw new Error('Expected gridController to be defined'); this.gridController.gridToDataTable(JSON.stringify(selection, bigIntReplacer), cursor); diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index 8953f42237..8ba8a1c652 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -585,6 +585,10 @@ class CoreClient { core.flattenDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.cursor); return; + case 'clientCoreCodeDataTableToDataTable': + core.codeDataTableToDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.cursor); + return; + case 'clientCoreGridToDataTable': core.gridToDataTable(e.data.selection, e.data.cursor); return; diff --git a/quadratic-core/src/controller/active_transactions/transaction_name.rs b/quadratic-core/src/controller/active_transactions/transaction_name.rs index efd61440be..846ae9d875 100644 --- a/quadratic-core/src/controller/active_transactions/transaction_name.rs +++ b/quadratic-core/src/controller/active_transactions/transaction_name.rs @@ -17,6 +17,7 @@ pub enum TransactionName { SetCode, RunCode, FlattenDataTable, + SwitchDataTableKind, GridToDataTable, UpdateDataTableName, DataTableFirstRowAsHeader, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 3395a056ba..9aeba5cfc5 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -4,7 +4,7 @@ use crate::{ active_transactions::pending_transaction::PendingTransaction, operations::operation::Operation, GridController, }, - grid::{DataTable, SortDirection}, + grid::{DataTable, DataTableKind, SortDirection}, ArraySize, CellValue, Pos, Rect, SheetRect, }; @@ -101,7 +101,11 @@ impl GridController { transaction: &mut PendingTransaction, op: Operation, ) -> Result<()> { - if let Operation::SetDataTableAt { sheet_pos, values } = op { + if let Operation::SetDataTableAt { + sheet_pos, + ref values, + } = op + { let sheet_id = sheet_pos.sheet_id; let mut pos = Pos::from(sheet_pos); let sheet = self.try_sheet_mut_result(sheet_id)?; @@ -134,11 +138,7 @@ impl GridController { self.send_to_wasm(transaction, &data_table_rect)?; - let forward_operations = vec![Operation::SetDataTableAt { - sheet_pos, - values: value.into(), - }]; - + let forward_operations = vec![op]; let reverse_operations = vec![Operation::SetDataTableAt { sheet_pos, values: old_value.into(), @@ -174,7 +174,6 @@ impl GridController { let values = data_table.display_value()?.into_array()?; let ArraySize { w, h } = values.size(); - let sheet_pos = data_table_pos.to_sheet_pos(sheet_id); let max = Pos { x: data_table_pos.x - 1 + w.get() as i64, y: data_table_pos.y - 1 + h.get() as i64, @@ -191,9 +190,72 @@ impl GridController { self.send_to_wasm(transaction, &sheet_rect)?; transaction.add_code_cell(sheet_id, data_table_pos); - let forward_operations = vec![Operation::FlattenDataTable { sheet_pos }]; + let forward_operations = vec![op]; + let reverse_operations = vec![ + Operation::GridToDataTable { sheet_rect }, + Operation::SwitchDataTableKind { + sheet_pos, + kind: data_table.kind, + }, + ]; + + self.data_table_operations( + transaction, + &sheet_rect, + forward_operations, + reverse_operations, + ); + self.check_deleted_data_tables(transaction, &sheet_rect); + + return Ok(()); + }; + + bail!("Expected Operation::FlattenDataTable in execute_flatten_data_table"); + } + + pub(super) fn execute_code_data_table_to_data_table( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::SwitchDataTableKind { + sheet_pos, + ref kind, + } = op + { + let sheet_id = sheet_pos.sheet_id; + let pos = Pos::from(sheet_pos); + let sheet = self.try_sheet_mut_result(sheet_id)?; + let data_table_pos = sheet.first_data_table_within(pos)?; + let data_table = sheet.data_table_mut(data_table_pos)?; + let old_data_table_kind = data_table.kind.to_owned(); + let old_data_table_name = data_table.name.to_owned(); + let sheet_rect = data_table.output_sheet_rect(sheet_pos, false); + + data_table.kind = match old_data_table_kind { + DataTableKind::CodeRun(_) => match kind { + DataTableKind::CodeRun(_) => kind.to_owned(), + DataTableKind::Import(import) => DataTableKind::Import(import.to_owned()), + }, + DataTableKind::Import(_) => match kind { + DataTableKind::CodeRun(code_run) => DataTableKind::CodeRun(code_run.to_owned()), + DataTableKind::Import(_) => kind.to_owned(), + }, + }; + + // let the client know that the code cell changed to remove the styles + if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() { + transaction.add_code_cell(sheet_id, data_table_pos); + } + + self.send_to_wasm(transaction, &sheet_rect)?; + transaction.add_code_cell(sheet_id, data_table_pos); - let reverse_operations = vec![Operation::GridToDataTable { sheet_rect }]; + let forward_operations = vec![op]; + let reverse_operations = vec![Operation::SwitchDataTableKind { + sheet_pos, + kind: old_data_table_kind, + }]; self.data_table_operations( transaction, @@ -241,8 +303,7 @@ impl GridController { self.send_to_wasm(transaction, &sheet_rect)?; - let forward_operations = vec![Operation::GridToDataTable { sheet_rect }]; - + let forward_operations = vec![op]; let reverse_operations = vec![Operation::FlattenDataTable { sheet_pos }]; self.data_table_operations( @@ -284,7 +345,6 @@ impl GridController { transaction.add_code_cell(sheet_id, data_table_pos.into()); let forward_operations = vec![op]; - let reverse_operations = vec![Operation::UpdateDataTableName { sheet_pos, name: old_name, @@ -341,7 +401,6 @@ impl GridController { transaction.add_code_cell(sheet_id, data_table_pos.into()); let forward_operations = vec![op]; - let reverse_operations = vec![Operation::SortDataTable { sheet_pos, column_index, @@ -388,7 +447,6 @@ impl GridController { transaction.add_code_cell(sheet_id, sheet_pos.into()); let forward_operations = vec![op]; - let reverse_operations = vec![Operation::DataTableFirstRowAsHeader { sheet_pos, first_row_is_header: !first_row_is_header, diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index f5283fb57f..9c5332bd8d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -38,6 +38,9 @@ impl GridController { Operation::FlattenDataTable { .. } => Self::handle_execution_operation_result( self.execute_flatten_data_table(transaction, op), ), + Operation::SwitchDataTableKind { .. } => Self::handle_execution_operation_result( + self.execute_code_data_table_to_data_table(transaction, op), + ), Operation::GridToDataTable { .. } => Self::handle_execution_operation_result( self.execute_grid_to_data_table(transaction, op), ), diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index 1918fc86ad..f86cb3090a 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -1,5 +1,10 @@ use super::operation::Operation; -use crate::{controller::GridController, SheetPos, SheetRect}; +use crate::{ + cellvalue::Import, controller::GridController, grid::DataTableKind, CellValue, SheetPos, + SheetRect, +}; + +use anyhow::Result; impl GridController { pub fn flatten_data_table_operations( @@ -10,6 +15,27 @@ impl GridController { vec![Operation::FlattenDataTable { sheet_pos }] } + pub fn code_data_table_to_data_table_operations( + &self, + sheet_pos: SheetPos, + _cursor: Option, + ) -> Result> { + let import = Import::new("".into()); + let kind = DataTableKind::Import(import.to_owned()); + let name = self.grid.next_data_table_name(); + // let cell_value = CellValue::Import(import); + + Ok(vec![ + Operation::SwitchDataTableKind { sheet_pos, kind }, + Operation::UpdateDataTableName { sheet_pos, name }, + // TODO(ddimaria): add this back in + // Operation::SetCellValues { + // sheet_pos, + // values: cell_value.into(), + // }, + ]) + } + pub fn grid_to_data_table_operations( &self, sheet_rect: SheetRect, diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index cbc06557c3..1f9afe2fa9 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -467,7 +467,6 @@ mod test { ]; let import = Import::new(file_name.into()); let cell_value = CellValue::Import(import.clone()); - let sheet = gc.try_sheet(sheet_id).unwrap(); let mut expected_data_table = DataTable::from((import, values.into(), &gc.grid)); assert_display_cell_value(&gc, sheet_id, 0, 0, &cell_value.to_string()); diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 06a59403c3..9430926a3a 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -10,7 +10,7 @@ use crate::{ formatting::CellFmtArray, js_types::JsRowHeight, sheet::{borders::BorderStyleCellUpdates, validations::validation::Validation}, - DataTable, Sheet, SheetBorders, SheetId, + DataTable, DataTableKind, Sheet, SheetBorders, SheetId, }, selection::Selection, SheetPos, SheetRect, @@ -48,6 +48,10 @@ pub enum Operation { FlattenDataTable { sheet_pos: SheetPos, }, + SwitchDataTableKind { + sheet_pos: SheetPos, + kind: DataTableKind, + }, GridToDataTable { sheet_rect: SheetRect, }, @@ -224,6 +228,13 @@ impl fmt::Display for Operation { Operation::FlattenDataTable { sheet_pos } => { write!(fmt, "FlattenDataTable {{ sheet_pos: {} }}", sheet_pos) } + Operation::SwitchDataTableKind { sheet_pos, kind } => { + write!( + fmt, + "SwitchDataTableKind {{ sheet_pos: {}, kind: {} }}", + sheet_pos, kind + ) + } Operation::GridToDataTable { sheet_rect } => { write!(fmt, "GridToDataTable {{ sheet_rect: {} }}", sheet_rect) } diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index 1a11d1a824..1e4f17303f 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -30,6 +30,17 @@ impl GridController { self.start_user_transaction(ops, cursor, TransactionName::FlattenDataTable); } + pub fn code_data_table_to_data_table( + &mut self, + sheet_pos: SheetPos, + cursor: Option, + ) -> Result<()> { + let ops = self.code_data_table_to_data_table_operations(sheet_pos, cursor.to_owned())?; + self.start_user_transaction(ops, cursor, TransactionName::SwitchDataTableKind); + + Ok(()) + } + pub fn grid_to_data_table(&mut self, sheet_rect: SheetRect, cursor: Option) { let ops = self.grid_to_data_table_operations(sheet_rect, cursor.to_owned()); self.start_user_transaction(ops, cursor, TransactionName::GridToDataTable); diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 0f964b4ac9..ec224c2db7 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -4,7 +4,6 @@ //! any given CellValue::Code type (ie, if it doesn't exist then a run hasn't been //! performed yet). -use std::fmt::{Display, Formatter}; use std::num::NonZeroU32; use crate::cellvalue::Import; @@ -18,6 +17,7 @@ use chrono::{DateTime, Utc}; use itertools::Itertools; use regex::Regex; use serde::{Deserialize, Serialize}; +use strum_macros::{Display, EnumString}; use tabled::{ builder::Builder, settings::{Color, Modify, Style}, @@ -76,7 +76,7 @@ pub struct DataTableColumn { pub value_index: u32, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Display)] pub enum DataTableKind { CodeRun(CodeRun), Import(Import), @@ -92,24 +92,16 @@ impl DataTableColumn { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS, Display, EnumString)] pub enum SortDirection { + #[strum(serialize = "asc")] Ascending, + #[strum(serialize = "desc")] Descending, + #[strum(serialize = "none")] None, } -// TODO(ddimarai): implement strum and remove this impl -impl Display for SortDirection { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - SortDirection::Ascending => write!(f, "asc"), - SortDirection::Descending => write!(f, "desc"), - SortDirection::None => write!(f, "none"), - } - } -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub struct DataTableSort { pub column_index: usize, diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index 87f440bf73..29f1b8d04f 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -33,6 +33,21 @@ impl GridController { Ok(()) } + /// Converts a DataTableKind::CodeRun to DataTableKind::Import + #[wasm_bindgen(js_name = "codeDataTableToDataTable")] + pub fn js_code_data_table_to_data_table( + &mut self, + sheet_id: String, + pos: String, + cursor: Option, + ) -> Result<(), JsValue> { + let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; + let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; + self.code_data_table_to_data_table(pos.to_sheet_pos(sheet_id), cursor); + + Ok(()) + } + /// Sort a Data Table #[wasm_bindgen(js_name = "sortDataTable")] pub fn js_sort_data_table( @@ -50,7 +65,7 @@ impl GridController { Ok(()) } - /// Toggle applin the first row as head + /// Toggle appling the first row as head #[wasm_bindgen(js_name = "dataTableFirstRowAsHeader")] pub fn js_data_table_first_row_as_header( &mut self, @@ -69,7 +84,7 @@ impl GridController { Ok(()) } - /// Flattens a Data Table + /// Update a Data Table's name #[wasm_bindgen(js_name = "updateDataTableName")] pub fn js_update_data_table_name( &mut self, From 302c1610fb6ea78876b6155afc57c398dd5891d4 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 22 Oct 2024 04:52:19 -0700 Subject: [PATCH 101/373] improve table name context menu --- .../public/images/postgres-icon.svg | 12 +++++-- .../HTMLGrid/contextMenus/GridContextMenu.tsx | 15 ++++---- .../contextMenus/TableColumnContextMenu.tsx | 2 +- .../contextMenus/TableContextMenu.tsx | 2 +- .../HTMLGrid/contextMenus/TableMenu.tsx | 34 +++++++++++++++++-- .../src/app/gridGL/cells/CellsMarkers.ts | 8 ++--- .../src/app/gridGL/cells/tables/Table.ts | 21 ++++++------ .../gridGL/cells/tables/TableColumnHeader.ts | 3 +- .../src/app/gridGL/cells/tables/TableName.ts | 13 ++++++- .../src/app/gridGL/cells/tables/Tables.ts | 5 ++- quadratic-rust-shared/src/auto_gen_path.rs | 4 +-- 11 files changed, 84 insertions(+), 35 deletions(-) diff --git a/quadratic-client/public/images/postgres-icon.svg b/quadratic-client/public/images/postgres-icon.svg index 8eb1e22759..de32e6e848 100644 --- a/quadratic-client/public/images/postgres-icon.svg +++ b/quadratic-client/public/images/postgres-icon.svg @@ -1,3 +1,9 @@ - - - \ No newline at end of file + + + + + + + + + diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index c617d63d5c..b878b2b26f 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -7,6 +7,7 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { focusGrid } from '@/app/helpers/focusGrid'; +import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { ControlledMenu, MenuDivider, SubMenu } from '@szhsin/react-menu'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useRecoilState } from 'recoil'; @@ -36,15 +37,13 @@ export const GridContextMenu = () => { const [columnRowAvailable, setColumnRowAvailable] = useState(false); const [canConvertToDataTable, setCanConvertToDataTable] = useState(false); - const [isDataTable, setIsDataTable] = useState(false); - const [tableType, setTableType] = useState(''); + const [table, setTable] = useState(); useEffect(() => { const updateCursor = () => { setColumnRowAvailable(sheets.sheet.cursor.hasOneColumnRowSelection(true)); setCanConvertToDataTable(sheets.sheet.cursor.canConvertToDataTable()); const codeCell = pixiApp.cellSheet().cursorOnDataTable(); - setIsDataTable(!!codeCell); - setTableType(codeCell?.language === 'Import' ? 'Data' : 'Code'); + setTable(codeCell); }; updateCursor(); @@ -103,10 +102,10 @@ export const GridContextMenu = () => { {canConvertToDataTable && } {canConvertToDataTable && } - {isDataTable && } - {isDataTable && ( - - + {table && } + {table && ( + + )} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx index 22c0139465..82629c57e0 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -52,7 +52,7 @@ export const TableContextMenu = () => { menuStyle={{ padding: '0', color: 'inherit' }} menuClassName="bg-background" > - + ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 5064792e5e..ecfcf9b703 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -52,7 +52,7 @@ export const TableContextMenu = () => { menuStyle={{ padding: '0', color: 'inherit' }} menuClassName="bg-background" > - + ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index 1951813d0f..2efa5890c2 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -1,17 +1,45 @@ import { Action } from '@/app/actions/actions'; import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; -import { MenuDivider } from '@szhsin/react-menu'; +import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { MenuDivider, MenuHeader } from '@szhsin/react-menu'; +import { useMemo } from 'react'; interface Props { - isCodeTable: boolean; defaultRename?: boolean; + codeCell?: JsRenderCodeCell; } export const TableMenu = (props: Props) => { - const { isCodeTable, defaultRename } = props; + const { defaultRename, codeCell } = props; + + const isCodeTable = codeCell?.language === 'Import' ? 'Data' : 'Code'; + + const header = useMemo(() => { + if (!codeCell) { + return ''; + } + if (codeCell.language === 'Import') { + return 'Data Table'; + } else if (codeCell.language === 'Formula') { + return 'Formula Table'; + } else if (codeCell.language === 'Python') { + return 'Python Table'; + } else if (codeCell.language === 'Javascript') { + return 'JavaScript Table'; + } else if (typeof codeCell.language === 'object') { + return codeCell.language.Connection?.kind; + } else { + throw new Error(`Unknown language: ${codeCell.language}`); + } + }, [codeCell]); + + if (!codeCell) { + return null; + } return ( <> + {header} diff --git a/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts b/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts index bcb3f47c76..2fe3270747 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts @@ -35,23 +35,23 @@ export const getLanguageSymbol = (language: CodeCellLanguage, isError: boolean): } else if (typeof language === 'object') { switch (language.Connection?.kind) { case 'MSSQL': - symbol.texture = Texture.from('/images/mssql-icon.png'); + symbol.texture = Texture.from('/images/mssql-icon.svg'); symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languageMssql); return symbol; case 'POSTGRES': symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languagePostgres); - symbol.texture = Texture.from('/images/postgres-icon.png'); + symbol.texture = Texture.from('postgres-icon'); return symbol; case 'MYSQL': symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languageMysql); - symbol.texture = Texture.from('/images/mysql-icon.png'); + symbol.texture = Texture.from('/images/mysql-icon.svg'); return symbol; case 'SNOWFLAKE': symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languageSnowflake); - symbol.texture = Texture.from('/images/snowflake-icon.png'); + symbol.texture = Texture.from('/images/snowflake-icon.svg'); return symbol; default: diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 7829f7c65a..3c13d2a95a 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -95,15 +95,6 @@ export class Table extends Container { ); } - intersectsTableName(world: Point): TablePointerDownResult | undefined { - if (intersects.rectanglePoint(this.tableName.getScaled(), world)) { - if (world.x <= this.tableName.x + this.tableName.getScaledTextWidth()) { - return { table: this.codeCell, type: 'table-name' }; - } - return { table: this.codeCell, type: 'dropdown' }; - } - } - update(bounds: Rectangle, gridHeading: number) { this.visible = intersects.rectangleRectangle(this.tableBounds, bounds); this.headingPosition(bounds, gridHeading); @@ -173,6 +164,11 @@ export class Table extends Container { } pointerMove(world: Point): boolean { + const name = this.tableName.intersects(world); + if (name?.type === 'dropdown') { + this.tableCursor = 'pointer'; + return true; + } const result = this.columnHeaders.pointerMove(world); if (result) { this.tableCursor = this.columnHeaders.tableCursor; @@ -183,9 +179,14 @@ export class Table extends Container { } pointerDown(world: Point): TablePointerDownResult | undefined { - if (intersects.rectanglePoint(this.tableName.getScaled(), world)) { + const result = this.tableName.intersects(world); + if (result?.type === 'table-name') { return { table: this.codeCell, type: 'table-name' }; } return this.columnHeaders.pointerDown(world); } + + intersectsTableName(world: Point): TablePointerDownResult | undefined { + return this.tableName.intersects(world); + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 6efab24ba4..984f728bdf 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -72,7 +72,7 @@ export class TableColumnHeader extends Container { } private drawSortButton(width: number, height: number, sort?: DataTableSort) { - this.sortButtonStart = this.columnHeaderBounds.x + width - SORT_BUTTON_RADIUS * 2 - SORT_BUTTON_PADDING; + this.sortButtonStart = this.columnHeaderBounds.x + this.columnName.width + this.columnName.x; this.sortButton = this.addChild(new Graphics()); this.sortButton.beginFill(0, SORT_BACKGROUND_ALPHA); this.sortButton.drawCircle(0, 0, SORT_BUTTON_RADIUS); @@ -113,6 +113,7 @@ export class TableColumnHeader extends Container { this.tableCursor = undefined; pixiApp.setViewportDirty(); } + this.tableCursor = undefined; return false; } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts index 05fd987a31..fe02c2406f 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts @@ -2,10 +2,12 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { DROPDOWN_SIZE } from '@/app/gridGL/cells/cellsLabel/drawSpecial'; import { getLanguageSymbol } from '@/app/gridGL/cells/CellsMarkers'; import { Table } from '@/app/gridGL/cells/tables/Table'; +import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; +import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; -import { BitmapText, Container, Graphics, Rectangle, Sprite, Texture } from 'pixi.js'; +import { BitmapText, Container, Graphics, Point, Rectangle, Sprite, Texture } from 'pixi.js'; export const TABLE_NAME_FONT_SIZE = 12; export const TABLE_NAME_PADDING = [4, 2]; @@ -125,4 +127,13 @@ export class TableName extends Container { getScaledTextWidth() { return this.text.width / pixiApp.viewport.scaled; } + + intersects(world: Point): TablePointerDownResult | undefined { + if (intersects.rectanglePoint(this.getScaled(), world)) { + if (world.x <= this.x + this.text.x + this.getScaledTextWidth()) { + return { table: this.table.codeCell, type: 'table-name' }; + } + return { table: this.table.codeCell, type: 'dropdown' }; + } + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 5413e0402f..93eb90e785 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -170,6 +170,7 @@ export class Tables extends Container
{ return true; } } + this.tableCursor = undefined; return false; } @@ -190,7 +191,9 @@ export class Tables extends Container
{ if (this.contextMenuTable) { // we keep the former context menu table active after the context // menu closes until the cursor moves again. - this.hoverTable = this.contextMenuTable; + if (this.contextMenuTable !== this.activeTable) { + this.hoverTable = this.contextMenuTable; + } this.contextMenuTable = undefined; } if (!options?.type) { diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index a0dea21ae2..8df3d2b4d5 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From d06922a30d5061a49c33e1c04dcbc3f0cade85f5 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 22 Oct 2024 05:03:02 -0700 Subject: [PATCH 102/373] keep table name active after rename --- quadratic-client/src/app/gridGL/cells/tables/Tables.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 93eb90e785..68c74a901e 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -184,7 +184,7 @@ export class Tables extends Container
{ this.renameDataTable.showTableName(); this.renameDataTable.showColumnHeaders(); if (this.activeTable !== this.renameDataTable) { - this.renameDataTable.hideActive(); + this.hoverTable = this.renameDataTable; } this.renameDataTable = undefined; } From b279bd851b6a951416c29e6d7714efbf5f7f22c5 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 22 Oct 2024 05:34:52 -0700 Subject: [PATCH 103/373] sort table --- .../src/app/actions/dataTableSpec.ts | 5 ++-- .../src/app/atoms/contextMenuAtom.ts | 4 +-- .../app/gridGL/HTMLGrid/HTMLGridContainer.tsx | 2 ++ .../HTMLGrid/contextMenus/TableSort.tsx | 30 +++++++++++++++++++ .../gridGL/cells/tables/TableColumnHeader.ts | 2 +- .../src/app/gridGL/cells/tables/TableName.ts | 4 +-- 6 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 6a8f17f85d..ec3ba9ee5b 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -128,10 +128,11 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.SortTable]: { - label: 'Sort table (coming soon)', + label: 'Sort table', Icon: SortIcon, run: async () => { - // open table sort dialog... + const table = getTable(); + pixiAppSettings.setContextMenu?.({ type: ContextMenuType.TableSort, table }); }, }, [Action.ToggleTableAlternatingColors]: { diff --git a/quadratic-client/src/app/atoms/contextMenuAtom.ts b/quadratic-client/src/app/atoms/contextMenuAtom.ts index dead4d62cd..c38f723f92 100644 --- a/quadratic-client/src/app/atoms/contextMenuAtom.ts +++ b/quadratic-client/src/app/atoms/contextMenuAtom.ts @@ -6,6 +6,7 @@ import { atom } from 'recoil'; export enum ContextMenuType { Grid = 'grid', Table = 'table', + TableSort = 'table-sort', } export interface ContextMenuState { @@ -15,8 +16,7 @@ export interface ContextMenuState { row?: number; table?: JsRenderCodeCell; - // special states we need to track - // rename is for tables + // special states we need to track rename for tables rename?: boolean; selectedColumn?: number; } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index bc4da503da..fc68f0f5df 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -18,6 +18,7 @@ import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Following } from '@/app/ui/components/Following'; import { ReactNode, useCallback, useEffect, useState } from 'react'; import { SuggestionDropDown } from './SuggestionDropdown'; +import { TableSort } from '@/app/gridGL/HTMLGrid/contextMenus/TableSort'; interface Props { parent?: HTMLDivElement; @@ -137,6 +138,7 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { + ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx new file mode 100644 index 0000000000..7ddac26997 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx @@ -0,0 +1,30 @@ +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/shadcn/ui/dialog'; +import { useCallback } from 'react'; +import { useRecoilState } from 'recoil'; + +export const TableSort = () => { + const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); + + const onClose = useCallback( + (open: boolean) => { + debugger; + if (!open) { + setContextMenu({}); + } + }, + [setContextMenu] + ); + + const open = contextMenu?.type === ContextMenuType.TableSort; + console.log(contextMenu?.type); + return ( + + + + Sort Table + + + + ); +}; diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 984f728bdf..29ae20f7ea 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -72,7 +72,7 @@ export class TableColumnHeader extends Container { } private drawSortButton(width: number, height: number, sort?: DataTableSort) { - this.sortButtonStart = this.columnHeaderBounds.x + this.columnName.width + this.columnName.x; + this.sortButtonStart = this.columnHeaderBounds.right - SORT_BUTTON_RADIUS - SORT_BUTTON_PADDING; this.sortButton = this.addChild(new Graphics()); this.sortButton.beginFill(0, SORT_BACKGROUND_ALPHA); this.sortButton.drawCircle(0, 0, SORT_BUTTON_RADIUS); diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts index fe02c2406f..fde2ce5228 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts @@ -125,12 +125,12 @@ export class TableName extends Container { // Returns the width of the table name text scaled to the viewport. getScaledTextWidth() { - return this.text.width / pixiApp.viewport.scaled; + return (this.tableNameBounds.width - this.dropdown.width - DROPDOWN_PADDING) / pixiApp.viewport.scaled; } intersects(world: Point): TablePointerDownResult | undefined { if (intersects.rectanglePoint(this.getScaled(), world)) { - if (world.x <= this.x + this.text.x + this.getScaledTextWidth()) { + if (world.x <= this.x + this.getScaledTextWidth()) { return { table: this.table.codeCell, type: 'table-name' }; } return { table: this.table.codeCell, type: 'dropdown' }; From 6090faad75bd550c6f692ce7ee79361df24a22df Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 22 Oct 2024 05:41:47 -0700 Subject: [PATCH 104/373] fix blur for PixiRename --- .../HTMLGrid/contextMenus/PixiRename.tsx | 24 +++++++++++++++++-- .../HTMLGrid/contextMenus/TableSort.tsx | 3 +-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx index 92d2bde315..8f79d2d9a2 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx @@ -34,9 +34,17 @@ export const PixiRename = (props: Props) => { focusGrid(); }, [onClose]); + // Validates the input value. + const validate = (value: string): boolean => { + if (value.trim().length === 0) { + return false; + } + return true; + }; + const saveAndClose = useCallback(() => { if (closed.current === true) return; - if (ref.current?.value !== defaultValue) { + if (ref.current?.value !== defaultValue && validate(ref.current?.value ?? '')) { onSave(ref.current?.value ?? ''); } onClose(); @@ -97,6 +105,19 @@ export const PixiRename = (props: Props) => { } }, [className]); + // Need to catch the Blur event via useEffect since the Input goes away when + // the context menu closes (eg, via a click outside the Input) + useEffect(() => { + const input = ref.current; + return () => { + if (!closed.current && input) { + if (input.value !== defaultValue && validate(input.value)) { + onSave(input.value); + } + } + }; + }, [onSave, defaultValue]); + if (!position) return null; return ( @@ -112,7 +133,6 @@ export const PixiRename = (props: Props) => { ...styles, }} onKeyDown={onKeyDown} - onBlur={saveAndClose} onChange={onChange} defaultValue={defaultValue} /> diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx index 7ddac26997..49e66c158e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx @@ -8,7 +8,6 @@ export const TableSort = () => { const onClose = useCallback( (open: boolean) => { - debugger; if (!open) { setContextMenu({}); } @@ -17,7 +16,7 @@ export const TableSort = () => { ); const open = contextMenu?.type === ContextMenuType.TableSort; - console.log(contextMenu?.type); + return ( From d4aca3cff03ab15788013a3ff589882dcb763d30 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 22 Oct 2024 14:25:08 -0600 Subject: [PATCH 105/373] Test sheet name change and code to data table, fix bugs --- .../execute_operation/execute_data_table.rs | 114 +++++++++++++++++- .../src/controller/operations/data_table.rs | 13 +- .../src/controller/user_actions/data_table.rs | 73 ++++++++++- quadratic-core/src/test_util.rs | 24 +++- .../wasm_bindings/controller/data_table.rs | 3 +- quadratic-rust-shared/src/auto_gen_path.rs | 4 +- 6 files changed, 212 insertions(+), 19 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 9aeba5cfc5..7c2365d291 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -78,7 +78,9 @@ impl GridController { if rect.contains(*pos) { // only delete when there's not another code cell in the same position (this maintains the original output until a run completes) if let Some(value) = sheet.cell_value(*pos) { - if matches!(value, CellValue::Code(_)) { + if matches!(value, CellValue::Code(_)) + || matches!(value, CellValue::Import(_)) + { None } else { Some(*pos) @@ -229,7 +231,7 @@ impl GridController { let data_table_pos = sheet.first_data_table_within(pos)?; let data_table = sheet.data_table_mut(data_table_pos)?; let old_data_table_kind = data_table.kind.to_owned(); - let old_data_table_name = data_table.name.to_owned(); + // let old_data_table_name = data_table.name.to_owned(); let sheet_rect = data_table.output_sheet_rect(sheet_pos, false); data_table.kind = match old_data_table_kind { @@ -468,6 +470,8 @@ impl GridController { #[cfg(test)] mod tests { + use std::collections::HashSet; + use crate::{ controller::{ execution::execute_operation::{ @@ -475,12 +479,12 @@ mod tests { }, user_actions::import::tests::{assert_simple_csv, simple_csv}, }, - grid::SheetId, + grid::{CodeCellLanguage, CodeRun, SheetId}, test_util::{ assert_cell_value_row, assert_data_table_cell_value, assert_data_table_cell_value_row, print_data_table, print_table, }, - SheetPos, + Array, CodeCellValue, SheetPos, Value, }; use super::*; @@ -600,6 +604,73 @@ mod tests { assert_flattened_simple_csv(&gc, sheet_id, pos, file_name); } + #[test] + fn test_execute_code_data_table_to_data_table() { + let code_run = CodeRun { + std_err: None, + std_out: None, + error: None, + return_type: Some("number".into()), + line_number: None, + output_type: None, + cells_accessed: HashSet::new(), + formatted_code_string: None, + }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run.clone()), + "Table 1", + Value::Array(Array::from(vec![vec!["1", "2", "3"]])), + false, + false, + true, + ); + + let mut gc = GridController::test(); + let sheet_id = gc.grid.sheets()[0].id; + let pos = Pos { x: 0, y: 0 }; + let sheet = gc.sheet_mut(sheet_id); + sheet.data_tables.insert_full(pos, data_table); + let code_cell_value = CodeCellValue { + language: CodeCellLanguage::Javascript, + code: "return [1,2,3]".into(), + }; + sheet.set_cell_value(pos, CellValue::Code(code_cell_value.clone())); + let data_table_pos = sheet.first_data_table_within(pos).unwrap(); + let sheet_pos = SheetPos::from((pos, sheet_id)); + let expected = vec!["1", "2", "3"]; + + // initial value + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, expected.clone()); + let data_table = &gc.sheet(sheet_id).data_table(data_table_pos).unwrap(); + assert_eq!(data_table.kind, DataTableKind::CodeRun(code_run.clone())); + + let import = Import::new("".into()); + let kind = DataTableKind::Import(import.to_owned()); + let op = Operation::SwitchDataTableKind { + sheet_pos, + kind: kind.clone(), + }; + let mut transaction = PendingTransaction::default(); + gc.execute_code_data_table_to_data_table(&mut transaction, op) + .unwrap(); + + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, expected.clone()); + let data_table = &gc.sheet(sheet_id).data_table(data_table_pos).unwrap(); + assert_eq!(data_table.kind, kind); + + // undo, the value should be a code run data table again + execute_reverse_operations(&mut gc, &transaction); + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, expected.clone()); + let data_table = &gc.sheet(sheet_id).data_table(data_table_pos).unwrap(); + assert_eq!(data_table.kind, DataTableKind::CodeRun(code_run)); + + // redo, the value should be a data table + execute_forward_operations(&mut gc, &mut transaction); + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, expected.clone()); + let data_table = &gc.sheet(sheet_id).data_table(data_table_pos).unwrap(); + assert_eq!(data_table.kind, kind); + } + #[test] fn test_execute_grid_to_data_table() { let (mut gc, sheet_id, pos, file_name) = simple_csv(); @@ -655,4 +726,39 @@ mod tests { print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); assert_sorted_data_table(&gc, sheet_id, pos, "simple.csv"); } + + #[test] + fn test_execute_update_data_table_name() { + let (mut gc, sheet_id, pos, _) = simple_csv(); + let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); + let updated_name = "My Table"; + + assert_eq!(&data_table.name, "simple.csv"); + println!("Initial data table name: {}", &data_table.name); + + let sheet_pos = SheetPos::from((pos, sheet_id)); + let op = Operation::UpdateDataTableName { + sheet_pos, + name: updated_name.into(), + }; + let mut transaction = PendingTransaction::default(); + gc.execute_update_data_table_name(&mut transaction, op) + .unwrap(); + + let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); + assert_eq!(&data_table.name, updated_name); + println!("Updated data table name: {}", &data_table.name); + + // undo, the value should be the initial name + execute_reverse_operations(&mut gc, &transaction); + let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); + assert_eq!(&data_table.name, "simple.csv"); + println!("Initial data table name: {}", &data_table.name); + + // redo, the value should be the updated name + execute_forward_operations(&mut gc, &mut transaction); + let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); + assert_eq!(&data_table.name, updated_name); + println!("Updated data table name: {}", &data_table.name); + } } diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index f86cb3090a..154cfcbe8e 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -22,17 +22,14 @@ impl GridController { ) -> Result> { let import = Import::new("".into()); let kind = DataTableKind::Import(import.to_owned()); - let name = self.grid.next_data_table_name(); - // let cell_value = CellValue::Import(import); + let cell_value = CellValue::Import(import); Ok(vec![ Operation::SwitchDataTableKind { sheet_pos, kind }, - Operation::UpdateDataTableName { sheet_pos, name }, - // TODO(ddimaria): add this back in - // Operation::SetCellValues { - // sheet_pos, - // values: cell_value.into(), - // }, + Operation::SetCellValues { + sheet_pos, + values: cell_value.into(), + }, ]) } diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index 1e4f17303f..959978cefc 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -85,4 +85,75 @@ impl GridController { #[cfg(test)] #[serial_test::parallel] -mod tests {} +mod tests { + use std::collections::HashSet; + + use crate::{ + cellvalue::Import, + controller::GridController, + grid::{CodeCellLanguage, CodeRun, DataTable, DataTableKind}, + test_util::{assert_cell_value, assert_data_table_cell_value_row, print_data_table}, + Array, CellValue, CodeCellValue, Pos, Rect, SheetPos, Value, + }; + + #[test] + fn test_code_data_table_to_data_table() { + let code_run = CodeRun { + std_err: None, + std_out: None, + error: None, + return_type: Some("number".into()), + line_number: None, + output_type: None, + cells_accessed: HashSet::new(), + formatted_code_string: None, + }; + let data_table = DataTable::new( + DataTableKind::CodeRun(code_run), + "Table 1", + Value::Array(Array::from(vec![vec!["1", "2", "3"]])), + false, + false, + true, + ); + + let mut gc = GridController::test(); + let sheet_id = gc.grid.sheets()[0].id; + let pos = Pos { x: 0, y: 0 }; + let sheet = gc.sheet_mut(sheet_id); + sheet.data_tables.insert_full(pos, data_table); + let code_cell_value = CodeCellValue { + language: CodeCellLanguage::Javascript, + code: "return [1,2,3]".into(), + }; + sheet.set_cell_value(pos, CellValue::Code(code_cell_value.clone())); + let sheet_pos = SheetPos::from((pos, sheet_id)); + let expected = vec!["1", "2", "3"]; + let import = Import::new("".into()); + + // initial value + print_data_table(&gc, sheet_id, Rect::new(0, 0, 2, 0)); + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, expected.clone()); + assert_cell_value( + &gc, + sheet_id, + 0, + 0, + CellValue::Code(code_cell_value.clone()), + ); + + gc.code_data_table_to_data_table(sheet_pos, None).unwrap(); + + print_data_table(&gc, sheet_id, Rect::new(0, 0, 2, 0)); + assert_data_table_cell_value_row(&gc, sheet_id, 0, 2, 0, expected.clone()); + assert_cell_value(&gc, sheet_id, 0, 0, CellValue::Import(import.clone())); + + // undo, the value should be a code run data table again + gc.undo(None); + assert_cell_value(&gc, sheet_id, 0, 0, CellValue::Code(code_cell_value)); + + // redo, the value should be a data table + gc.redo(None); + assert_cell_value(&gc, sheet_id, 0, 0, CellValue::Import(import)); + } +} diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index 2ece690fb7..6fb16989d1 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -11,6 +11,26 @@ use tabled::{ settings::{Modify, Style}, }; +/// Run an assertion that a cell value is equal to the given value +#[track_caller] +#[cfg(test)] +pub fn assert_cell_value( + grid_controller: &GridController, + sheet_id: SheetId, + x: i64, + y: i64, + value: CellValue, +) { + let sheet = grid_controller.sheet(sheet_id); + let cell_value = sheet.cell_value(Pos { x, y }).unwrap(); + + assert_eq!( + value, cell_value, + "Cell at ({}, {}) does not have the value {:?}, it's actually {:?}", + x, y, value, cell_value + ); +} + /// Run an assertion that a cell value is equal to the given value #[track_caller] #[cfg(test)] @@ -73,8 +93,6 @@ pub fn assert_data_table_cell_value( let data_table_pos = sheet.first_data_table_within(pos).unwrap(); let data_table = sheet.data_table_result(data_table_pos).unwrap(); - println!("Data table at {:?}", data_table); - if data_table.show_header && !data_table.header_is_first_row { pos.y += 1; } @@ -237,7 +255,7 @@ pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rec if let Some(sheet) = grid_controller.try_sheet(sheet_id) { let data_table = sheet.data_table(rect.min).unwrap(); - let max = rect.max.y - rect.min.y; + let max = rect.max.y - rect.min.y + 1; crate::grid::data_table::test::pretty_print_data_table( data_table, None, diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index 29f1b8d04f..a1aaf9bb22 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -43,7 +43,8 @@ impl GridController { ) -> Result<(), JsValue> { let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; - self.code_data_table_to_data_table(pos.to_sheet_pos(sheet_id), cursor); + self.code_data_table_to_data_table(pos.to_sheet_pos(sheet_id), cursor) + .map_err(|e| e.to_string())?; Ok(()) } diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index 8df3d2b4d5..a0dea21ae2 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From 66ecd41765d34c610801dc490f4f184b3e053ccb Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 22 Oct 2024 20:21:44 -0600 Subject: [PATCH 106/373] Unique sheet name on user edits --- .../execution/execute_operation/execute_data_table.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 7c2365d291..c4fcd36c99 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -332,13 +332,13 @@ impl GridController { } = op { let sheet_id = sheet_pos.sheet_id; + let name = self.grid.unique_data_table_name(name); let sheet = self.try_sheet_mut_result(sheet_id)?; let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; let data_table = sheet.data_table_mut(data_table_pos)?; let old_name = data_table.name.to_owned(); - data_table.name = name.to_owned(); - + data_table.name = name; let data_table_rect = data_table .output_rect(sheet_pos.into(), true) .to_sheet_rect(sheet_id); From d7f05c92c024a04a6dbbf00b9a4011d5af631227 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 22 Oct 2024 20:34:17 -0600 Subject: [PATCH 107/373] Test for unique data table names in operations --- .../execute_operation/execute_data_table.rs | 17 +++++++++++++---- quadratic-core/src/grid/data_table.rs | 5 +++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index c4fcd36c99..6ebaf2f2ab 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -742,7 +742,7 @@ mod tests { name: updated_name.into(), }; let mut transaction = PendingTransaction::default(); - gc.execute_update_data_table_name(&mut transaction, op) + gc.execute_update_data_table_name(&mut transaction, op.clone()) .unwrap(); let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); @@ -756,9 +756,18 @@ mod tests { println!("Initial data table name: {}", &data_table.name); // redo, the value should be the updated name - execute_forward_operations(&mut gc, &mut transaction); + { + execute_forward_operations(&mut gc, &mut transaction); + let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); + assert_eq!(&data_table.name, updated_name); + println!("Updated data table name: {}", &data_table.name); + } + + // ensure names are unique + let mut transaction = PendingTransaction::default(); + gc.execute_update_data_table_name(&mut transaction, op) + .unwrap(); let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); - assert_eq!(&data_table.name, updated_name); - println!("Updated data table name: {}", &data_table.name); + assert_eq!(&data_table.name, "My Table1") } } diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index ec224c2db7..ac04385d61 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -37,6 +37,11 @@ impl Grid { .flat_map(|sheet| sheet.data_tables.values().map(|table| &table.name)) .collect::>(); + // short circuit if the name is unique + if !all_names.contains(&&name.to_string()) { + return name.to_string(); + } + let mut num = 1; let mut name = String::from(""); From b4163cf79d0ac9d4594dc900ffaa5e9cbdd5741b Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 22 Oct 2024 20:50:51 -0600 Subject: [PATCH 108/373] Move unique_name to shared utils --- quadratic-core/src/grid/data_table.rs | 31 +++++---------------------- quadratic-core/src/util.rs | 27 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index ac04385d61..9a6ffd4976 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -9,13 +9,13 @@ use std::num::NonZeroU32; use crate::cellvalue::Import; use crate::grid::js_types::JsDataTableColumn; use crate::grid::CodeRun; +use crate::util::unique_name; use crate::{ Array, ArraySize, CellValue, Pos, Rect, RunError, RunErrorMsg, SheetPos, SheetRect, Value, }; use anyhow::{anyhow, Ok, Result}; use chrono::{DateTime, Utc}; use itertools::Itertools; -use regex::Regex; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; use tabled::{ @@ -28,34 +28,13 @@ use super::Grid; impl Grid { pub fn unique_data_table_name(&self, name: &str) -> String { - let re = Regex::new(r"\d+$").unwrap(); - let base = re.replace(&name, ""); - - let all_names = self + let all_names = &self .sheets() .iter() - .flat_map(|sheet| sheet.data_tables.values().map(|table| &table.name)) - .collect::>(); - - // short circuit if the name is unique - if !all_names.contains(&&name.to_string()) { - return name.to_string(); - } - - let mut num = 1; - let mut name = String::from(""); - - while name == "" { - let new_name = format!("{}{}", base, num); - - if !all_names.contains(&&new_name) { - name = new_name; - } - - num += 1; - } + .flat_map(|sheet| sheet.data_tables.values().map(|table| table.name.as_str())) + .collect_vec(); - name + return unique_name(name, all_names); } pub fn update_data_table_name(&mut self, sheet_pos: SheetPos, name: &str) -> Result<()> { diff --git a/quadratic-core/src/util.rs b/quadratic-core/src/util.rs index 70513f119e..366fd3fd09 100644 --- a/quadratic-core/src/util.rs +++ b/quadratic-core/src/util.rs @@ -3,6 +3,7 @@ use std::ops::Range; use chrono::Utc; use itertools::Itertools; +use regex::Regex; pub(crate) mod btreemap_serde { use std::collections::{BTreeMap, HashMap}; @@ -204,6 +205,32 @@ pub fn unused_name(prefix: &str, already_used: &[&str]) -> String { format!("{prefix} {i}") } +pub fn unique_name(name: &str, already_used: &[&str]) -> String { + let re = Regex::new(r"\d+$").expect("regex should compile"); + let base = re.replace(&name, ""); + + // short circuit if the name is unique + if !already_used.contains(&&name) { + return name.to_string(); + } + + // if not unique, try appending numbers until we find a unique name + let mut num = 1; + let mut name = String::from(""); + + while name == "" { + let new_name = format!("{}{}", base, num); + + if !already_used.contains(&new_name.as_str()) { + name = new_name; + } + + num += 1; + } + + name +} + pub fn maybe_reverse_range( range: Range, rev: bool, From b8816db5376e954e2aab9839f0a9b225b69569bb Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 23 Oct 2024 05:11:38 -0700 Subject: [PATCH 109/373] sort dialog WIP --- .../src/app/actions/dataTableSpec.ts | 23 ++-- .../app/gridGL/HTMLGrid/HTMLGridContainer.tsx | 2 +- .../HTMLGrid/contextMenus/GridContextMenu.tsx | 13 +- .../contextMenus/TableContextMenu.tsx | 2 +- .../HTMLGrid/contextMenus/TableSort.tsx | 29 ---- .../contextMenus/tableSort/TableSort.tsx | 127 ++++++++++++++++++ .../contextMenus/tableSort/TableSortEntry.tsx | 55 ++++++++ .../src/app/gridGL/cells/tables/Table.ts | 10 ++ .../gridGL/cells/tables/TableColumnHeader.ts | 1 + .../gridGL/cells/tables/TableColumnHeaders.ts | 40 +++--- .../src/app/gridGL/cells/tables/TableName.ts | 16 +-- .../src/app/gridGL/cells/tables/Tables.ts | 9 ++ .../interaction/pointer/PointerTable.ts | 6 +- .../src/app/gridGL/pixiApp/PixiApp.ts | 2 +- .../Validation/ValidationUI/ValidationUI.tsx | 5 +- .../src/shared/components/Icons.tsx | 4 + 16 files changed, 268 insertions(+), 76 deletions(-) delete mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index ec3ba9ee5b..1c937c6326 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -40,7 +40,7 @@ const isHeadingShowing = (): boolean => { }; const getTable = (): JsRenderCodeCell | undefined => { - return pixiAppSettings.contextMenu?.table ?? pixiApp.cellSheet().cursorOnDataTable(); + return pixiAppSettings.contextMenu?.table ?? pixiApp.cellsSheet().cursorOnDataTable(); }; const isAlternatingColorsShowing = (): boolean => { @@ -85,16 +85,14 @@ export const dataTableSpec: DataTableSpec = { label: 'Rename table', defaultOption: true, Icon: FileRenameIcon, - run: async () => { + run: () => { const table = getTable(); const contextMenu = pixiAppSettings.contextMenu; - console.log('contextMenu', contextMenu); if (contextMenu) { setTimeout(() => { const newContextMenu = { type: ContextMenuType.Table, rename: true, table }; - pixiAppSettings.setContextMenu?.(newContextMenu); events.emit('contextMenu', newContextMenu); - }, 0); + }); } }, }, @@ -109,7 +107,7 @@ export const dataTableSpec: DataTableSpec = { [Action.DeleteDataTable]: { label: 'Delete table', Icon: DeleteIcon, - run: async () => { + run: () => { const table = getTable(); if (table) { const selection = createSelection({ sheetId: sheets.sheet.id, rects: [new Rectangle(table.x, table.y, 1, 1)] }); @@ -120,7 +118,7 @@ export const dataTableSpec: DataTableSpec = { [Action.CodeToDataTable]: { label: 'Convert to data table', Icon: TableIcon, - run: async () => { + run: () => { const table = getTable(); if (table) { quadraticCore.codeDataTableToDataTable(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); @@ -130,15 +128,18 @@ export const dataTableSpec: DataTableSpec = { [Action.SortTable]: { label: 'Sort table', Icon: SortIcon, - run: async () => { - const table = getTable(); - pixiAppSettings.setContextMenu?.({ type: ContextMenuType.TableSort, table }); + run: () => { + setTimeout(() => { + const table = getTable(); + const contextMenu = { type: ContextMenuType.TableSort, table }; + events.emit('contextMenu', contextMenu); + }); }, }, [Action.ToggleTableAlternatingColors]: { label: 'Toggle alternating colors', checkbox: isAlternatingColorsShowing, - run: async () => { + run: () => { console.log('TODO: toggle alternating colors'); // quadraticCore.dataTableToggleAlternatingColors(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); }, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index fc68f0f5df..70e76ae7bd 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -18,7 +18,7 @@ import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Following } from '@/app/ui/components/Following'; import { ReactNode, useCallback, useEffect, useState } from 'react'; import { SuggestionDropDown } from './SuggestionDropdown'; -import { TableSort } from '@/app/gridGL/HTMLGrid/contextMenus/TableSort'; +import { TableSort } from '@/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort'; interface Props { parent?: HTMLDivElement; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index b878b2b26f..94e2094a2f 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -8,6 +8,7 @@ import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { focusGrid } from '@/app/helpers/focusGrid'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { TableIcon } from '@/shared/components/Icons'; import { ControlledMenu, MenuDivider, SubMenu } from '@szhsin/react-menu'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useRecoilState } from 'recoil'; @@ -42,7 +43,7 @@ export const GridContextMenu = () => { const updateCursor = () => { setColumnRowAvailable(sheets.sheet.cursor.hasOneColumnRowSelection(true)); setCanConvertToDataTable(sheets.sheet.cursor.canConvertToDataTable()); - const codeCell = pixiApp.cellSheet().cursorOnDataTable(); + const codeCell = pixiApp.cellsSheet().cursorOnDataTable(); setTable(codeCell); }; @@ -104,7 +105,15 @@ export const GridContextMenu = () => { {table && } {table && ( - + + +
{table?.language === 'Import' ? 'Data' : 'Code'} Table
+ + } + >
)} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index ecfcf9b703..e35107b1f4 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -13,7 +13,7 @@ export const TableContextMenu = () => { const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { - if (contextMenu.type === ContextMenuType.Table && contextMenu.rename) { + if (contextMenu.type !== ContextMenuType.Table || (contextMenu.type === ContextMenuType.Table && !contextMenu.rename)) { return; } setContextMenu({}); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx deleted file mode 100644 index 49e66c158e..0000000000 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableSort.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/shadcn/ui/dialog'; -import { useCallback } from 'react'; -import { useRecoilState } from 'recoil'; - -export const TableSort = () => { - const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); - - const onClose = useCallback( - (open: boolean) => { - if (!open) { - setContextMenu({}); - } - }, - [setContextMenu] - ); - - const open = contextMenu?.type === ContextMenuType.TableSort; - - return ( - - - - Sort Table - - - - ); -}; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx new file mode 100644 index 0000000000..80c017f7f3 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -0,0 +1,127 @@ +//! Shows the Table Sort Dialog for a table + +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { events } from '@/app/events/events'; +import { TableSortEntry } from '@/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { DataTableSort, SortDirection } from '@/app/quadratic-core-types'; +import { Button } from '@/shared/shadcn/ui/button'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useRecoilState } from 'recoil'; + +export const TableSort = () => { + const ref = useRef(null); + const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); + + const close = useCallback(() => { + setContextMenu({}); + }, [setContextMenu]); + + const [sort, setSort] = useState([]); + useEffect(() => { + if (contextMenu.table && contextMenu.table.sort) { + setSort([ + ...contextMenu.table.sort.filter((item) => item.direction !== 'None'), + { column_index: -1, direction: 'None' }, + ]); + } else { + setSort([{ column_index: -1, direction: 'None' }]); + } + }, [contextMenu.table]); + + useEffect(() => { + const changePosition = () => { + if (!ref.current) { + setTimeout(changePosition, 0); + return; + } + if (contextMenu.table) { + const position = pixiApp.cellsSheet().tables.getSortDialogPosition(contextMenu.table); + if (position) { + ref.current.style.left = `${position.x}px`; + ref.current.style.top = `${position.y}px`; + ref.current.style.display = 'block'; + } + } + }; + const viewportChanged = () => { + if (ref.current) { + ref.current.style.transform = `scale(${1 / pixiApp.viewport.scaled})`; + } + changePosition(); + }; + + changePosition(); + events.on('viewportChanged', viewportChanged); + return () => { + events.off('viewportChanged', viewportChanged); + }; + }, [contextMenu.table]); + + const columnNames = useMemo(() => contextMenu.table?.column_names ?? [], [contextMenu.table]); + const nonNoneSortItems = useMemo( + () => contextMenu.table?.sort?.filter((item) => item.direction !== 'None') ?? [], + [contextMenu.table?.sort] + ); + + const availableColumns = useMemo(() => { + const availableColumns = columnNames.filter( + (_, index) => !nonNoneSortItems.some((item) => item.column_index === index) + ); + return availableColumns.map((column) => column.name); + }, [columnNames, nonNoneSortItems]); + + const handleChange = (index: number, column: string, direction: SortDirection) => { + setSort((prev) => { + const columnIndex = column === '' ? -1 : columnNames.findIndex((c) => c.name === column); + const newSort = [...prev]; + newSort[index] = { column_index: columnIndex, direction }; + const last = newSort[newSort.length - 1]; + console.log(availableColumns); + if (last.column_index !== -1 && availableColumns.length > 1) { + newSort.push({ column_index: -1, direction: 'None' }); + } + return newSort; + }); + }; + + if (contextMenu.type !== ContextMenuType.TableSort) return null; + + return ( +
+
Table Sort
+
+ {sort.map((entry, index) => { + const name = entry.column_index !== -1 ? '' : contextMenu.table?.column_names[entry.column_index]?.name ?? ''; + const columns = name ? [name, ...availableColumns] : availableColumns; + return ( + + ); + })} +
+
+ + +
+
+ ); +}; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx new file mode 100644 index 0000000000..a8ab972bf1 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx @@ -0,0 +1,55 @@ +import { SortDirection } from '@/app/quadratic-core-types'; +import { ValidationDropdown } from '@/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI'; +import { DeleteIcon, DragIndicatorIcon } from '@/shared/components/Icons'; +import { Button } from '@/shared/shadcn/ui/button'; +import { cn } from '@/shared/shadcn/utils'; +import { useEffect, useState } from 'react'; + +interface Props { + index: number; + availableColumns: string[]; + name: string; + direction: SortDirection; + onChange: (index: number, column: string, direction: SortDirection) => void; + last?: boolean; +} + +export const TableSortEntry = (props: Props) => { + const { index, availableColumns, direction, name, onChange, last } = props; + + const [newColumn, setNewColumn] = useState(name); + const [newDirection, setNewDirection] = useState(direction); + useEffect(() => { + if (newColumn !== name && newDirection !== direction) { + onChange(index, newColumn, newDirection); + } + }, [direction, index, name, newColumn, newDirection, onChange]); + + return ( +
+ + + ↑ Ascending, value: 'Ascending' }, + { label: <>↓ Descending , value: 'Descending' }, + ]} + onChange={(direction) => setNewDirection(direction as SortDirection)} + includeBlank + /> + +
+ ); +}; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 3c13d2a95a..11769835e8 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -6,6 +6,7 @@ import { TableOutline } from '@/app/gridGL/cells/tables/TableOutline'; import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { Coordinate } from '@/app/gridGL/types/size'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { Container, Point, Rectangle } from 'pixi.js'; @@ -189,4 +190,13 @@ export class Table extends Container { intersectsTableName(world: Point): TablePointerDownResult | undefined { return this.tableName.intersects(world); } + + getSortDialogPosition(): Coordinate | undefined { + // we need to force the column headers to be updated first to avoid a + // flicker since the update normally happens on the tick instead of on the + // viewport event (caused by inconsistency between React and pixi's update + // loop) + this.update(pixiApp.viewport.getVisibleBounds(), pixiApp.headings.headingSize.height / pixiApp.viewport.scaled); + return this.columnHeaders.getSortDialogPosition(); + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 29ae20f7ea..23d6fb57dd 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -46,6 +46,7 @@ export class TableColumnHeader extends Container { this.table = table; this.index = index; this.onSortPressed = onSortPressed; + console.log(height); this.columnHeaderBounds = new Rectangle(table.tableBounds.x + x, table.tableBounds.y, width, height); this.position.set(x, 0); diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 6f02c81121..b8dff5997a 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -5,6 +5,7 @@ import { Table } from '@/app/gridGL/cells/tables/Table'; import { TableColumnHeader } from '@/app/gridGL/cells/tables/TableColumnHeader'; import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { Coordinate } from '@/app/gridGL/types/size'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { JsDataTableColumn, SortDirection } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; @@ -64,21 +65,6 @@ export class TableColumnHeaders extends Container { sheets.getCursorPosition() ); - // todo: once Rust is fixed, this should be the SortDirection enum - // todo: not sure if this is worthwhile - // let newOrderRust: SortDirection; - // switch (newOrder) { - // case 'asc': - // newOrderRust = 'Ascending'; - // break; - // case 'desc': - // newOrderRust = 'Descending'; - // break; - // case 'none': - // newOrderRust = 'None'; - // break; - // } - // // we optimistically update the Sort array while we wait for core to finish the sort // table.sort = table.sort // ? table.sort.map((s) => (s.column_index === column.valueIndex ? { ...s, direction: newOrderRust } : s)) @@ -127,7 +113,9 @@ export class TableColumnHeaders extends Container { } pointerMove(world: Point): boolean { - const found = this.columns.children.find((column) => column.pointerMove(world)); + const adjustedWorld = world.clone(); + adjustedWorld.y -= this.y; + const found = this.columns.children.find((column) => column.pointerMove(adjustedWorld)); if (!found) { this.tableCursor = undefined; } else { @@ -147,8 +135,11 @@ export class TableColumnHeaders extends Container { } pointerDown(world: Point): TablePointerDownResult | undefined { + // need to adjust the world position in the case of sticky headers + const adjustedWorld = world.clone(); + adjustedWorld.y -= this.y; for (const column of this.columns.children) { - const result = column.pointerDown(world); + const result = column.pointerDown(adjustedWorld); if (result) { return result; } @@ -159,7 +150,14 @@ export class TableColumnHeaders extends Container { if (index < 0 || index >= this.columns.children.length) { throw new Error('Invalid column header index in getColumnHeaderBounds'); } - return this.columns.children[index]?.columnHeaderBounds; + const bounds = this.columns.children[index]?.columnHeaderBounds; + if (!bounds) { + throw new Error('Column header bounds not found in getColumnHeaderBounds'); + } + // need to adjust the bounds in the case of sticky headers + const adjustedBounds = bounds.clone(); + adjustedBounds.y -= this.y; + return adjustedBounds; } // Hides a column header @@ -177,4 +175,10 @@ export class TableColumnHeaders extends Container { show() { this.columns.children.forEach((column) => (column.visible = true)); } + + getSortDialogPosition(): Coordinate | undefined { + if (this.columns.children.length === 0) return; + const firstColumn = this.columns.children[0]; + return { x: firstColumn.columnHeaderBounds.left, y: firstColumn.columnHeaderBounds.bottom + this.y }; + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts index fde2ce5228..49521d9a5d 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts @@ -7,12 +7,12 @@ import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; +import { CELL_HEIGHT } from '@/shared/constants/gridConstants'; import { BitmapText, Container, Graphics, Point, Rectangle, Sprite, Texture } from 'pixi.js'; export const TABLE_NAME_FONT_SIZE = 12; export const TABLE_NAME_PADDING = [4, 2]; -const TABLE_NAME_HEIGHT = 20; const DROPDOWN_PADDING = 10; const SYMBOL_SCALE = 0.5; const SYMBOL_PADDING = 5; @@ -29,7 +29,7 @@ export class TableName extends Container { constructor(table: Table) { super(); this.table = table; - this.tableNameBounds = new Rectangle(0, 0, 0, TABLE_NAME_HEIGHT); + this.tableNameBounds = new Rectangle(0, 0, 0, CELL_HEIGHT); this.background = this.addChild(new Graphics()); this.text = this.addChild(new BitmapText('', { fontSize: TABLE_NAME_FONT_SIZE, fontName: 'OpenSans-Bold' })); this.symbol = this.addChild(new Sprite()); @@ -54,7 +54,7 @@ export class TableName extends Container { (this.symbol ? SYMBOL_PADDING + this.symbol.width : 0); this.background.clear(); this.background.beginFill(getCSSVariableTint('primary')); - this.background.drawShape(new Rectangle(0, -TABLE_NAME_HEIGHT, width, TABLE_NAME_HEIGHT)); + this.background.drawShape(new Rectangle(0, -CELL_HEIGHT, width, CELL_HEIGHT)); this.background.endFill(); this.tableNameBounds.width = width; @@ -69,10 +69,10 @@ export class TableName extends Container { this.symbol = getLanguageSymbol(this.table.codeCell.language, false); if (this.symbol) { this.addChild(this.symbol); - this.symbol.width = TABLE_NAME_HEIGHT * SYMBOL_SCALE; + this.symbol.width = CELL_HEIGHT * SYMBOL_SCALE; this.symbol.scale.y = this.symbol.scale.x; this.symbol.anchor.set(0, 0.5); - this.symbol.y = -TABLE_NAME_HEIGHT / 2; + this.symbol.y = -CELL_HEIGHT / 2; this.symbol.x = SYMBOL_PADDING; if (this.table.codeCell.language === 'Formula' || this.table.codeCell.language === 'Python') { this.symbol.tint = 0xffffff; @@ -86,7 +86,7 @@ export class TableName extends Container { this.text.anchor.set(0, 0.5); this.text.position.set( TABLE_NAME_PADDING[0] + (this.symbol ? SYMBOL_PADDING + this.symbol.width : 0), - -TABLE_NAME_HEIGHT / 2 + OPEN_SANS_FIX.y + -CELL_HEIGHT / 2 + OPEN_SANS_FIX.y ); } @@ -97,7 +97,7 @@ export class TableName extends Container { DROPDOWN_PADDING + TABLE_NAME_PADDING[0] + (this.symbol ? SYMBOL_PADDING + this.symbol.width : 0), - -TABLE_NAME_HEIGHT / 2 + -CELL_HEIGHT / 2 ); } @@ -111,7 +111,7 @@ export class TableName extends Container { this.drawBackground(); this.tableNameBounds.x = this.table.tableBounds.x; - this.tableNameBounds.y = this.table.tableBounds.y - TABLE_NAME_HEIGHT; + this.tableNameBounds.y = this.table.tableBounds.y - CELL_HEIGHT; } // Returns the table name bounds scaled to the viewport. diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 68c74a901e..386bf1d95f 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -8,6 +8,7 @@ import { Sheet } from '@/app/grid/sheet/Sheet'; import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; import { Table } from '@/app/gridGL/cells/tables/Table'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { Coordinate } from '@/app/gridGL/types/size'; import { JsCodeCell, JsRenderCodeCell } from '@/app/quadratic-core-types'; import { Container, Point, Rectangle } from 'pixi.js'; @@ -250,4 +251,12 @@ export class Tables extends Container
{ cursorOnDataTable(): JsRenderCodeCell | undefined { return this.children.find((table) => table.isCursorOnDataTable())?.codeCell; } + + getSortDialogPosition(codeCell: JsRenderCodeCell): Coordinate | undefined { + const table = this.children.find((table) => table.codeCell === codeCell); + if (!table) { + return; + } + return table.getSortDialogPosition(); + } } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index 094f65e2f0..f23b9a2632 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -72,7 +72,7 @@ export class PointerTable { } pointerDown(world: Point, event: PointerEvent): boolean { - const tableDown = pixiApp.cellSheet().tables.pointerDown(world); + const tableDown = pixiApp.cellsSheet().tables.pointerDown(world); if (!tableDown?.table) return false; if (event.button === 2 || (isMac && event.button === 0 && event.ctrlKey)) { @@ -102,8 +102,8 @@ export class PointerTable { clearTimeout(this.doubleClickTimeout); this.doubleClickTimeout = undefined; } - const result = pixiApp.cellSheet().tables.pointerMove(world); - this.cursor = pixiApp.cellSheet().tables.tableCursor; + const result = pixiApp.cellsSheet().tables.pointerMove(world); + this.cursor = pixiApp.cellsSheet().tables.tableCursor; return result; } } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index 649978e541..f6f91aee72 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -375,7 +375,7 @@ export class PixiApp { } } - cellSheet(): CellsSheet { + cellsSheet(): CellsSheet { if (!this.cellsSheets.current) { throw new Error('cellSheet not found in pixiApp'); } diff --git a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI.tsx b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI.tsx index 8bc6cdc1bc..4de2f662b3 100644 --- a/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI.tsx +++ b/quadratic-client/src/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI.tsx @@ -114,18 +114,19 @@ interface DropdownProps { options: (string | { value: string; label: string | JSX.Element })[]; disabled?: boolean; readOnly?: boolean; + style?: React.CSSProperties; // first entry is blank includeBlank?: boolean; } export const ValidationDropdown = (props: DropdownProps) => { - const { label, value, className, onChange, options, disabled, readOnly, includeBlank } = props; + const { label, value, className, onChange, options, disabled, readOnly, includeBlank, style } = props; const optionsBlank = includeBlank ? [{ value: 'blank', label: '' }, ...options] : options; return ( -
+
{label &&
{label}
}
{ if (table) { if (!renderCodeCell) { table.hideTableName(); + pixiApp.cellsSheet().cellsFills.updateAlternatingColors(x, y); + this.removeChild(table); table.destroy(); } else { table.updateCodeCell(renderCodeCell); diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index a0dea21ae2..8df3d2b4d5 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From 0dea7dd734f4c99426f17f5166a49e1f069fceac Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 23 Oct 2024 07:28:31 -0700 Subject: [PATCH 113/373] working through bugs with sticky column headers --- .../src/app/gridGL/cells/tables/TableColumnHeaders.ts | 7 ++++--- .../gridGL/cells/tables/TableColumnHeadersGridLines.ts | 9 ++++++++- quadratic-client/src/app/gridGL/cells/tables/Tables.ts | 4 ++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 813f06520d..6726a0ed9d 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -185,11 +185,12 @@ export class TableColumnHeaders extends Container { getColumnHeaderLines(): { y0: number; y1: number; lines: number[] } { const lines: number[] = []; this.columns.children.forEach((column, index) => { - lines.push(this.table.x + column.columnHeaderBounds.left); + lines.push(column.columnHeaderBounds.left); if (index === this.columns.children.length - 1) { - lines.push(this.table.x + column.columnHeaderBounds.right); + lines.push(column.columnHeaderBounds.right); } }); - return { y0: this.y, y1: this.y + this.headerHeight, lines }; + console.log(lines); + return { y0: 0, y1: this.headerHeight, lines }; } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts index 4251d3ad7e..82ef7d4e83 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts @@ -3,6 +3,7 @@ import { Table } from '@/app/gridGL/cells/tables/Table'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { Graphics } from 'pixi.js'; export class TableColumnHeadersGridLines extends Graphics { @@ -20,7 +21,13 @@ export class TableColumnHeadersGridLines extends Graphics { const currentLineStyle = pixiApp.gridLines.currentLineStyle; if (!currentLineStyle) return; - this.lineStyle(pixiApp.gridLines.currentLineStyle); + if (pixiApp.cellsSheet().tables.isActive(this.table)) { + console.log('active?'); + this.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); + } else { + this.lineStyle(currentLineStyle); + } + lines.forEach((line) => { this.moveTo(line, y0).lineTo(line, y1); }); diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 9a599c1fc5..519abd4f99 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -261,4 +261,8 @@ export class Tables extends Container
{ } return table.getSortDialogPosition(); } + + isActive(table: Table): boolean { + return this.activeTable === table || this.hoverTable === table || this.contextMenuTable === table; + } } From 0539fd1f91f53ef945a3220db0c0b0e201d99e44 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 23 Oct 2024 08:55:24 -0600 Subject: [PATCH 114/373] Search for spaces between unique names and numbers + require number optional --- .../execute_operation/execute_data_table.rs | 2 +- quadratic-core/src/grid/data_table.rs | 25 ++++++++++++++----- quadratic-core/src/util.rs | 11 +++++--- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 6ebaf2f2ab..18eb6db39b 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -332,7 +332,7 @@ impl GridController { } = op { let sheet_id = sheet_pos.sheet_id; - let name = self.grid.unique_data_table_name(name); + let name = self.grid.unique_data_table_name(name, false); let sheet = self.try_sheet_mut_result(sheet_id)?; let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; let data_table = sheet.data_table_mut(data_table_pos)?; diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 9a6ffd4976..c02155b74c 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -27,18 +27,23 @@ use ts_rs::TS; use super::Grid; impl Grid { - pub fn unique_data_table_name(&self, name: &str) -> String { + pub fn unique_data_table_name(&self, name: &str, require_number: bool) -> String { let all_names = &self .sheets() .iter() .flat_map(|sheet| sheet.data_tables.values().map(|table| table.name.as_str())) .collect_vec(); - return unique_name(name, all_names); + return unique_name(name, all_names, require_number); } - pub fn update_data_table_name(&mut self, sheet_pos: SheetPos, name: &str) -> Result<()> { - let unique_name = self.unique_data_table_name(name); + pub fn update_data_table_name( + &mut self, + sheet_pos: SheetPos, + name: &str, + require_number: bool, + ) -> Result<()> { + let unique_name = self.unique_data_table_name(name, require_number); let sheet = self .try_sheet_mut(sheet_pos.sheet_id) .ok_or_else(|| anyhow!("Sheet {} not found", sheet_pos.sheet_id))?; @@ -49,7 +54,7 @@ impl Grid { } pub fn next_data_table_name(&self) -> String { - self.unique_data_table_name("Table") + self.unique_data_table_name("Table", true) } } @@ -110,7 +115,7 @@ pub struct DataTable { impl From<(Import, Array, &Grid)> for DataTable { fn from((import, cell_values, grid): (Import, Array, &Grid)) -> Self { - let name = grid.unique_data_table_name(&import.file_name); + let name = grid.unique_data_table_name(&import.file_name, false); DataTable::new( DataTableKind::Import(import), @@ -268,6 +273,14 @@ impl DataTable { display: bool, ) -> anyhow::Result<()> { self.check_index(index, true)?; + // let all_names = &self + // .columns + // .as_ref() + // .unwrap() + // .iter() + // .map(|column| column.name.to_string().to_owned().as_str()) + // .collect_vec(); + // let name = unique_name(&name, all_names); self.columns .as_mut() diff --git a/quadratic-core/src/util.rs b/quadratic-core/src/util.rs index 366fd3fd09..3a74642d43 100644 --- a/quadratic-core/src/util.rs +++ b/quadratic-core/src/util.rs @@ -205,12 +205,15 @@ pub fn unused_name(prefix: &str, already_used: &[&str]) -> String { format!("{prefix} {i}") } -pub fn unique_name(name: &str, already_used: &[&str]) -> String { +/// Returns a unique name by appending numbers to the base name if the name is not unique. +/// Starts at 1, and checks if the name is unique, then 2, etc. +/// If `require_number` is true, the name will always have an appended number. +pub fn unique_name(name: &str, all_names: &[&str], require_number: bool) -> String { let re = Regex::new(r"\d+$").expect("regex should compile"); let base = re.replace(&name, ""); // short circuit if the name is unique - if !already_used.contains(&&name) { + if !all_names.contains(&&name) { return name.to_string(); } @@ -220,8 +223,10 @@ pub fn unique_name(name: &str, already_used: &[&str]) -> String { while name == "" { let new_name = format!("{}{}", base, num); + let new_name_alt = format!("{} {}", base, num); + let new_names = [new_name.as_str(), new_name_alt.as_str()]; - if !already_used.contains(&new_name.as_str()) { + if !all_names.iter().any(|item| new_names.contains(item)) { name = new_name; } From 7e6014a53c8a0808b8ef6b8e83d6fb56f49cca30 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 23 Oct 2024 09:04:16 -0700 Subject: [PATCH 115/373] column headers are above gridLines --- .../src/app/gridGL/cells/tables/Table.ts | 33 ++++++++++++------- .../gridGL/cells/tables/TableColumnHeader.ts | 4 +++ .../gridGL/cells/tables/TableColumnHeaders.ts | 20 ++++------- .../tables/TableColumnHeadersGridLines.ts | 21 ++++++++---- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index ed587af57d..595a01fd9c 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -22,7 +22,9 @@ export class Table extends Container { private tableName: TableName; private gridLines: TableColumnHeadersGridLines; - private inOverHeadings = false; + + // whether the column headers are in the overHeadings container + inOverHeadings = false; sheet: Sheet; tableBounds: Rectangle; @@ -79,24 +81,31 @@ export class Table extends Container { }; // places column headers back into the table (instead of the overHeadings container) - private addChildColumnHeaders() { + private columnHeadersHere() { this.columnHeaders.x = 0; this.columnHeaders.y = 0; - this.addChild(this.columnHeaders); + + // need to keep columnHeaders in the same position in the z-order + this.addChildAt(this.columnHeaders, 0); + + this.gridLines.visible = false; + this.inOverHeadings = false; + } + + private columnHeadersInOverHeadings(bounds: Rectangle, gridHeading: number) { + this.columnHeaders.x = this.tableBounds.x; + this.columnHeaders.y = this.tableBounds.y + bounds.top + gridHeading - this.tableBounds.top; + pixiApp.overHeadings.addChild(this.columnHeaders); + this.gridLines.visible = true; + this.inOverHeadings = true; } private headingPosition = (bounds: Rectangle, gridHeading: number) => { if (this.visible) { if (this.tableBounds.top < bounds.top + gridHeading) { - this.columnHeaders.x = this.tableBounds.x; - this.columnHeaders.y = this.tableBounds.y + bounds.top + gridHeading - this.tableBounds.top; - pixiApp.overHeadings.addChild(this.columnHeaders); - this.gridLines.visible = true; - this.inOverHeadings = true; + this.columnHeadersInOverHeadings(bounds, gridHeading); } else { - this.addChildColumnHeaders(); - this.gridLines.visible = false; - this.inOverHeadings = false; + this.columnHeadersHere(); } } }; @@ -123,7 +132,7 @@ export class Table extends Container { update(bounds: Rectangle, gridHeading: number) { this.visible = intersects.rectangleRectangle(this.tableBounds, bounds); if (!this.visible && this.columnHeaders.parent !== this) { - this.addChildColumnHeaders(); + this.columnHeadersHere(); } this.headingPosition(bounds, gridHeading); if (this.isShowingTableName()) { diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 29ae20f7ea..e0656ac1f8 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -26,6 +26,8 @@ export class TableColumnHeader extends Container { sortButton?: Graphics; columnHeaderBounds: Rectangle; + w: number; + h: number; private onSortPressed: Function; @@ -47,6 +49,8 @@ export class TableColumnHeader extends Container { this.index = index; this.onSortPressed = onSortPressed; this.columnHeaderBounds = new Rectangle(table.tableBounds.x + x, table.tableBounds.y, width, height); + this.w = width; + this.h = height; this.position.set(x, 0); const tint = getCSSVariableTint('table-column-header-foreground'); diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 6726a0ed9d..daf70aa19c 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -64,13 +64,6 @@ export class TableColumnHeaders extends Container { newOrder, sheets.getCursorPosition() ); - - // // we optimistically update the Sort array while we wait for core to finish the sort - // table.sort = table.sort - // ? table.sort.map((s) => (s.column_index === column.valueIndex ? { ...s, direction: newOrderRust } : s)) - // : null; - // this.createColumnHeaders(); - // pixiApp.setViewportDirty(); } private createColumnHeaders() { @@ -79,7 +72,6 @@ export class TableColumnHeaders extends Container { this.columns.visible = false; return; } - this.columns.visible = true; let x = 0; const codeCell = this.table.codeCell; codeCell.column_names.forEach((column, index) => { @@ -114,7 +106,8 @@ export class TableColumnHeaders extends Container { pointerMove(world: Point): boolean { const adjustedWorld = world.clone(); - adjustedWorld.y -= this.y; + // need to adjust the y position in the case of sticky headers + adjustedWorld.y -= this.y ? this.y - this.table.y : 0; const found = this.columns.children.find((column) => column.pointerMove(adjustedWorld)); if (!found) { this.tableCursor = undefined; @@ -135,9 +128,9 @@ export class TableColumnHeaders extends Container { } pointerDown(world: Point): TablePointerDownResult | undefined { - // need to adjust the world position in the case of sticky headers const adjustedWorld = world.clone(); - adjustedWorld.y -= this.y; + // need to adjust the y position in the case of sticky headers + adjustedWorld.y -= this.y ? this.y - this.table.y : 0; for (const column of this.columns.children) { const result = column.pointerDown(adjustedWorld); if (result) { @@ -185,12 +178,11 @@ export class TableColumnHeaders extends Container { getColumnHeaderLines(): { y0: number; y1: number; lines: number[] } { const lines: number[] = []; this.columns.children.forEach((column, index) => { - lines.push(column.columnHeaderBounds.left); + lines.push(column.x); if (index === this.columns.children.length - 1) { - lines.push(column.columnHeaderBounds.right); + lines.push(column.x + column.w); } }); - console.log(lines); return { y0: 0, y1: this.headerHeight, lines }; } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts index 82ef7d4e83..cb1035488a 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts @@ -21,16 +21,23 @@ export class TableColumnHeadersGridLines extends Graphics { const currentLineStyle = pixiApp.gridLines.currentLineStyle; if (!currentLineStyle) return; - if (pixiApp.cellsSheet().tables.isActive(this.table)) { - console.log('active?'); - this.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); - } else { - this.lineStyle(currentLineStyle); - } + lines.forEach((line, index) => { + if (pixiApp.cellsSheet().tables.isActive(this.table)) { + this.lineStyle({ + color: getCSSVariableTint('primary'), + width: 2, + alignment: index === lines.length - 1 ? 0 : 1, + }); + } else { + this.lineStyle(currentLineStyle); + } - lines.forEach((line) => { this.moveTo(line, y0).lineTo(line, y1); }); + + this.lineStyle(currentLineStyle); + this.moveTo(lines[0], y0).lineTo(lines[lines.length - 1], y0); + this.moveTo(lines[0], y1).lineTo(lines[lines.length - 1], y1); } } } From 1a8728b9cbda9e80b9feda11b2af0c33be756d6b Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 23 Oct 2024 09:05:40 -0700 Subject: [PATCH 116/373] fix final bug with sticky column headers borders --- .../src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts index cb1035488a..9a55abe76b 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts @@ -22,7 +22,7 @@ export class TableColumnHeadersGridLines extends Graphics { if (!currentLineStyle) return; lines.forEach((line, index) => { - if (pixiApp.cellsSheet().tables.isActive(this.table)) { + if (pixiApp.cellsSheet().tables.isActive(this.table) && (index === 0 || index === lines.length - 1)) { this.lineStyle({ color: getCSSVariableTint('primary'), width: 2, From d71c3420a7cfa714c24beb9944d221c31a88b700 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 23 Oct 2024 09:20:59 -0700 Subject: [PATCH 117/373] tableName is handled before grid headings --- .../src/app/gridGL/cells/tables/Table.ts | 7 ++++-- .../src/app/gridGL/cells/tables/TableName.ts | 24 ++++++++++++------- .../app/gridGL/interaction/pointer/Pointer.ts | 6 ++--- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 595a01fd9c..bab781ee50 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -18,11 +18,11 @@ export class Table extends Container { // Both columnHeaders and tableName are either children of Table or, when they // are sticky, children of pixiApp.overHeadings. - private columnHeaders: TableColumnHeaders; private tableName: TableName; - private gridLines: TableColumnHeadersGridLines; + columnHeaders: TableColumnHeaders; + // whether the column headers are in the overHeadings container inOverHeadings = false; @@ -208,6 +208,9 @@ export class Table extends Container { if (name?.type === 'dropdown') { this.tableCursor = 'pointer'; return true; + } else if (name?.type === 'table-name') { + this.tableCursor = undefined; + return true; } const result = this.columnHeaders.pointerMove(world); if (result) { diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts index 49521d9a5d..8d30dacfd8 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts @@ -23,13 +23,11 @@ export class TableName extends Container { private symbol: Sprite | undefined; private text: BitmapText; private dropdown: Sprite; - - tableNameBounds: Rectangle; + private backgroundWidth = 0; constructor(table: Table) { super(); this.table = table; - this.tableNameBounds = new Rectangle(0, 0, 0, CELL_HEIGHT); this.background = this.addChild(new Graphics()); this.text = this.addChild(new BitmapText('', { fontSize: TABLE_NAME_FONT_SIZE, fontName: 'OpenSans-Bold' })); this.symbol = this.addChild(new Sprite()); @@ -57,7 +55,7 @@ export class TableName extends Container { this.background.drawShape(new Rectangle(0, -CELL_HEIGHT, width, CELL_HEIGHT)); this.background.endFill(); - this.tableNameBounds.width = width; + this.backgroundWidth = width; } private drawSymbol() { @@ -109,17 +107,27 @@ export class TableName extends Container { this.drawText(); this.drawDropdown(); this.drawBackground(); + } - this.tableNameBounds.x = this.table.tableBounds.x; - this.tableNameBounds.y = this.table.tableBounds.y - CELL_HEIGHT; + get tableNameBounds(): Rectangle { + const rect = new Rectangle(0, 0, this.backgroundWidth, CELL_HEIGHT); + if (this.table.inOverHeadings) { + rect.x = this.table.columnHeaders.x; + rect.y = this.table.columnHeaders.y - CELL_HEIGHT; + } else { + rect.x = this.table.tableBounds.x; + rect.y = this.table.tableBounds.y - CELL_HEIGHT; + } + return rect; } // Returns the table name bounds scaled to the viewport. getScaled() { - const scaled = this.tableNameBounds.clone(); + const scaled = this.tableNameBounds; + const originalHeight = scaled.height; scaled.width /= pixiApp.viewport.scaled; scaled.height /= pixiApp.viewport.scaled; - scaled.y -= scaled.height - this.tableNameBounds.height; + scaled.y -= scaled.height - originalHeight; return scaled; } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts index 8ede5029d6..c878e9f3e9 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts @@ -100,10 +100,10 @@ export class Pointer { this.pointerImages.pointerDown(world) || this.pointerCellMoving.pointerDown(event) || this.pointerHtmlCells.pointerDown(e) || + this.pointerTable.pointerDown(world, event) || this.pointerHeading.pointerDown(world, event) || this.pointerLink.pointerDown(world, event) || this.pointerAutoComplete.pointerDown(world) || - this.pointerTable.pointerDown(world, event) || this.pointerDown.pointerDown(world, event); this.updateCursor(); @@ -125,12 +125,12 @@ export class Pointer { this.pointerImages.pointerMove(world) || this.pointerCellMoving.pointerMove(event, world) || this.pointerHtmlCells.pointerMove(e) || + this.pointerTable.pointerMove(world) || this.pointerHeading.pointerMove(world) || this.pointerAutoComplete.pointerMove(world) || this.pointerDown.pointerMove(world, event) || this.pointerCursor.pointerMove(world, event) || - this.pointerLink.pointerMove(world, event) || - this.pointerTable.pointerMove(world); + this.pointerLink.pointerMove(world, event); this.updateCursor(); }; From a329895994233d29077b13d4e4ce36e53038a3cb Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 23 Oct 2024 13:01:20 -0600 Subject: [PATCH 118/373] Require number param --- quadratic-api/src/data/current_blank.grid | Bin 260 -> 260 bytes quadratic-core/src/util.rs | 12 +++++++++--- .../data/grid/current_blank.grid | Bin 260 -> 260 bytes quadratic-rust-shared/src/auto_gen_path.rs | 4 ++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/quadratic-api/src/data/current_blank.grid b/quadratic-api/src/data/current_blank.grid index 07f4cd25d80246245efef7f59e8ea432bfa9851d..ec70e7ff3ef378c142eb30a1bf6e174654ebe070 100644 GIT binary patch literal 260 zcmV+f0sH;~000000000nE;uT90aZ|2Zi6royh^p-P=$!3lxy@8A+%sSv1HiFHi=Y= zyLWAHr2JSj`se6Pfk3n2VO4$7V!>l}`V0Ha3F?8qNBeqFH#SvHwK7=;fBy`BP)c%`8AN z58&xHXBf$Qnlm#}`HZc;-4DVduZ)37OvJZkv@fLEJ|1C`k06VOZQJ-J4{05T<^xxs K4Q&7Ot&i}A9Cru+ literal 260 zcmV+f0sH;~000000000nE;uT90aZ}#ZiFBZe3fQ@YlE#C>udCbyt4X)mGL`wUoSVx{@`YOUuMZyD|o$ z0L^y+J$y%zw+aeaP@#cdA4(N~W)(z$GwR^B+0$tli(wqo?6K?UO=RVcPma$@T!jtI z&YhjOQduvT7F5S;F&7O!ug!=iBdq+TY-9!xt|aM8jAs5njr}J#TIyR`r%%l(H!%;% z+=IiPoMA@iaL-JM&1G!l?S2swd1drOVj{XNp>rYC&hZG7d<0qChY;u{4`D2YrUO^+ K9fUu-Z;v0}0CkW6 diff --git a/quadratic-core/src/util.rs b/quadratic-core/src/util.rs index 3a74642d43..4c7e29e589 100644 --- a/quadratic-core/src/util.rs +++ b/quadratic-core/src/util.rs @@ -3,8 +3,13 @@ use std::ops::Range; use chrono::Utc; use itertools::Itertools; +use lazy_static::lazy_static; use regex::Regex; +lazy_static! { + pub static ref MATCH_NUMBERS: Regex = Regex::new(r"\d+$").expect("regex should compile"); +} + pub(crate) mod btreemap_serde { use std::collections::{BTreeMap, HashMap}; @@ -209,11 +214,12 @@ pub fn unused_name(prefix: &str, already_used: &[&str]) -> String { /// Starts at 1, and checks if the name is unique, then 2, etc. /// If `require_number` is true, the name will always have an appended number. pub fn unique_name(name: &str, all_names: &[&str], require_number: bool) -> String { - let re = Regex::new(r"\d+$").expect("regex should compile"); - let base = re.replace(&name, ""); + let base = MATCH_NUMBERS.replace(&name, ""); + let contains_number = base != name; + let should_short_circuit = !(require_number && !contains_number); // short circuit if the name is unique - if !all_names.contains(&&name) { + if should_short_circuit && !all_names.contains(&&name) { return name.to_string(); } diff --git a/quadratic-rust-shared/data/grid/current_blank.grid b/quadratic-rust-shared/data/grid/current_blank.grid index 07f4cd25d80246245efef7f59e8ea432bfa9851d..ec70e7ff3ef378c142eb30a1bf6e174654ebe070 100644 GIT binary patch literal 260 zcmV+f0sH;~000000000nE;uT90aZ|2Zi6royh^p-P=$!3lxy@8A+%sSv1HiFHi=Y= zyLWAHr2JSj`se6Pfk3n2VO4$7V!>l}`V0Ha3F?8qNBeqFH#SvHwK7=;fBy`BP)c%`8AN z58&xHXBf$Qnlm#}`HZc;-4DVduZ)37OvJZkv@fLEJ|1C`k06VOZQJ-J4{05T<^xxs K4Q&7Ot&i}A9Cru+ literal 260 zcmV+f0sH;~000000000nE;uT90aZ}#ZiFBZe3fQ@YlE#C>udCbyt4X)mGL`wUoSVx{@`YOUuMZyD|o$ z0L^y+J$y%zw+aeaP@#cdA4(N~W)(z$GwR^B+0$tli(wqo?6K?UO=RVcPma$@T!jtI z&YhjOQduvT7F5S;F&7O!ug!=iBdq+TY-9!xt|aM8jAs5njr}J#TIyR`r%%l(H!%;% z+=IiPoMA@iaL-JM&1G!l?S2swd1drOVj{XNp>rYC&hZG7d<0qChY;u{4`D2YrUO^+ K9fUu-Z;v0}0CkW6 diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs index 8df3d2b4d5..a0dea21ae2 100644 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ b/quadratic-rust-shared/src/auto_gen_path.rs @@ -1,2 +1,2 @@ -const FIXTURES_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/davidfig/Programming/quadratic/quadratic-rust-shared/data"; \ No newline at end of file +const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; +const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From b87385528e89c9d17c5f5b28be401a7c59102a2e Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 23 Oct 2024 14:21:08 -0600 Subject: [PATCH 119/373] Make sorts in rust more generic --- .../execute_operation/execute_data_table.rs | 40 +++++-------------- .../src/controller/operations/data_table.rs | 15 +++---- .../src/controller/operations/operation.rs | 16 +++----- .../src/controller/user_actions/data_table.rs | 7 ++-- quadratic-core/src/grid/data_table.rs | 16 +++++--- .../wasm_bindings/controller/data_table.rs | 6 +-- 6 files changed, 38 insertions(+), 62 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 18eb6db39b..fd3e73bdbd 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -4,7 +4,7 @@ use crate::{ active_transactions::pending_transaction::PendingTransaction, operations::operation::Operation, GridController, }, - grid::{DataTable, DataTableKind, SortDirection}, + grid::{DataTable, DataTableKind}, ArraySize, CellValue, Pos, Rect, SheetRect, }; @@ -370,12 +370,7 @@ impl GridController { transaction: &mut PendingTransaction, op: Operation, ) -> Result<()> { - if let Operation::SortDataTable { - sheet_pos, - column_index, - sort_order, - } = op.to_owned() - { + if let Operation::SortDataTable { sheet_pos, sort } = op.to_owned() { let sheet_id = sheet_pos.sheet_id; let sheet = self.try_sheet_mut_result(sheet_id)?; let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; @@ -384,20 +379,8 @@ impl GridController { .output_rect(sheet_pos.into(), true) .to_sheet_rect(sheet_id); - // DSF: this would be better if we used the enum directly. TS will - // send it as a string (using the export_types definition) and it's - // easy to parse. Additionally, we probably don't need the "None" - // value as we should use Option so we can set the - // entire table's sort value to None if there are no remaining sort - // orders. - let sort_order_enum = match sort_order.as_str() { - "asc" => SortDirection::Ascending, - "desc" => SortDirection::Descending, - "none" => SortDirection::None, - _ => bail!("Invalid sort order"), - }; - - let old_value = data_table.sort_column(column_index as usize, sort_order_enum)?; + let old_value = data_table.sort.to_owned(); + data_table.sort = sort; self.send_to_wasm(transaction, &data_table_sheet_rect)?; transaction.add_code_cell(sheet_id, data_table_pos.into()); @@ -405,11 +388,7 @@ impl GridController { let forward_operations = vec![op]; let reverse_operations = vec![Operation::SortDataTable { sheet_pos, - column_index, - sort_order: old_value - .map(|v| v.direction) - .unwrap_or(SortDirection::None) - .to_string(), + sort: old_value, }]; self.data_table_operations( @@ -479,7 +458,7 @@ mod tests { }, user_actions::import::tests::{assert_simple_csv, simple_csv}, }, - grid::{CodeCellLanguage, CodeRun, SheetId}, + grid::{CodeCellLanguage, CodeRun, DataTableSort, SheetId, SortDirection}, test_util::{ assert_cell_value_row, assert_data_table_cell_value, assert_data_table_cell_value_row, print_data_table, print_table, @@ -705,10 +684,13 @@ mod tests { print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); let sheet_pos = SheetPos::from((pos, sheet_id)); + let sort = vec![DataTableSort { + column_index: 0, + direction: SortDirection::Ascending, + }]; let op = Operation::SortDataTable { sheet_pos, - column_index: 0, - sort_order: "asc".into(), + sort: Some(sort), }; let mut transaction = PendingTransaction::default(); gc.execute_sort_data_table(&mut transaction, op).unwrap(); diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index 154cfcbe8e..ff0377724b 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -1,7 +1,9 @@ use super::operation::Operation; use crate::{ - cellvalue::Import, controller::GridController, grid::DataTableKind, CellValue, SheetPos, - SheetRect, + cellvalue::Import, + controller::GridController, + grid::{DataTableKind, DataTableSort}, + CellValue, SheetPos, SheetRect, }; use anyhow::Result; @@ -53,15 +55,10 @@ impl GridController { pub fn sort_data_table_operations( &self, sheet_pos: SheetPos, - column_index: u32, - sort_order: String, + sort: Option>, _cursor: Option, ) -> Vec { - vec![Operation::SortDataTable { - sheet_pos, - column_index, - sort_order, - }] + vec![Operation::SortDataTable { sheet_pos, sort }] } pub fn data_table_first_row_as_header_operations( diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 9430926a3a..b82f300983 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -10,7 +10,7 @@ use crate::{ formatting::CellFmtArray, js_types::JsRowHeight, sheet::{borders::BorderStyleCellUpdates, validations::validation::Validation}, - DataTable, DataTableKind, Sheet, SheetBorders, SheetId, + DataTable, DataTableKind, DataTableSort, Sheet, SheetBorders, SheetId, }, selection::Selection, SheetPos, SheetRect, @@ -61,9 +61,7 @@ pub enum Operation { }, SortDataTable { sheet_pos: SheetPos, - column_index: u32, - // TODO(ddimarai): rename this to `direction` - sort_order: String, + sort: Option>, }, DataTableFirstRowAsHeader { sheet_pos: SheetPos, @@ -245,15 +243,11 @@ impl fmt::Display for Operation { sheet_pos, name ) } - Operation::SortDataTable { - sheet_pos, - column_index, - sort_order, - } => { + Operation::SortDataTable { sheet_pos, sort } => { write!( fmt, - "SortDataTable {{ sheet_pos: {}, column_index: {}, sort_order: {} }}", - sheet_pos, column_index, sort_order + "SortDataTable {{ sheet_pos: {}, sort: {:?} }}", + sheet_pos, sort ) } Operation::DataTableFirstRowAsHeader { diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index 959978cefc..288c36fdf9 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -1,5 +1,6 @@ use crate::{ controller::{active_transactions::transaction_name::TransactionName, GridController}, + grid::DataTableSort, Pos, SheetPos, SheetRect, }; @@ -59,12 +60,10 @@ impl GridController { pub fn sort_data_table( &mut self, sheet_pos: SheetPos, - column_index: u32, - sort_order: String, + sort: Option>, cursor: Option, ) { - let ops = - self.sort_data_table_operations(sheet_pos, column_index, sort_order, cursor.to_owned()); + let ops = self.sort_data_table_operations(sheet_pos, sort, cursor.to_owned()); self.start_user_transaction(ops, cursor, TransactionName::GridToDataTable); } diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index c02155b74c..d5edfcd4fc 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -17,7 +17,7 @@ use anyhow::{anyhow, Ok, Result}; use chrono::{DateTime, Utc}; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use strum_macros::{Display, EnumString}; +use strum_macros::Display; use tabled::{ builder::Builder, settings::{Color, Modify, Style}, @@ -81,13 +81,10 @@ impl DataTableColumn { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS, Display, EnumString)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub enum SortDirection { - #[strum(serialize = "asc")] Ascending, - #[strum(serialize = "desc")] Descending, - #[strum(serialize = "none")] None, } @@ -325,6 +322,13 @@ impl DataTable { direction: SortDirection, ) -> Result> { let old = self.prepend_sort(column_index, direction.clone()); + + self.sort_all()?; + + Ok(old) + } + + pub fn sort_all(&mut self) -> Result<()> { self.display_buffer = None; let value = self.display_value()?.into_array()?; let mut display_buffer = (0..value.height()).map(|i| i as u64).collect::>(); @@ -355,7 +359,7 @@ impl DataTable { self.display_buffer = Some(display_buffer); - Ok(old) + Ok(()) } pub fn prepend_sort( diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index a1aaf9bb22..0ed234b78e 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -55,13 +55,13 @@ impl GridController { &mut self, sheet_id: String, pos: String, - column_index: u32, - sort_order: String, + sort: Option, cursor: Option, ) -> Result<(), JsValue> { let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; - self.sort_data_table(pos.to_sheet_pos(sheet_id), column_index, sort_order, cursor); + let sort = sort.map(|s| serde_json::from_str::>(&s).unwrap_or_default()); + self.sort_data_table(pos.to_sheet_pos(sheet_id), sort, cursor); Ok(()) } From 6df7bba39228819c44f1a40f79e771bc4807e032 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 24 Oct 2024 04:16:12 -0700 Subject: [PATCH 120/373] cleaning up --- .../gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx | 1 - .../HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index cb6948610a..d74dd2230f 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -136,7 +136,6 @@ export const TableSort = () => { availableColumns={columns} onChange={handleChange} onDelete={handleDelete} - last={sort.length !== contextMenu.table?.column_names.length && index === sort.length - 1} /> ); })} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx index 95355697aa..ae7b1ed684 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx @@ -12,11 +12,10 @@ interface Props { direction: SortDirection; onChange: (index: number, column: string, direction: SortDirection) => void; onDelete: (index: number) => void; - last?: boolean; } export const TableSortEntry = (props: Props) => { - const { index, availableColumns, direction, name, onChange, onDelete, last } = props; + const { index, availableColumns, direction, name, onChange, onDelete } = props; const [newColumn, setNewColumn] = useState(name); const [newDirection, setNewDirection] = useState(direction); @@ -28,7 +27,7 @@ export const TableSortEntry = (props: Props) => { return (
- { onChange={(direction) => setNewDirection(direction as SortDirection)} includeBlank /> -
From 122e3a02c2e311865ef606511f266e5733c7c864 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 24 Oct 2024 04:42:04 -0700 Subject: [PATCH 121/373] reordering in sort dialog --- .../contextMenus/tableSort/TableSort.tsx | 11 +++++++++++ .../contextMenus/tableSort/TableSortEntry.tsx | 17 ++++++++++++----- .../src/shared/components/Icons.tsx | 8 ++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index d74dd2230f..3dfefcb404 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -110,6 +110,15 @@ export const TableSort = () => { }); }; + const handleReorder = (index: number, direction: 'up' | 'down') => { + setSort((prev) => { + const sort = [...prev]; + sort.splice(index, 1); + sort.splice(index + (direction === 'up' ? -1 : 1), 0, prev[index]); + return sort; + }); + }; + if (contextMenu.type !== ContextMenuType.TableSort) return null; return ( @@ -136,6 +145,8 @@ export const TableSort = () => { availableColumns={columns} onChange={handleChange} onDelete={handleDelete} + onReorder={handleReorder} + last={index === sort.length - 1 || (sort.length === 2 && sort[sort.length - 1].column_index === -1)} /> ); })} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx index ae7b1ed684..2d07b6d4e7 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx @@ -1,6 +1,6 @@ import { SortDirection } from '@/app/quadratic-core-types'; import { ValidationDropdown } from '@/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI'; -import { DeleteIcon, DragIndicatorIcon } from '@/shared/components/Icons'; +import { DeleteIcon, DownArrowIcon, UpArrowIcon } from '@/shared/components/Icons'; import { Button } from '@/shared/shadcn/ui/button'; import { cn } from '@/shared/shadcn/utils'; import { useEffect, useState } from 'react'; @@ -12,10 +12,12 @@ interface Props { direction: SortDirection; onChange: (index: number, column: string, direction: SortDirection) => void; onDelete: (index: number) => void; + onReorder: (index: number, direction: 'up' | 'down') => void; + last: boolean; } export const TableSortEntry = (props: Props) => { - const { index, availableColumns, direction, name, onChange, onDelete } = props; + const { index, availableColumns, direction, name, onChange, onDelete, onReorder, last } = props; const [newColumn, setNewColumn] = useState(name); const [newDirection, setNewDirection] = useState(direction); @@ -27,9 +29,6 @@ export const TableSortEntry = (props: Props) => { return (
- { onChange={(direction) => setNewDirection(direction as SortDirection)} includeBlank /> +
+ + +
diff --git a/quadratic-client/src/shared/components/Icons.tsx b/quadratic-client/src/shared/components/Icons.tsx index 461cf602c1..4030dab606 100644 --- a/quadratic-client/src/shared/components/Icons.tsx +++ b/quadratic-client/src/shared/components/Icons.tsx @@ -454,3 +454,11 @@ export const SortIcon: IconComponent = (props) => { export const DragIndicatorIcon: IconComponent = (props) => { return drag_indicator; }; + +export const UpArrowIcon: IconComponent = (props) => { + return arrow_upward; +}; + +export const DownArrowIcon: IconComponent = (props) => { + return arrow_downward; +}; From 8cd69791e8faa07fda2fcc51739ce889d1776f0d Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 24 Oct 2024 04:48:33 -0700 Subject: [PATCH 122/373] reorder sorting --- .../HTMLGrid/contextMenus/tableSort/TableSort.tsx | 10 +++++++++- .../HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx | 2 +- .../Validation/ValidationUI/ValidationUI.tsx | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index 3dfefcb404..ff6d085b66 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -1,6 +1,5 @@ //! Shows the Table Sort Dialog for a table -/* eslint-disable @typescript-eslint/no-unused-vars */ import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { TableSortEntry } from '@/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry'; @@ -130,6 +129,15 @@ export const TableSort = () => { transform: `scale(${1 / pixiApp.viewport.scaled})`, width: 400, }} + onKeyDown={(e) => { + if (e.key === 'Escape') { + handleClose(); + } else if (e.key === 'Enter') { + handleSave(); + } + e.stopPropagation(); + }} + autoFocus >
Table Sort
diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx index 2d07b6d4e7..293d21d8af 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx @@ -31,7 +31,7 @@ export const TableSortEntry = (props: Props) => {
{ - const { label, value, className, onChange, options, disabled, readOnly, includeBlank, style } = props; + const { label, value, className, onChange, options, disabled, readOnly, includeBlank, style, tabIndex } = props; const optionsBlank = includeBlank ? [{ value: 'blank', label: '' }, ...options] : options; @@ -135,6 +136,7 @@ export const ValidationDropdown = (props: DropdownProps) => { // this is needed to avoid selecting text when clicking the dropdown e.preventDefault(); }} + tabIndex={tabIndex} > From b48bedc1377c59f82d5f13d904eb996f5cc958f8 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 24 Oct 2024 05:49:22 -0700 Subject: [PATCH 123/373] clean up sorting dialog UI --- .../contextMenus/tableSort/TableSort.tsx | 39 +++++++----- .../contextMenus/tableSort/TableSortEntry.tsx | 62 ++++++++++++++----- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index ff6d085b66..acabbff460 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -22,11 +22,11 @@ export const TableSort = () => { if (contextMenu.table && contextMenu.table.sort) { const sort = [...contextMenu.table.sort.filter((item) => item.direction !== 'None')]; if (sort.length !== contextMenu.table.column_names.length) { - sort.push({ column_index: -1, direction: 'None' }); + sort.push({ column_index: -1, direction: 'Ascending' }); } setSort(sort); } else { - setSort([{ column_index: -1, direction: 'None' }]); + setSort([{ column_index: -1, direction: 'Ascending' }]); } }, [contextMenu.table]); @@ -72,24 +72,27 @@ export const TableSort = () => { }, [contextMenu.table]); const columnNames = useMemo(() => contextMenu.table?.column_names ?? [], [contextMenu.table]); - const nonNoneSortItems = useMemo(() => sort.filter((item) => item.direction !== 'None') ?? [], [sort]); const availableColumns = useMemo(() => { - const availableColumns = columnNames.filter( - (_, index) => !nonNoneSortItems.some((item) => item.column_index === index) - ); + const availableColumns = columnNames.filter((_, index) => !sort.some((item) => item.column_index === index)); return availableColumns.map((column) => column.name); - }, [columnNames, nonNoneSortItems]); + }, [columnNames, sort]); const handleChange = (index: number, column: string, direction: SortDirection) => { setSort((prev) => { - const columnIndex = column === '' ? -1 : columnNames.findIndex((c) => c.name === column); - const newSort = [...prev]; - newSort[index] = { column_index: columnIndex, direction }; - const last = newSort[newSort.length - 1]; - console.log(availableColumns); - if (last.column_index !== -1 && availableColumns.length > 1) { - newSort.push({ column_index: -1, direction: 'None' }); + const columnIndex = columnNames.findIndex((c) => c.name === column); + if (columnIndex === -1) return prev; + + // remove new entry from old sort + const newSort = [...prev.filter((value) => value.column_index !== -1)]; + + if (index === -1) { + newSort.push({ column_index: columnIndex, direction }); + } else { + newSort[index] = { column_index: columnIndex, direction }; + } + if (sort.length !== columnNames.length) { + newSort.push({ column_index: -1, direction: 'Ascending' }); } return newSort; }); @@ -103,7 +106,7 @@ export const TableSort = () => { sort.length && sort[sort.length - 1].column_index !== -1 ) { - sort.push({ column_index: -1, direction: 'None' }); + sort.push({ column_index: -1, direction: 'Ascending' }); } return sort; }); @@ -127,7 +130,7 @@ export const TableSort = () => { style={{ transformOrigin: 'top left', transform: `scale(${1 / pixiApp.viewport.scaled})`, - width: 400, + width: 450, }} onKeyDown={(e) => { if (e.key === 'Escape') { @@ -154,7 +157,9 @@ export const TableSort = () => { onChange={handleChange} onDelete={handleDelete} onReorder={handleReorder} - last={index === sort.length - 1 || (sort.length === 2 && sort[sort.length - 1].column_index === -1)} + last={ + index === sort.length - 1 || (sort[sort.length - 1].column_index === -1 && index === sort.length - 2) + } /> ); })} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx index 293d21d8af..f5e9e650de 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx @@ -3,7 +3,7 @@ import { ValidationDropdown } from '@/app/ui/menus/Validations/Validation/Valida import { DeleteIcon, DownArrowIcon, UpArrowIcon } from '@/shared/components/Icons'; import { Button } from '@/shared/shadcn/ui/button'; import { cn } from '@/shared/shadcn/utils'; -import { useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; interface Props { index: number; @@ -19,33 +19,65 @@ interface Props { export const TableSortEntry = (props: Props) => { const { index, availableColumns, direction, name, onChange, onDelete, onReorder, last } = props; - const [newColumn, setNewColumn] = useState(name); - const [newDirection, setNewDirection] = useState(direction); - useEffect(() => { - if (newColumn !== name && newDirection !== direction) { - onChange(index, newColumn, newDirection); - } - }, [direction, index, name, newColumn, newDirection, onChange]); + const [newColumn, setNewColumn] = useState(name); + const [newDirection, setNewDirection] = useState(direction ?? 'Descending'); + + const updateValues = useCallback( + (column?: string, direction?: string) => { + if (column !== undefined) setNewColumn(column); + if (direction !== undefined) setNewDirection(direction as SortDirection); + + // only update if the new column and direction are valid + if (column || newColumn) { + console.log(column, newColumn); + onChange(index, column ?? newColumn!, (direction as SortDirection) ?? newDirection!); + } + }, + [index, onChange, newColumn, newDirection] + ); return (
updateValues(column)} includeBlank /> ↑ Ascending, value: 'Ascending' }, - { label: <>↓ Descending , value: 'Descending' }, + { + label: ( +
+
+
A
+
+
Z
+
+
Ascending
+
+ ), + value: 'Ascending', + }, + { + label: ( +
+
+
Z
+
+
A
+
+
Descending
+
+ ), + value: 'Descending', + }, ]} - onChange={(direction) => setNewDirection(direction as SortDirection)} - includeBlank + onChange={(direction) => updateValues(undefined, direction as SortDirection)} />
{ private activeTable: Table | undefined; private hoverTable: Table | undefined; private contextMenuTable: Table | undefined; - private renameDataTable: Table | undefined; + + // either rename or sort + private actionDataTable: Table | undefined; tableCursor: string | undefined; @@ -137,7 +139,7 @@ export class Tables extends Container
{ } const hover = this.children.find((table) => table.checkHover(world)); // if we already have the active table open, then don't show hover - if (hover && (hover === this.contextMenuTable || hover === this.activeTable || hover === this.renameDataTable)) { + if (hover && (hover === this.contextMenuTable || hover === this.activeTable || hover === this.actionDataTable)) { return; } if (hover !== this.hoverTable) { @@ -183,13 +185,13 @@ export class Tables extends Container
{ contextMenu = (options?: ContextMenuOptions) => { // we keep the former context menu table active after the rename finishes // until the cursor moves again. - if (this.renameDataTable) { - this.renameDataTable.showTableName(); - this.renameDataTable.showColumnHeaders(); - if (this.activeTable !== this.renameDataTable) { - this.hoverTable = this.renameDataTable; + if (this.actionDataTable) { + this.actionDataTable.showTableName(); + this.actionDataTable.showColumnHeaders(); + if (this.activeTable !== this.actionDataTable) { + this.hoverTable = this.actionDataTable; } - this.renameDataTable = undefined; + this.actionDataTable = undefined; } if (this.contextMenuTable) { // we keep the former context menu table active after the context @@ -203,15 +205,23 @@ export class Tables extends Container
{ pixiApp.setViewportDirty(); return; } - if (options.type === ContextMenuType.Table && options.table) { + if (options.type === ContextMenuType.TableSort) { + this.actionDataTable = this.children.find((table) => table.codeCell === options.table); + if (this.actionDataTable) { + this.actionDataTable.showActive(); + if (this.hoverTable === this.actionDataTable) { + this.hoverTable = undefined; + } + } + } else if (options.type === ContextMenuType.Table && options.table) { if (options.rename) { - this.renameDataTable = this.children.find((table) => table.codeCell === options.table); - if (this.renameDataTable) { - this.renameDataTable.showActive(); + this.actionDataTable = this.children.find((table) => table.codeCell === options.table); + if (this.actionDataTable) { + this.actionDataTable.showActive(); if (options.selectedColumn === undefined) { - this.renameDataTable.hideTableName(); + this.actionDataTable.hideTableName(); } else { - this.renameDataTable.hideColumnHeaders(options.selectedColumn); + this.actionDataTable.hideColumnHeaders(options.selectedColumn); } this.hoverTable = undefined; } From cf0ea0eba58f7104159ca71885ba86a6f5dc0d51 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 24 Oct 2024 06:13:10 -0700 Subject: [PATCH 126/373] ensure table name is always on top of column headers --- quadratic-client/src/app/gridGL/cells/tables/Table.ts | 6 +++--- quadratic-client/src/app/gridGL/cells/tables/TableName.ts | 2 +- quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts | 7 ++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 6c80eb2fa4..68051bc502 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -95,7 +95,7 @@ export class Table extends Container { private columnHeadersInOverHeadings(bounds: Rectangle, gridHeading: number) { this.columnHeaders.x = this.tableBounds.x; this.columnHeaders.y = this.tableBounds.y + bounds.top + gridHeading - this.tableBounds.top; - pixiApp.overHeadings.addChild(this.columnHeaders); + pixiApp.overHeadingsColumnsHeaders.addChild(this.columnHeaders); this.gridLines.visible = true; this.inOverHeadings = true; } @@ -157,11 +157,11 @@ export class Table extends Container { } showTableName() { - pixiApp.overHeadings.addChild(this.tableName); + pixiApp.overHeadingsTableNames.addChild(this.tableName); } hideTableName() { - pixiApp.overHeadings.removeChild(this.tableName); + pixiApp.overHeadingsTableNames.removeChild(this.tableName); } hideColumnHeaders(index: number) { diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts index 8d30dacfd8..5ec1d892f4 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts @@ -38,7 +38,7 @@ export class TableName extends Container { // we only add to overHeadings if the sheet is active if (sheets.sheet.id === this.table.sheet.id) { - pixiApp.overHeadings.addChild(this); + pixiApp.overHeadingsTableNames.addChild(this); } } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index f6f91aee72..f7e8b04242 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -58,7 +58,10 @@ export class PixiApp { // this is used to display content over the headings (eg, table name when off // the screen) - overHeadings: Container; + private overHeadings: Container; + overHeadingsColumnsHeaders: Container; + overHeadingsTableNames: Container; + cellMoving!: UICellMoving; headings!: GridHeadings; boxCells!: BoxCells; @@ -90,6 +93,8 @@ export class PixiApp { this.cellImages = new UICellImages(); this.validations = new UIValidations(); this.overHeadings = new Container(); + this.overHeadingsColumnsHeaders = this.overHeadings.addChild(new Container()); + this.overHeadingsTableNames = this.overHeadings.addChild(new Container()); this.viewport = new Viewport(); } From 2aec59e6c2c26507330134e8c090950516d9d278 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 24 Oct 2024 08:50:44 -0700 Subject: [PATCH 127/373] table column context menu --- quadratic-client/src/app/actions/actions.ts | 5 ++ .../src/app/actions/dataTableSpec.ts | 54 +++++++++++++++++++ .../src/app/atoms/contextMenuAtom.ts | 1 + .../app/gridGL/HTMLGrid/HTMLGridContainer.tsx | 4 +- .../contextMenus/TableColumnContextMenu.tsx | 46 ++++++++++++---- .../contextMenus/TableContextMenu.tsx | 10 +++- .../HTMLGrid/contextMenus/TableMenu.tsx | 7 +++ .../contextMenus/tableSort/TableSort.tsx | 1 - .../interaction/pointer/PointerTable.ts | 2 + .../src/shared/components/Icons.tsx | 8 +++ 10 files changed, 123 insertions(+), 15 deletions(-) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 1be082e600..ad95eca4a3 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -149,4 +149,9 @@ export enum Action { CodeToDataTable = 'code_to_data_table', SortTable = 'table_sort', ToggleTableAlternatingColors = 'toggle_table_alternating_colors', + RenameTableColumn = 'rename_table_column', + SortTableColumnAscending = 'sort_table_column_ascending', + SortTableColumnDescending = 'sort_table_column_descending', + HideTableColumn = 'hide_table_column', + ShowAllColumns = 'show_all_columns', } diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 1c937c6326..f1e6f5ab50 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -8,11 +8,15 @@ import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { DeleteIcon, + DownArrowIcon, FileRenameIcon, FlattenTableIcon, + HideIcon, + ShowIcon, SortIcon, TableConvertIcon, TableIcon, + UpArrowIcon, } from '@/shared/components/Icons'; import { Rectangle } from 'pixi.js'; import { sheets } from '../grid/controller/Sheets'; @@ -29,6 +33,11 @@ type DataTableSpec = Pick< | Action.CodeToDataTable | Action.SortTable | Action.ToggleTableAlternatingColors + | Action.RenameTableColumn + | Action.SortTableColumnAscending + | Action.SortTableColumnDescending + | Action.HideTableColumn + | Action.ShowAllColumns >; const isFirstRowHeader = (): boolean => { @@ -144,4 +153,49 @@ export const dataTableSpec: DataTableSpec = { // quadraticCore.dataTableToggleAlternatingColors(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); }, }, + [Action.RenameTableColumn]: { + label: 'Rename column', + defaultOption: true, + Icon: FileRenameIcon, + run: () => { + const table = getTable(); + if (table) { + setTimeout(() => { + const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; + if (selectedColumn !== undefined) { + const contextMenu = { type: ContextMenuType.Table, rename: true, table, selectedColumn }; + events.emit('contextMenu', contextMenu); + } + }); + } + }, + }, + [Action.SortTableColumnAscending]: { + label: 'Sort column ascending', + Icon: UpArrowIcon, + run: () => { + console.log('TODO: sort column ascending'); + }, + }, + [Action.SortTableColumnDescending]: { + label: 'Sort column descending', + Icon: DownArrowIcon, + run: () => { + console.log('TODO: sort column descending'); + }, + }, + [Action.HideTableColumn]: { + label: 'Hide column', + Icon: HideIcon, + run: () => { + console.log('TODO: hide column'); + }, + }, + [Action.ShowAllColumns]: { + label: 'Show all columns', + Icon: ShowIcon, + run: () => { + console.log('TODO: show all columns'); + }, + }, }; diff --git a/quadratic-client/src/app/atoms/contextMenuAtom.ts b/quadratic-client/src/app/atoms/contextMenuAtom.ts index c38f723f92..1713aae52f 100644 --- a/quadratic-client/src/app/atoms/contextMenuAtom.ts +++ b/quadratic-client/src/app/atoms/contextMenuAtom.ts @@ -7,6 +7,7 @@ export enum ContextMenuType { Grid = 'grid', Table = 'table', TableSort = 'table-sort', + TableColumn = 'table-column', } export interface ContextMenuState { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index 70e76ae7bd..d5b95854df 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -3,9 +3,11 @@ import { Annotations } from '@/app/gridGL/HTMLGrid/annotations/Annotations'; import { CodeHint } from '@/app/gridGL/HTMLGrid/CodeHint'; import { CodeRunning } from '@/app/gridGL/HTMLGrid/codeRunning/CodeRunning'; import { GridContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/GridContextMenu'; +import { TableColumnContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu'; import { TableColumnHeaderRename } from '@/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename'; import { TableContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableContextMenu'; import { TableRename } from '@/app/gridGL/HTMLGrid/contextMenus/TableRename'; +import { TableSort } from '@/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort'; import { HoverCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; import { HoverTooltip } from '@/app/gridGL/HTMLGrid/hoverTooltip/HoverTooltip'; import { HtmlCells } from '@/app/gridGL/HTMLGrid/htmlCells/HtmlCells'; @@ -18,7 +20,6 @@ import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Following } from '@/app/ui/components/Following'; import { ReactNode, useCallback, useEffect, useState } from 'react'; import { SuggestionDropDown } from './SuggestionDropdown'; -import { TableSort } from '@/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort'; interface Props { parent?: HTMLDivElement; @@ -136,6 +137,7 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { > + diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx index 82629c57e0..1d6fcac14c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -1,22 +1,27 @@ //! This shows the table column's header context menu. +import { Action } from '@/app/actions/actions'; import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; +import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { focusGrid } from '@/app/helpers/focusGrid'; -import { ControlledMenu } from '@szhsin/react-menu'; +import { TableIcon } from '@/shared/components/Icons'; +import { ControlledMenu, MenuDivider, SubMenu } from '@szhsin/react-menu'; import { useCallback, useEffect, useRef } from 'react'; import { useRecoilState } from 'recoil'; -export const TableContextMenu = () => { +export const TableColumnContextMenu = () => { const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { - setContextMenu({}); - events.emit('contextMenuClose'); - focusGrid(); - }, [setContextMenu]); + if (contextMenu.type === ContextMenuType.TableColumn) { + setContextMenu({}); + events.emit('contextMenuClose'); + focusGrid(); + } + }, [contextMenu.type, setContextMenu]); useEffect(() => { pixiApp.viewport.on('moved', onClose); @@ -30,6 +35,11 @@ export const TableContextMenu = () => { const ref = useRef(null); + const display = + contextMenu.type === ContextMenuType.Table && contextMenu.column !== undefined && !contextMenu.rename + ? 'block' + : 'none'; + return (
{ top: contextMenu.world?.y ?? 0, transform: `scale(${1 / pixiApp.viewport.scale.x})`, pointerEvents: 'auto', - display: - contextMenu.type === ContextMenuType.Table && contextMenu.column !== undefined && !contextMenu.rename - ? 'block' - : 'none', + display, }} > { menuStyle={{ padding: '0', color: 'inherit' }} menuClassName="bg-background" > - + + + + + + + + + +
{contextMenu.table?.language === 'Import' ? 'Data' : 'Code'} Table
+
+ } + > + + ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index e35107b1f4..f9d47c0b64 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -13,7 +13,10 @@ export const TableContextMenu = () => { const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const onClose = useCallback(() => { - if (contextMenu.type !== ContextMenuType.Table || (contextMenu.type === ContextMenuType.Table && !contextMenu.rename)) { + if ( + contextMenu.type !== ContextMenuType.Table || + (contextMenu.type === ContextMenuType.Table && !contextMenu.rename) + ) { return; } setContextMenu({}); @@ -42,7 +45,10 @@ export const TableContextMenu = () => { top: contextMenu.world?.y ?? 0, transform: `scale(${1 / pixiApp.viewport.scale.x})`, pointerEvents: 'auto', - display: contextMenu.type === ContextMenuType.Table && !contextMenu.rename ? 'block' : 'none', + display: + contextMenu.type === ContextMenuType.TableColumn && contextMenu.selectedColumn !== undefined + ? 'block' + : 'none', }} > { } }, [codeCell]); + const hasHiddenColumns = useMemo(() => { + console.log('TODO: hasHiddenColumns', codeCell); + return false; + // return codeCell?.; + }, [codeCell]); + if (!codeCell) { return null; } @@ -42,6 +48,7 @@ export const TableMenu = (props: Props) => { {header} + {hasHiddenColumns && } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index 138c2d0a85..af88a15ed1 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -41,7 +41,6 @@ export const TableSort = () => { setSort([{ column_index: -1, direction: 'Ascending' }]); } }, [contextMenu]); - console.log(sort); const handleSave = useCallback(() => { if (contextMenu.table) { const sortToSend = sort.filter((item) => item.direction !== 'None' && item.column_index !== -1); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index f23b9a2632..d6bde24497 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -82,7 +82,9 @@ export class PointerTable { column: tableDown.table.x, row: tableDown.table.y, table: tableDown.table, + selectedColumn: tableDown.column, }); + return true; } if (tableDown.type === 'table-name') { diff --git a/quadratic-client/src/shared/components/Icons.tsx b/quadratic-client/src/shared/components/Icons.tsx index 4030dab606..41af862e51 100644 --- a/quadratic-client/src/shared/components/Icons.tsx +++ b/quadratic-client/src/shared/components/Icons.tsx @@ -462,3 +462,11 @@ export const UpArrowIcon: IconComponent = (props) => { export const DownArrowIcon: IconComponent = (props) => { return arrow_downward; }; + +export const HideIcon: IconComponent = (props) => { + return visibility_off; +}; + +export const ShowIcon: IconComponent = (props) => { + return visibility; +}; From c12a208ab69b46b2288f89539abdd6a0fb56c971 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 24 Oct 2024 10:06:49 -0700 Subject: [PATCH 128/373] fix table context menu --- .../gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx | 2 +- .../src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx index 1d6fcac14c..6b2ad2e6bb 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -36,7 +36,7 @@ export const TableColumnContextMenu = () => { const ref = useRef(null); const display = - contextMenu.type === ContextMenuType.Table && contextMenu.column !== undefined && !contextMenu.rename + contextMenu.type === ContextMenuType.Table && contextMenu.selectedColumn !== undefined && !contextMenu.rename ? 'block' : 'none'; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index f9d47c0b64..0fdd697c79 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -46,9 +46,7 @@ export const TableContextMenu = () => { transform: `scale(${1 / pixiApp.viewport.scale.x})`, pointerEvents: 'auto', display: - contextMenu.type === ContextMenuType.TableColumn && contextMenu.selectedColumn !== undefined - ? 'block' - : 'none', + contextMenu.type === ContextMenuType.Table && contextMenu.selectedColumn === undefined ? 'block' : 'none', }} > Date: Thu, 24 Oct 2024 10:24:52 -0700 Subject: [PATCH 129/373] remove flicker for sort dialog --- .../app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index af88a15ed1..8b7b86d237 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -53,6 +53,7 @@ export const TableSort = () => { handleClose(); }, [contextMenu.table, sort, handleClose]); + const [display, setDisplay] = useState('none'); useEffect(() => { const changePosition = () => { if (!ref.current) { @@ -65,6 +66,7 @@ export const TableSort = () => { ref.current.style.left = `${position.x}px`; ref.current.style.top = `${position.y}px`; ref.current.style.display = 'block'; + setDisplay('block'); } } }; @@ -142,6 +144,7 @@ export const TableSort = () => { transformOrigin: 'top left', transform: `scale(${1 / pixiApp.viewport.scaled})`, width: 450, + display, }} onKeyDown={(e) => { if (e.key === 'Escape') { From 079e1670bfda921611e1653f38ab440227dc8f41 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 24 Oct 2024 13:00:43 -0600 Subject: [PATCH 130/373] Finish reworking the sort api --- .../HTMLGrid/contextMenus/tableSort/TableSort.tsx | 2 +- .../app/gridGL/cells/tables/TableColumnHeaders.ts | 4 ++-- .../quadraticCore/coreClientMessages.ts | 3 +-- .../app/web-workers/quadraticCore/quadraticCore.ts | 11 ++++++++--- .../app/web-workers/quadraticCore/worker/core.ts | 11 +++++++++-- .../web-workers/quadraticCore/worker/coreClient.ts | 2 +- quadratic-core/src/grid/data_table.rs | 10 ++++++++-- .../src/wasm_bindings/controller/data_table.rs | 14 ++++++++++++-- 8 files changed, 42 insertions(+), 15 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index 138c2d0a85..46295b2550 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -41,7 +41,7 @@ export const TableSort = () => { setSort([{ column_index: -1, direction: 'Ascending' }]); } }, [contextMenu]); - console.log(sort); + const handleSave = useCallback(() => { if (contextMenu.table) { const sortToSend = sort.filter((item) => item.direction !== 'None' && item.column_index !== -1); diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index daf70aa19c..7eee3d8943 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -56,12 +56,12 @@ export class TableColumnHeaders extends Container { throw new Error('Unknown sort order in onSortPressed'); } const table = this.table.codeCell; + quadraticCore.sortDataTable( sheets.sheet.id, table.x, table.y, - column.valueIndex, - newOrder, + [{ column_index: column.valueIndex, direction: newOrder }], sheets.getCursorPosition() ); } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index ca76fb8cb6..608073e046 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -1052,8 +1052,7 @@ export interface ClientCoreSortDataTable { sheetId: string; x: number; y: number; - columnIndex: number; - sortOrder: string; + sort: { column_index: number; direction: string }[]; cursor: string; } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index 167d19cdfd..2858b5713f 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -1220,14 +1220,19 @@ class QuadraticCore { }); } - sortDataTable(sheetId: string, x: number, y: number, columnIndex: number, sortOrder: string, cursor: string) { + sortDataTable( + sheetId: string, + x: number, + y: number, + sort: { column_index: number; direction: string }[], + cursor: string + ) { this.send({ type: 'clientCoreSortDataTable', sheetId, x, y, - columnIndex, - sortOrder, + sort, cursor, }); } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index ba6e340489..1f29a56f02 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1135,9 +1135,16 @@ class Core { this.gridController.updateDataTableName(sheetId, posToPos(x, y), name, cursor); } - sortDataTable(sheetId: string, x: number, y: number, column_index: number, sort_order: string, cursor: string) { + sortDataTable( + sheetId: string, + x: number, + y: number, + sort: { column_index: number; direction: string }[], + cursor: string + ) { if (!this.gridController) throw new Error('Expected gridController to be defined'); - this.gridController.sortDataTable(sheetId, posToPos(x, y), column_index, sort_order, cursor); + console.log('sortDataTable', sheetId, x, y, sort, cursor); + this.gridController.sortDataTable(sheetId, posToPos(x, y), JSON.stringify(sort), cursor); } dataTableFirstRowAsHeader(sheetId: string, x: number, y: number, firstRowAsHeader: boolean, cursor: string) { diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index 8ba8a1c652..d554769734 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -598,7 +598,7 @@ class CoreClient { return; case 'clientCoreSortDataTable': - core.sortDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.columnIndex, e.data.sortOrder, e.data.cursor); + core.sortDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.sort, e.data.cursor); return; case 'clientCoreDataTableFirstRowAsHeader': diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index d5edfcd4fc..254e970550 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -17,7 +17,7 @@ use anyhow::{anyhow, Ok, Result}; use chrono::{DateTime, Utc}; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use strum_macros::Display; +use strum_macros::{Display, EnumString}; use tabled::{ builder::Builder, settings::{Color, Modify, Style}, @@ -81,10 +81,16 @@ impl DataTableColumn { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS, Display, EnumString)] pub enum SortDirection { + #[serde(rename = "asc")] + #[strum(serialize = "asc")] Ascending, + #[serde(rename = "desc")] + #[strum(serialize = "desc")] Descending, + #[serde(rename = "none")] + #[strum(serialize = "none")] None, } diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index 0ed234b78e..64bf73544d 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -55,12 +55,22 @@ impl GridController { &mut self, sheet_id: String, pos: String, - sort: Option, + sort_js: Option, cursor: Option, ) -> Result<(), JsValue> { let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; - let sort = sort.map(|s| serde_json::from_str::>(&s).unwrap_or_default()); + + let mut sort = None; + + if let Some(sort_js) = sort_js { + sort = Some( + serde_json::from_str::>(&sort_js).map_err(|e| e.to_string())?, + ); + } + + // let sort = sort.map(|s| serde_json::from_str::>(&s)?); + dbgjs!(&sort); self.sort_data_table(pos.to_sheet_pos(sheet_id), sort, cursor); Ok(()) From 5f11f308a586ae08b7aa0163ba45a0983925c19b Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 24 Oct 2024 16:24:06 -0600 Subject: [PATCH 131/373] Fix all broken tests --- .../execute_operation/execute_data_table.rs | 3 +- .../execution/run_code/run_javascript.rs | 25 +++++++++++++---- .../execution/run_code/run_python.rs | 23 ++++++++++++--- .../src/controller/execution/spills.rs | 28 ++++++++++++++++--- quadratic-core/src/grid/data_table.rs | 19 +++++++------ quadratic-core/src/grid/js_types.rs | 9 +++++- quadratic-core/src/grid/sheet/rendering.rs | 7 +++-- 7 files changed, 88 insertions(+), 26 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index fd3e73bdbd..18c8a785f5 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -381,6 +381,7 @@ impl GridController { let old_value = data_table.sort.to_owned(); data_table.sort = sort; + data_table.sort_all()?; self.send_to_wasm(transaction, &data_table_sheet_rect)?; transaction.add_code_cell(sheet_id, data_table_pos.into()); @@ -483,7 +484,7 @@ mod tests { gc.execute_flatten_data_table(&mut transaction, op).unwrap(); assert_eq!(transaction.forward_operations.len(), 1); - assert_eq!(transaction.reverse_operations.len(), 1); + assert_eq!(transaction.reverse_operations.len(), 2); assert!(gc.sheet(sheet_id).first_data_table_within(pos).is_err()); diff --git a/quadratic-core/src/controller/execution/run_code/run_javascript.rs b/quadratic-core/src/controller/execution/run_code/run_javascript.rs index f43c991fb0..82ff891b14 100644 --- a/quadratic-core/src/controller/execution/run_code/run_javascript.rs +++ b/quadratic-core/src/controller/execution/run_code/run_javascript.rs @@ -35,7 +35,7 @@ mod tests { controller::{ execution::run_code::get_cells::JsGetCellResponse, transaction_types::JsCodeResult, }, - grid::js_types::JsRenderCell, + grid::js_types::{JsRenderCell, JsRenderCellSpecial}, ArraySize, CellValue, Pos, Rect, }; use bigdecimal::BigDecimal; @@ -334,14 +334,29 @@ mod tests { let sheet = gc.try_sheet(sheet_id).unwrap(); let cells = sheet.get_render_cells(Rect::from_numbers(0, 0, 1, 3)); - // println!("{:?}", cells); + assert_eq!(cells.len(), 3); assert_eq!( cells[0], - JsRenderCell::new_number(0, 0, 1, Some(CodeCellLanguage::Javascript)) + JsRenderCell::new_number( + 0, + 0, + 1, + Some(CodeCellLanguage::Javascript), + Some(JsRenderCellSpecial::TableAlternatingColor) + ) + ); + assert_eq!(cells[1], JsRenderCell::new_number(0, 1, 2, None, None)); + assert_eq!( + cells[2], + JsRenderCell::new_number( + 0, + 2, + 3, + None, + Some(JsRenderCellSpecial::TableAlternatingColor) + ) ); - assert_eq!(cells[1], JsRenderCell::new_number(0, 1, 2, None)); - assert_eq!(cells[2], JsRenderCell::new_number(0, 2, 3, None)); } #[test] diff --git a/quadratic-core/src/controller/execution/run_code/run_python.rs b/quadratic-core/src/controller/execution/run_code/run_python.rs index 8d65fbedeb..e455cd7ee1 100644 --- a/quadratic-core/src/controller/execution/run_code/run_python.rs +++ b/quadratic-core/src/controller/execution/run_code/run_python.rs @@ -34,7 +34,7 @@ mod tests { controller::{ execution::run_code::get_cells::JsGetCellResponse, transaction_types::JsCodeResult, }, - grid::js_types::JsRenderCell, + grid::js_types::{JsRenderCell, JsRenderCellSpecial}, ArraySize, CellValue, Pos, Rect, }; use bigdecimal::BigDecimal; @@ -353,10 +353,25 @@ mod tests { assert_eq!(cells.len(), 3); assert_eq!( cells[0], - JsRenderCell::new_number(0, 0, 1, Some(CodeCellLanguage::Python)) + JsRenderCell::new_number( + 0, + 0, + 1, + Some(CodeCellLanguage::Python), + Some(JsRenderCellSpecial::TableAlternatingColor) + ) + ); + assert_eq!(cells[1], JsRenderCell::new_number(0, 1, 2, None, None)); + assert_eq!( + cells[2], + JsRenderCell::new_number( + 0, + 2, + 3, + None, + Some(JsRenderCellSpecial::TableAlternatingColor) + ) ); - assert_eq!(cells[1], JsRenderCell::new_number(0, 1, 2, None)); - assert_eq!(cells[2], JsRenderCell::new_number(0, 2, 3, None)); // transaction should be completed let async_transaction = gc.transactions.get_async_transaction(transaction_id); diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index 3a8dd28191..96a9dab147 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -133,6 +133,7 @@ mod tests { y: i64, n: &str, language: Option, + special: Option, ) -> Vec { vec![JsRenderCell { x, @@ -141,6 +142,7 @@ mod tests { value: n.into(), align: Some(CellAlign::Right), number: Some(JsNumber::default()), + special, ..Default::default() }] } @@ -293,7 +295,13 @@ mod tests { // should be B0: "1" since spill was removed assert_eq!( render_cells, - output_number(0, 0, "1", Some(CodeCellLanguage::Formula)), + output_number( + 0, + 0, + "1", + Some(CodeCellLanguage::Formula), + Some(JsRenderCellSpecial::TableAlternatingColor) + ), ); } @@ -330,10 +338,16 @@ mod tests { let render_cells = sheet.get_render_cells(Rect::single_pos(Pos { x: 0, y: 0 })); assert_eq!( render_cells, - output_number(0, 0, "1", Some(CodeCellLanguage::Formula)) + output_number( + 0, + 0, + "1", + Some(CodeCellLanguage::Formula), + Some(JsRenderCellSpecial::TableAlternatingColor) + ) ); let render_cells = sheet.get_render_cells(Rect::single_pos(Pos { x: 0, y: 1 })); - assert_eq!(render_cells, output_number(0, 1, "2", None),); + assert_eq!(render_cells, output_number(0, 1, "2", None, None)); gc.set_code_cell( SheetPos { @@ -416,7 +430,13 @@ mod tests { let render_cells = sheet.get_render_cells(Rect::single_pos(Pos { x: 11, y: 9 })); assert_eq!( render_cells, - output_number(11, 9, "1", Some(CodeCellLanguage::Formula)) + output_number( + 11, + 9, + "1", + Some(CodeCellLanguage::Formula), + Some(JsRenderCellSpecial::TableAlternatingColor) + ) ); } diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table.rs index 254e970550..9c06cbe4ee 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table.rs @@ -17,7 +17,7 @@ use anyhow::{anyhow, Ok, Result}; use chrono::{DateTime, Utc}; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use strum_macros::{Display, EnumString}; +use strum_macros::Display; use tabled::{ builder::Builder, settings::{Color, Modify, Style}, @@ -81,16 +81,13 @@ impl DataTableColumn { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS, Display, EnumString)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub enum SortDirection { #[serde(rename = "asc")] - #[strum(serialize = "asc")] Ascending, #[serde(rename = "desc")] - #[strum(serialize = "desc")] Descending, #[serde(rename = "none")] - #[strum(serialize = "none")] None, } @@ -712,9 +709,15 @@ pub mod test { let values = data_table.value.clone().into_array().unwrap(); let expected_values = Value::Array(values.clone().into()); - let expected_data_table = - DataTable::new(kind.clone(), "Table 1", expected_values, false, false, true) - .with_last_modified(data_table.last_modified); + let expected_data_table = DataTable::new( + kind.clone(), + "test.csv", + expected_values, + false, + false, + true, + ) + .with_last_modified(data_table.last_modified); let expected_array_size = ArraySize::new(4, 5).unwrap(); assert_eq!(data_table, expected_data_table); assert_eq!(data_table.output_size(), expected_array_size); diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index e4be5b2041..90fc8eecdb 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -95,7 +95,13 @@ pub struct JsRenderCell { #[cfg(test)] impl JsRenderCell { - pub fn new_number(x: i64, y: i64, value: isize, language: Option) -> Self { + pub fn new_number( + x: i64, + y: i64, + value: isize, + language: Option, + special: Option, + ) -> Self { Self { x, y, @@ -103,6 +109,7 @@ impl JsRenderCell { language, align: Some(CellAlign::Right), number: Some(JsNumber::default()), + special, ..Default::default() } } diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 1d8e68c781..8f5c49f344 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -1032,6 +1032,7 @@ mod tests { language: Some(CodeCellLanguage::Formula), align: Some(CellAlign::Right), number: Some(JsNumber::default()), + special: Some(JsRenderCellSpecial::TableAlternatingColor), ..Default::default() }] ); @@ -1208,21 +1209,21 @@ mod tests { y: 0, value: "true".to_string(), language: Some(CodeCellLanguage::Formula), - special: Some(JsRenderCellSpecial::Logical), + special: Some(JsRenderCellSpecial::TableAlternatingColor), ..Default::default() }, JsRenderCell { x: 1, y: 0, value: "false".to_string(), - special: Some(JsRenderCellSpecial::Logical), + special: Some(JsRenderCellSpecial::TableAlternatingColor), ..Default::default() }, JsRenderCell { x: 2, y: 0, value: "true".to_string(), - special: Some(JsRenderCellSpecial::Logical), + special: Some(JsRenderCellSpecial::TableAlternatingColor), ..Default::default() }, ]; From d10943790600e5037990520585999969ec2f6344 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 24 Oct 2024 16:59:40 -0600 Subject: [PATCH 132/373] Refactor data_table.rs into a folder with multiple files --- quadratic-core/src/bin/export_types.rs | 4 +- .../execute_operation/execute_data_table.rs | 5 +- .../src/controller/operations/data_table.rs | 2 +- .../src/controller/operations/operation.rs | 3 +- .../src/controller/user_actions/data_table.rs | 2 +- quadratic-core/src/grid/data_table/column.rs | 262 ++++++++++ .../src/grid/data_table/display_value.rs | 73 +++ .../grid/{data_table.rs => data_table/mod.rs} | 448 +----------------- quadratic-core/src/grid/data_table/sort.rs | 134 ++++++ .../src/grid/file/serialize/data_table.rs | 6 +- quadratic-core/src/grid/js_types.rs | 3 +- quadratic-core/src/grid/mod.rs | 2 +- .../wasm_bindings/controller/data_table.rs | 3 +- 13 files changed, 496 insertions(+), 451 deletions(-) create mode 100644 quadratic-core/src/grid/data_table/column.rs create mode 100644 quadratic-core/src/grid/data_table/display_value.rs rename quadratic-core/src/grid/{data_table.rs => data_table/mod.rs} (53%) create mode 100644 quadratic-core/src/grid/data_table/sort.rs diff --git a/quadratic-core/src/bin/export_types.rs b/quadratic-core/src/bin/export_types.rs index 65bf7dd224..67959ad91b 100644 --- a/quadratic-core/src/bin/export_types.rs +++ b/quadratic-core/src/bin/export_types.rs @@ -3,6 +3,7 @@ use std::fs::create_dir_all; use crate::grid::sheet::borders::{JsBorderHorizontal, JsBorderVertical, JsBordersSheet}; use controller::operations::clipboard::PasteSpecial; use formulas::{CellRef, CellRefCoord, RangeRef}; +use grid::data_table::sort::{DataTableSort, SortDirection}; use grid::formats::format::Format; use grid::js_types::{ CellFormatSummary, JsCellValue, JsClipboard, JsDataTableColumn, JsOffset, JsPos, JsRenderFill, @@ -28,8 +29,7 @@ use grid::sheet::validations::validation_rules::validation_text::{ }; use grid::sheet::validations::validation_rules::ValidationRule; use grid::{ - CellAlign, CellVerticalAlign, CellWrap, DataTableSort, GridBounds, NumericFormat, - NumericFormatKind, SheetId, SortDirection, + CellAlign, CellVerticalAlign, CellWrap, GridBounds, NumericFormat, NumericFormatKind, SheetId, }; use quadratic_core::color::Rgba; use quadratic_core::controller::active_transactions::transaction_name::TransactionName; diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 18c8a785f5..55543f1924 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -459,7 +459,10 @@ mod tests { }, user_actions::import::tests::{assert_simple_csv, simple_csv}, }, - grid::{CodeCellLanguage, CodeRun, DataTableSort, SheetId, SortDirection}, + grid::{ + data_table::sort::{DataTableSort, SortDirection}, + CodeCellLanguage, CodeRun, SheetId, + }, test_util::{ assert_cell_value_row, assert_data_table_cell_value, assert_data_table_cell_value_row, print_data_table, print_table, diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index ff0377724b..08b25ffb19 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -2,7 +2,7 @@ use super::operation::Operation; use crate::{ cellvalue::Import, controller::GridController, - grid::{DataTableKind, DataTableSort}, + grid::{data_table::sort::DataTableSort, DataTableKind}, CellValue, SheetPos, SheetRect, }; diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index b82f300983..56321faa0e 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -5,12 +5,13 @@ use uuid::Uuid; use crate::{ cell_values::CellValues, grid::{ + data_table::sort::DataTableSort, file::sheet_schema::SheetSchema, formats::Formats, formatting::CellFmtArray, js_types::JsRowHeight, sheet::{borders::BorderStyleCellUpdates, validations::validation::Validation}, - DataTable, DataTableKind, DataTableSort, Sheet, SheetBorders, SheetId, + DataTable, DataTableKind, Sheet, SheetBorders, SheetId, }, selection::Selection, SheetPos, SheetRect, diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index 288c36fdf9..fa0ce4c457 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -1,6 +1,6 @@ use crate::{ controller::{active_transactions::transaction_name::TransactionName, GridController}, - grid::DataTableSort, + grid::sort::DataTableSort, Pos, SheetPos, SheetRect, }; diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs new file mode 100644 index 0000000000..2c15b829a2 --- /dev/null +++ b/quadratic-core/src/grid/data_table/column.rs @@ -0,0 +1,262 @@ +//! + +use crate::grid::js_types::JsDataTableColumn; +use crate::{CellValue, Value}; +use anyhow::{anyhow, Ok}; +use serde::{Deserialize, Serialize}; + +use super::DataTable; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DataTableColumn { + pub name: CellValue, + pub display: bool, + pub value_index: u32, +} + +impl DataTableColumn { + pub fn new(name: String, display: bool, value_index: u32) -> Self { + DataTableColumn { + name: CellValue::Text(name), + display, + value_index, + } + } +} + +impl DataTable { + /// Takes the first row of the array and sets it as the column headings. + pub fn apply_first_row_as_header(&mut self) { + self.header_is_first_row = true; + + self.columns = match self.value { + // Value::Array(ref mut array) => array.shift().ok().map(|array| { + Value::Array(ref mut array) => array.get_row(0).ok().map(|array| { + array + .iter() + .enumerate() + .map(|(i, value)| DataTableColumn::new(value.to_string(), true, i as u32)) + .collect::>() + }), + _ => None, + }; + } + + pub fn toggle_first_row_as_header(&mut self, first_row_as_header: bool) { + self.header_is_first_row = first_row_as_header; + + match first_row_as_header { + true => self.apply_first_row_as_header(), + false => self.apply_default_header(), + } + } + + /// Apply default column headings to the DataTable. + /// For example, the column headings will be "Column 1", "Column 2", etc. + pub fn apply_default_header(&mut self) { + self.columns = match self.value { + Value::Array(ref mut array) => Some( + (1..=array.width()) + .map(|i| DataTableColumn::new(format!("Column {i}"), true, i - 1)) + .collect::>(), + ), + _ => None, + }; + } + + /// Ensure that the index is within the bounds of the columns. + /// If there are no columns, apply default headers first if `apply_default_header` is true. + fn check_index(&mut self, index: usize, apply_default_header: bool) -> anyhow::Result<()> { + match self.columns { + Some(ref mut columns) => { + let column_len = columns.len(); + + if index >= column_len { + return Err(anyhow!("Column {index} out of bounds: {column_len}")); + } + } + // there are no columns, so we need to apply default headers first + None => { + apply_default_header.then(|| self.apply_default_header()); + } + }; + + Ok(()) + } + + /// Replace a column header at the given index in place. + pub fn set_header_at( + &mut self, + index: usize, + name: String, + display: bool, + ) -> anyhow::Result<()> { + self.check_index(index, true)?; + // let all_names = &self + // .columns + // .as_ref() + // .unwrap() + // .iter() + // .map(|column| column.name.to_string().to_owned().as_str()) + // .collect_vec(); + // let name = unique_name(&name, all_names); + + self.columns + .as_mut() + .and_then(|columns| columns.get_mut(index)) + .map(|column| { + column.name = CellValue::Text(name); + column.display = display; + }); + + Ok(()) + } + + /// Set the display of a column header at the given index. + pub fn set_header_display_at(&mut self, index: usize, display: bool) -> anyhow::Result<()> { + self.check_index(index, true)?; + + self.columns + .as_mut() + .and_then(|columns| columns.get_mut(index)) + .map(|column| { + column.display = display; + }); + + Ok(()) + } + + pub fn adjust_for_header(&self, index: usize) -> usize { + if self.header_is_first_row { + index + 1 + } else { + index + } + } + + /// Prepares the columns to be sent to the client. If no columns are set, it + /// will create default columns. + pub fn send_columns(&self) -> Vec { + match self.columns.as_ref() { + Some(columns) => columns + .iter() + .map(|column| JsDataTableColumn::from(column.to_owned())) + .collect(), + // TODO(ddimaria): refacor this to use the default columns + None => { + let size = self.output_size(); + (0..size.w.get()) + .map(|i| DataTableColumn::new(format!("Column {}", i + 1), true, i).into()) + .collect::>() + } + } + } +} + +#[cfg(test)] +pub mod test { + + use super::*; + use crate::{ + cellvalue::Import, + grid::{test::new_data_table, DataTableKind, Sheet}, + Array, Pos, + }; + use chrono::Utc; + use serial_test::parallel; + + #[test] + #[parallel] + fn test_data_table_and_headers() { + // test data table without column headings + let (_, mut data_table) = new_data_table(); + let kind = data_table.kind.clone(); + let values = data_table.value.clone().into_array().unwrap(); + + data_table.apply_default_header(); + let expected_columns = vec![ + DataTableColumn::new("Column 1".into(), true, 0), + DataTableColumn::new("Column 2".into(), true, 1), + DataTableColumn::new("Column 3".into(), true, 2), + DataTableColumn::new("Column 4".into(), true, 3), + ]; + assert_eq!(data_table.columns, Some(expected_columns)); + + // test column headings taken from first row + let value = Value::Array(values.clone().into()); + let mut data_table = DataTable::new(kind.clone(), "Table 1", value, false, true, true) + .with_last_modified(data_table.last_modified); + + data_table.apply_first_row_as_header(); + let expected_columns = vec![ + DataTableColumn::new("city".into(), true, 0), + DataTableColumn::new("region".into(), true, 1), + DataTableColumn::new("country".into(), true, 2), + DataTableColumn::new("population".into(), true, 3), + ]; + assert_eq!(data_table.columns, Some(expected_columns)); + + let expected_values = values.clone(); + assert_eq!( + data_table.value.clone().into_array().unwrap(), + expected_values + ); + + // test setting header at index + data_table.set_header_at(0, "new".into(), true).unwrap(); + assert_eq!( + data_table.columns.as_ref().unwrap()[0].name, + CellValue::Text("new".into()) + ); + + // test setting header display at index + data_table.set_header_display_at(0, false).unwrap(); + assert_eq!(data_table.columns.as_ref().unwrap()[0].display, false); + } + + #[test] + #[parallel] + fn test_headers_y() { + let mut sheet = Sheet::test(); + let array = Array::from_str_vec(vec![vec!["first"], vec!["second"]], true).unwrap(); + let pos = Pos { x: 1, y: 1 }; + let t = DataTable { + kind: DataTableKind::Import(Import::new("test.csv".to_string())), + name: "Table 1".into(), + columns: None, + sort: None, + display_buffer: None, + value: Value::Array(array), + readonly: false, + spill_error: false, + last_modified: Utc::now(), + show_header: true, + header_is_first_row: true, + alternating_colors: true, + }; + sheet.set_cell_value( + pos, + Some(CellValue::Import(Import::new("test.csv".to_string()))), + ); + sheet.set_data_table(pos, Some(t.clone())); + assert_eq!( + sheet.display_value(pos), + Some(CellValue::Text("first".into())) + ); + + let data_table = sheet.data_table_mut((1, 1).into()).unwrap(); + data_table.toggle_first_row_as_header(false); + assert_eq!( + sheet.display_value(pos), + Some(CellValue::Text("Column 1".into())) + ); + assert_eq!( + sheet.display_value(Pos { x: 1, y: 2 }), + Some(CellValue::Text("first".into())) + ); + assert_eq!( + sheet.display_value(Pos { x: 1, y: 3 }), + Some(CellValue::Text("second".into())) + ); + } +} diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs new file mode 100644 index 0000000000..2c02aafd50 --- /dev/null +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -0,0 +1,73 @@ +//! + +use crate::{Array, CellValue, Pos, Value}; +use anyhow::{anyhow, Ok, Result}; + +use super::DataTable; + +impl DataTable { + pub fn display_value_from_buffer(&self, display_buffer: &Vec) -> Result { + let value = self.value.to_owned().into_array()?; + + let values = display_buffer + .iter() + .filter_map(|index| { + value + .get_row(*index as usize) + .map(|row| row.into_iter().cloned().collect::>()) + .ok() + }) + .collect::>>(); + + let array = Array::from(values); + + Ok(array.into()) + } + + pub fn display_value_from_buffer_at( + &self, + display_buffer: &Vec, + pos: Pos, + ) -> Result<&CellValue> { + let y = display_buffer + .get(pos.y as usize) + .ok_or_else(|| anyhow!("Y {} out of bounds: {}", pos.y, display_buffer.len()))?; + let cell_value = self.value.get(pos.x as u32, *y as u32)?; + + Ok(cell_value) + } + + pub fn display_value(&self) -> Result { + match self.display_buffer { + Some(ref display_buffer) => self.display_value_from_buffer(display_buffer), + None => Ok(self.value.to_owned()), + } + } + + pub fn display_value_at(&self, mut pos: Pos) -> Result<&CellValue> { + // println!("pos: {:?}", pos); + // println!("self.columns: {:?}", self.columns); + + if pos.y == 0 && self.show_header { + if let Some(columns) = &self.columns { + // println!("columns: {:?}", columns); + if let Some(column) = columns.get(pos.x as usize) { + // println!("column: {:?}", column); + return Ok(column.name.as_ref()); + } + } + } + + if !self.header_is_first_row && self.show_header { + pos.y = pos.y - 1; + } + + match self.display_buffer { + Some(ref display_buffer) => self.display_value_from_buffer_at(display_buffer, pos), + None => Ok(self.value.get(pos.x as u32, pos.y as u32)?), + } + } +} + +#[cfg(test)] +pub mod test {} diff --git a/quadratic-core/src/grid/data_table.rs b/quadratic-core/src/grid/data_table/mod.rs similarity index 53% rename from quadratic-core/src/grid/data_table.rs rename to quadratic-core/src/grid/data_table/mod.rs index 9c06cbe4ee..f2dc2dea7b 100644 --- a/quadratic-core/src/grid/data_table.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -4,10 +4,13 @@ //! any given CellValue::Code type (ie, if it doesn't exist then a run hasn't been //! performed yet). +pub mod column; +pub mod display_value; +pub mod sort; + use std::num::NonZeroU32; use crate::cellvalue::Import; -use crate::grid::js_types::JsDataTableColumn; use crate::grid::CodeRun; use crate::util::unique_name; use crate::{ @@ -15,14 +18,15 @@ use crate::{ }; use anyhow::{anyhow, Ok, Result}; use chrono::{DateTime, Utc}; +use column::DataTableColumn; use itertools::Itertools; use serde::{Deserialize, Serialize}; +use sort::DataTableSort; use strum_macros::Display; use tabled::{ builder::Builder, settings::{Color, Modify, Style}, }; -use ts_rs::TS; use super::Grid; @@ -58,45 +62,12 @@ impl Grid { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct DataTableColumn { - pub name: CellValue, - pub display: bool, - pub value_index: u32, -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Display)] pub enum DataTableKind { CodeRun(CodeRun), Import(Import), } -impl DataTableColumn { - pub fn new(name: String, display: bool, value_index: u32) -> Self { - DataTableColumn { - name: CellValue::Text(name), - display, - value_index, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] -pub enum SortDirection { - #[serde(rename = "asc")] - Ascending, - #[serde(rename = "desc")] - Descending, - #[serde(rename = "none")] - None, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] -pub struct DataTableSort { - pub column_index: usize, - pub direction: SortDirection, -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct DataTable { pub kind: DataTableKind, @@ -171,288 +142,16 @@ impl DataTable { data_table } - /// Directly creates a new DataTable with the given kind, value, spill_error, and columns. - pub fn new_raw( - kind: DataTableKind, - name: &str, - header_is_first_row: bool, - show_header: bool, - columns: Option>, - sort: Option>, - display_buffer: Option>, - value: Value, - readonly: bool, - spill_error: bool, - ) -> Self { - DataTable { - kind, - name: name.into(), - header_is_first_row, - show_header, - columns, - sort, - display_buffer, - value, - readonly, - spill_error, - last_modified: Utc::now(), - alternating_colors: true, - } - } - /// Apply a new last modified date to the DataTable. pub fn with_last_modified(mut self, last_modified: DateTime) -> Self { self.last_modified = last_modified; self } - /// Takes the first row of the array and sets it as the column headings. - pub fn apply_first_row_as_header(&mut self) { - self.header_is_first_row = true; - - self.columns = match self.value { - // Value::Array(ref mut array) => array.shift().ok().map(|array| { - Value::Array(ref mut array) => array.get_row(0).ok().map(|array| { - array - .iter() - .enumerate() - .map(|(i, value)| DataTableColumn::new(value.to_string(), true, i as u32)) - .collect::>() - }), - _ => None, - }; - } - - pub fn toggle_first_row_as_header(&mut self, first_row_as_header: bool) { - self.header_is_first_row = first_row_as_header; - - match first_row_as_header { - true => self.apply_first_row_as_header(), - false => self.apply_default_header(), - } - } - - /// Apply default column headings to the DataTable. - /// For example, the column headings will be "Column 1", "Column 2", etc. - pub fn apply_default_header(&mut self) { - self.columns = match self.value { - Value::Array(ref mut array) => Some( - (1..=array.width()) - .map(|i| DataTableColumn::new(format!("Column {i}"), true, i - 1)) - .collect::>(), - ), - _ => None, - }; - } - - /// Ensure that the index is within the bounds of the columns. - /// If there are no columns, apply default headers first if `apply_default_header` is true. - fn check_index(&mut self, index: usize, apply_default_header: bool) -> anyhow::Result<()> { - match self.columns { - Some(ref mut columns) => { - let column_len = columns.len(); - - if index >= column_len { - return Err(anyhow!("Column {index} out of bounds: {column_len}")); - } - } - // there are no columns, so we need to apply default headers first - None => { - apply_default_header.then(|| self.apply_default_header()); - } - }; - - Ok(()) - } - - /// Replace a column header at the given index in place. - pub fn set_header_at( - &mut self, - index: usize, - name: String, - display: bool, - ) -> anyhow::Result<()> { - self.check_index(index, true)?; - // let all_names = &self - // .columns - // .as_ref() - // .unwrap() - // .iter() - // .map(|column| column.name.to_string().to_owned().as_str()) - // .collect_vec(); - // let name = unique_name(&name, all_names); - - self.columns - .as_mut() - .and_then(|columns| columns.get_mut(index)) - .map(|column| { - column.name = CellValue::Text(name); - column.display = display; - }); - - Ok(()) - } - - /// Set the display of a column header at the given index. - pub fn set_header_display_at(&mut self, index: usize, display: bool) -> anyhow::Result<()> { - self.check_index(index, true)?; - - self.columns - .as_mut() - .and_then(|columns| columns.get_mut(index)) - .map(|column| { - column.display = display; - }); - - Ok(()) - } - - fn adjust_for_header(&self, index: usize) -> usize { - if self.header_is_first_row { - index + 1 - } else { - index - } - } - pub fn update_table_name(&mut self, name: &str) { self.name = name.into(); } - pub fn sort_column( - &mut self, - column_index: usize, - direction: SortDirection, - ) -> Result> { - let old = self.prepend_sort(column_index, direction.clone()); - - self.sort_all()?; - - Ok(old) - } - - pub fn sort_all(&mut self) -> Result<()> { - self.display_buffer = None; - let value = self.display_value()?.into_array()?; - let mut display_buffer = (0..value.height()).map(|i| i as u64).collect::>(); - - if let Some(ref mut sort) = self.sort.to_owned() { - for sort in sort - .iter() - .rev() - .filter(|s| s.direction != SortDirection::None) - { - display_buffer = display_buffer - .into_iter() - .skip(self.adjust_for_header(0)) - .map(|i| (i, value.get(sort.column_index as u32, i as u32).unwrap())) - .sorted_by(|a, b| match sort.direction { - SortDirection::Ascending => a.1.total_cmp(b.1), - SortDirection::Descending => b.1.total_cmp(a.1), - SortDirection::None => std::cmp::Ordering::Equal, - }) - .map(|(i, _)| i) - .collect::>(); - - if self.header_is_first_row { - display_buffer.insert(0, 0); - } - } - } - - self.display_buffer = Some(display_buffer); - - Ok(()) - } - - pub fn prepend_sort( - &mut self, - column_index: usize, - direction: SortDirection, - ) -> Option { - let data_table_sort = DataTableSort { - column_index, - direction, - }; - - let old = self.sort.as_mut().and_then(|sort| { - let index = sort - .iter() - .position(|sort| sort.column_index == column_index); - - index.and_then(|index| Some(sort.remove(index))) - }); - - match self.sort { - Some(ref mut sort) => sort.insert(0, data_table_sort), - None => self.sort = Some(vec![data_table_sort]), - } - - old - } - - pub fn display_value_from_buffer(&self, display_buffer: &Vec) -> Result { - let value = self.value.to_owned().into_array()?; - - let values = display_buffer - .iter() - .filter_map(|index| { - value - .get_row(*index as usize) - .map(|row| row.into_iter().cloned().collect::>()) - .ok() - }) - .collect::>>(); - - let array = Array::from(values); - - Ok(array.into()) - } - - pub fn display_value_from_buffer_at( - &self, - display_buffer: &Vec, - pos: Pos, - ) -> Result<&CellValue> { - let y = display_buffer - .get(pos.y as usize) - .ok_or_else(|| anyhow!("Y {} out of bounds: {}", pos.y, display_buffer.len()))?; - let cell_value = self.value.get(pos.x as u32, *y as u32)?; - - Ok(cell_value) - } - - pub fn display_value(&self) -> Result { - match self.display_buffer { - Some(ref display_buffer) => self.display_value_from_buffer(display_buffer), - None => Ok(self.value.to_owned()), - } - } - - pub fn display_value_at(&self, mut pos: Pos) -> Result<&CellValue> { - // println!("pos: {:?}", pos); - // println!("self.columns: {:?}", self.columns); - - if pos.y == 0 && self.show_header { - if let Some(columns) = &self.columns { - // println!("columns: {:?}", columns); - if let Some(column) = columns.get(pos.x as usize) { - // println!("column: {:?}", column); - return Ok(column.name.as_ref()); - } - } - } - - if !self.header_is_first_row && self.show_header { - pos.y = pos.y - 1; - } - - match self.display_buffer { - Some(ref display_buffer) => self.display_value_from_buffer_at(display_buffer, pos), - None => Ok(self.value.get(pos.x as u32, pos.y as u32)?), - } - } - /// Helper function to get the CodeRun from the DataTable. /// Returns `None` if the DataTableKind is not CodeRun. pub fn code_run(&self) -> Option<&CodeRun> { @@ -623,24 +322,6 @@ impl DataTable { format!("\nData Table: {title}\n{table}") } - - /// Prepares the columns to be sent to the client. If no columns are set, it - /// will create default columns. - pub fn send_columns(&self) -> Vec { - match self.columns.as_ref() { - Some(columns) => columns - .iter() - .map(|column| JsDataTableColumn::from(column.to_owned())) - .collect(), - // TODO(ddimaria): refacor this to use the default columns - None => { - let size = self.output_size(); - (0..size.w.get()) - .map(|i| DataTableColumn::new(format!("Column {}", i + 1), true, i).into()) - .collect::>() - } - } - } } #[cfg(test)] @@ -702,9 +383,9 @@ pub mod test { #[test] #[parallel] - fn test_import_data_table_and_headers() { + fn test_import_data_table() { // test data table without column headings - let (_, mut data_table) = new_data_table(); + let (_, data_table) = new_data_table(); let kind = data_table.kind.clone(); let values = data_table.value.clone().into_array().unwrap(); @@ -728,73 +409,6 @@ pub mod test { "Data Table: {:?}", data_table.display_value_at((0, 1).into()).unwrap() ); - - // test default column headings - data_table.apply_default_header(); - let expected_columns = vec![ - DataTableColumn::new("Column 1".into(), true, 0), - DataTableColumn::new("Column 2".into(), true, 1), - DataTableColumn::new("Column 3".into(), true, 2), - DataTableColumn::new("Column 4".into(), true, 3), - ]; - assert_eq!(data_table.columns, Some(expected_columns)); - - // test column headings taken from first row - let value = Value::Array(values.clone().into()); - let mut data_table = DataTable::new(kind.clone(), "Table 1", value, false, true, true) - .with_last_modified(data_table.last_modified); - - data_table.apply_first_row_as_header(); - let expected_columns = vec![ - DataTableColumn::new("city".into(), true, 0), - DataTableColumn::new("region".into(), true, 1), - DataTableColumn::new("country".into(), true, 2), - DataTableColumn::new("population".into(), true, 3), - ]; - assert_eq!(data_table.columns, Some(expected_columns)); - - let expected_values = values.clone(); - assert_eq!( - data_table.value.clone().into_array().unwrap(), - expected_values - ); - - // test setting header at index - data_table.set_header_at(0, "new".into(), true).unwrap(); - assert_eq!( - data_table.columns.as_ref().unwrap()[0].name, - CellValue::Text("new".into()) - ); - - // test setting header display at index - data_table.set_header_display_at(0, false).unwrap(); - assert_eq!(data_table.columns.as_ref().unwrap()[0].display, false); - } - - #[test] - #[parallel] - fn test_data_table_sort() { - let (_, mut data_table) = new_data_table(); - data_table.apply_first_row_as_header(); - - let values = test_csv_values(); - pretty_print_data_table(&data_table, Some("Original Data Table"), None); - - // sort by population city ascending - data_table.sort_column(0, SortDirection::Ascending).unwrap(); - pretty_print_data_table(&data_table, Some("Sorted by City"), None); - assert_data_table_row(&data_table, 1, values[2].clone()); - assert_data_table_row(&data_table, 2, values[3].clone()); - assert_data_table_row(&data_table, 3, values[1].clone()); - - // sort by population descending - data_table - .sort_column(3, SortDirection::Descending) - .unwrap(); - pretty_print_data_table(&data_table, Some("Sorted by Population Descending"), None); - assert_data_table_row(&data_table, 1, values[2].clone()); - assert_data_table_row(&data_table, 2, values[1].clone()); - assert_data_table_row(&data_table, 3, values[3].clone()); } #[test] @@ -903,50 +517,4 @@ pub mod test { SheetRect::new(1, 2, 10, 13, sheet_id) ); } - - #[test] - #[parallel] - fn test_headers_y() { - let mut sheet = Sheet::test(); - let array = Array::from_str_vec(vec![vec!["first"], vec!["second"]], true).unwrap(); - let pos = Pos { x: 1, y: 1 }; - let t = DataTable { - kind: DataTableKind::Import(Import::new("test.csv".to_string())), - name: "Table 1".into(), - columns: None, - sort: None, - display_buffer: None, - value: Value::Array(array), - readonly: false, - spill_error: false, - last_modified: Utc::now(), - show_header: true, - header_is_first_row: true, - alternating_colors: true, - }; - sheet.set_cell_value( - pos, - Some(CellValue::Import(Import::new("test.csv".to_string()))), - ); - sheet.set_data_table(pos, Some(t.clone())); - assert_eq!( - sheet.display_value(pos), - Some(CellValue::Text("first".into())) - ); - - let data_table = sheet.data_table_mut((1, 1).into()).unwrap(); - data_table.toggle_first_row_as_header(false); - assert_eq!( - sheet.display_value(pos), - Some(CellValue::Text("Column 1".into())) - ); - assert_eq!( - sheet.display_value(Pos { x: 1, y: 2 }), - Some(CellValue::Text("first".into())) - ); - assert_eq!( - sheet.display_value(Pos { x: 1, y: 3 }), - Some(CellValue::Text("second".into())) - ); - } } diff --git a/quadratic-core/src/grid/data_table/sort.rs b/quadratic-core/src/grid/data_table/sort.rs new file mode 100644 index 0000000000..f4c35ab44a --- /dev/null +++ b/quadratic-core/src/grid/data_table/sort.rs @@ -0,0 +1,134 @@ +//! + +use anyhow::{Ok, Result}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use super::DataTable; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +pub enum SortDirection { + #[serde(rename = "asc")] + Ascending, + #[serde(rename = "desc")] + Descending, + #[serde(rename = "none")] + None, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +pub struct DataTableSort { + pub column_index: usize, + pub direction: SortDirection, +} + +impl DataTable { + pub fn sort_column( + &mut self, + column_index: usize, + direction: SortDirection, + ) -> Result> { + let old = self.prepend_sort(column_index, direction.clone()); + + self.sort_all()?; + + Ok(old) + } + + pub fn sort_all(&mut self) -> Result<()> { + self.display_buffer = None; + let value = self.display_value()?.into_array()?; + let mut display_buffer = (0..value.height()).map(|i| i as u64).collect::>(); + + if let Some(ref mut sort) = self.sort.to_owned() { + for sort in sort + .iter() + .rev() + .filter(|s| s.direction != SortDirection::None) + { + display_buffer = display_buffer + .into_iter() + .skip(self.adjust_for_header(0)) + .map(|i| (i, value.get(sort.column_index as u32, i as u32).unwrap())) + .sorted_by(|a, b| match sort.direction { + SortDirection::Ascending => a.1.total_cmp(b.1), + SortDirection::Descending => b.1.total_cmp(a.1), + SortDirection::None => std::cmp::Ordering::Equal, + }) + .map(|(i, _)| i) + .collect::>(); + + if self.header_is_first_row { + display_buffer.insert(0, 0); + } + } + } + + self.display_buffer = Some(display_buffer); + + Ok(()) + } + + pub fn prepend_sort( + &mut self, + column_index: usize, + direction: SortDirection, + ) -> Option { + let data_table_sort = DataTableSort { + column_index, + direction, + }; + + let old = self.sort.as_mut().and_then(|sort| { + let index = sort + .iter() + .position(|sort| sort.column_index == column_index); + + index.and_then(|index| Some(sort.remove(index))) + }); + + match self.sort { + Some(ref mut sort) => sort.insert(0, data_table_sort), + None => self.sort = Some(vec![data_table_sort]), + } + + old + } +} + +#[cfg(test)] +pub mod test { + + use super::*; + use crate::grid::test::{ + assert_data_table_row, new_data_table, pretty_print_data_table, test_csv_values, + }; + use serial_test::parallel; + + #[test] + #[parallel] + fn test_data_table_sort() { + let (_, mut data_table) = new_data_table(); + data_table.apply_first_row_as_header(); + + let values = test_csv_values(); + pretty_print_data_table(&data_table, Some("Original Data Table"), None); + + // sort by population city ascending + data_table.sort_column(0, SortDirection::Ascending).unwrap(); + pretty_print_data_table(&data_table, Some("Sorted by City"), None); + assert_data_table_row(&data_table, 1, values[2].clone()); + assert_data_table_row(&data_table, 2, values[3].clone()); + assert_data_table_row(&data_table, 3, values[1].clone()); + + // sort by population descending + data_table + .sort_column(3, SortDirection::Descending) + .unwrap(); + pretty_print_data_table(&data_table, Some("Sorted by Population Descending"), None); + assert_data_table_row(&data_table, 1, values[2].clone()); + assert_data_table_row(&data_table, 2, values[1].clone()); + assert_data_table_row(&data_table, 3, values[3].clone()); + } +} diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index ff579696cf..1e0619a0a5 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -4,7 +4,11 @@ use indexmap::IndexMap; use itertools::Itertools; use crate::{ - grid::{CodeRun, DataTable, DataTableColumn, DataTableKind, DataTableSort, SortDirection}, + grid::{ + data_table::column::DataTableColumn, + data_table::sort::{DataTableSort, SortDirection}, + CodeRun, DataTable, DataTableKind, + }, ArraySize, Axis, Pos, RunError, RunErrorMsg, Value, }; diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index 90fc8eecdb..e267515bee 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -4,10 +4,11 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use uuid::Uuid; +use super::data_table::{column::DataTableColumn, sort::DataTableSort}; use super::formats::format::Format; use super::formatting::{CellAlign, CellVerticalAlign, CellWrap}; use super::sheet::validations::validation::ValidationStyle; -use super::{CodeCellLanguage, DataTableColumn, DataTableSort, NumericFormat}; +use super::{CodeCellLanguage, NumericFormat}; use crate::{Pos, SheetRect}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)] diff --git a/quadratic-core/src/grid/mod.rs b/quadratic-core/src/grid/mod.rs index b7b49bc290..512bf0edce 100644 --- a/quadratic-core/src/grid/mod.rs +++ b/quadratic-core/src/grid/mod.rs @@ -26,7 +26,7 @@ mod block; mod borders; mod bounds; mod code_run; -mod column; +pub mod column; pub mod data_table; pub mod file; pub mod formats; diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index 64bf73544d..09416346ec 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -1,4 +1,5 @@ use selection::Selection; +use sort::DataTableSort; use super::*; @@ -69,8 +70,6 @@ impl GridController { ); } - // let sort = sort.map(|s| serde_json::from_str::>(&s)?); - dbgjs!(&sort); self.sort_data_table(pos.to_sheet_pos(sheet_id), sort, cursor); Ok(()) From 82c8803658ca43b37aaff081fac0586b5622b27c Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 05:08:41 -0700 Subject: [PATCH 133/373] multisort --- .../HTMLGrid/contextMenus/tableSort/TableSort.tsx | 12 ++++++++---- quadratic-core/src/grid/data_table/sort.rs | 3 --- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index 1a904c7905..28c5bb4b10 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -2,9 +2,11 @@ import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; import { TableSortEntry } from '@/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { DataTableSort, SortDirection } from '@/app/quadratic-core-types'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Button } from '@/shared/shadcn/ui/button'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useRecoilState } from 'recoil'; @@ -45,11 +47,13 @@ export const TableSort = () => { const handleSave = useCallback(() => { if (contextMenu.table) { const sortToSend = sort.filter((item) => item.direction !== 'None' && item.column_index !== -1); - console.log( - `Sending table ${contextMenu.table.x},${contextMenu.table.y} with sort ${JSON.stringify(sortToSend)}` + quadraticCore.sortDataTable( + sheets.sheet.id, + contextMenu.table.x, + contextMenu.table.y, + sortToSend, + sheets.getCursorPosition() ); - // todo: need a fn to send the entire sorted data table - // quadraticCore.sortDataTable(contextMenu.table, sort); } handleClose(); }, [contextMenu.table, sort, handleClose]); diff --git a/quadratic-core/src/grid/data_table/sort.rs b/quadratic-core/src/grid/data_table/sort.rs index f4c35ab44a..b6c7b62076 100644 --- a/quadratic-core/src/grid/data_table/sort.rs +++ b/quadratic-core/src/grid/data_table/sort.rs @@ -9,11 +9,8 @@ use super::DataTable; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub enum SortDirection { - #[serde(rename = "asc")] Ascending, - #[serde(rename = "desc")] Descending, - #[serde(rename = "none")] None, } From c8c99f286893fe7b11568ab0e82268a04b012701 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 05:23:52 -0700 Subject: [PATCH 134/373] sort works better --- .../gridGL/cells/tables/TableColumnHeader.ts | 46 ++++++++++----- .../gridGL/cells/tables/TableColumnHeaders.ts | 56 +++++++++++-------- 2 files changed, 64 insertions(+), 38 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index e0656ac1f8..4d158be9c4 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -66,6 +66,17 @@ export class TableColumnHeader extends Container { this.columnName.position.set(OPEN_SANS_FIX.x, OPEN_SANS_FIX.y); } + // Called when the CodeCell is updated + updateHeader(x: number, width: number, height: number, name: string, sort?: DataTableSort) { + this.columnHeaderBounds = new Rectangle(this.table.tableBounds.x + x, this.table.tableBounds.y, width, height); + this.w = width; + this.h = height; + this.position.set(x, 0); + this.columnName.text = name; + this.clipName(name, width); + this.updateSortButton(width, height, sort); + } + // tests the width of the text and clips it if it is too wide private clipName(name: string, width: number) { let clippedName = name; @@ -84,22 +95,29 @@ export class TableColumnHeader extends Container { this.sortButton.position.set(width - SORT_BUTTON_RADIUS - SORT_BUTTON_PADDING, height / 2); this.sortButton.visible = false; - if (sort) { - let texture: Texture; - if (sort.direction === 'Descending') { - texture = Texture.from('arrow-up'); - } else if (sort.direction === 'Ascending') { - texture = Texture.from('arrow-down'); - } else { - texture = Texture.EMPTY; - } + const texture = sort ? Texture.from(sort.direction === 'Descending' ? 'arrow-up' : 'arrow-down') : Texture.EMPTY; + this.sortIcon = this.addChild(new Sprite(texture)); + this.sortIcon.anchor.set(0.5); + this.sortIcon.position = this.sortButton.position; + this.sortIcon.width = SORT_ICON_SIZE; + this.sortIcon.scale.y = this.sortIcon.scale.x; + } - this.sortIcon = this.addChild(new Sprite(texture)); - this.sortIcon.anchor.set(0.5); - this.sortIcon.position = this.sortButton.position; - this.sortIcon.width = SORT_ICON_SIZE; - this.sortIcon.scale.y = this.sortIcon.scale.x; + private updateSortButton(width: number, height: number, sort?: DataTableSort) { + this.sortButtonStart = this.columnHeaderBounds.right - SORT_BUTTON_RADIUS - SORT_BUTTON_PADDING; + if (!this.sortButton) { + throw new Error('Expected sortButton to be defined in updateSortButton'); + } + this.sortButton.position.set(width - SORT_BUTTON_RADIUS - SORT_BUTTON_PADDING, height / 2); + if (!this.sortIcon) { + throw new Error('Expected sortIcon to be defined in updateSortButton'); } + this.sortIcon.position = this.sortButton.position; + this.sortIcon.texture = sort + ? Texture.from(sort.direction === 'Descending' ? 'arrow-up' : 'arrow-down') + : Texture.EMPTY; + this.sortIcon.width = SORT_ICON_SIZE; + this.sortIcon.scale.y = this.sortIcon.scale.x; } pointerMove(world: Point): boolean { diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 7eee3d8943..c2258cbe02 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -35,59 +35,67 @@ export class TableColumnHeaders extends Container { }; private onSortPressed(column: JsDataTableColumn) { - // todo: once Rust is fixed, this should be the SortDirection enum const sortOrder: SortDirection | undefined = this.table.codeCell.sort?.find( (s) => s.column_index === column.valueIndex )?.direction; - let newOrder: 'asc' | 'desc' | 'none' = 'none'; + let newOrder: SortDirection; switch (sortOrder) { case undefined: case 'None': - newOrder = 'asc'; + newOrder = 'Ascending'; break; case 'Ascending': - newOrder = 'desc'; + newOrder = 'Descending'; break; case 'Descending': - newOrder = 'none'; + newOrder = 'None'; break; } if (!newOrder) { throw new Error('Unknown sort order in onSortPressed'); } const table = this.table.codeCell; - - quadraticCore.sortDataTable( - sheets.sheet.id, - table.x, - table.y, - [{ column_index: column.valueIndex, direction: newOrder }], - sheets.getCursorPosition() - ); + const sort = newOrder === 'None' ? [] : [{ column_index: column.valueIndex, direction: newOrder }]; + quadraticCore.sortDataTable(sheets.sheet.id, table.x, table.y, sort, sheets.getCursorPosition()); } private createColumnHeaders() { - this.columns.removeChildren(); if (!this.table.codeCell.show_header) { this.columns.visible = false; return; } + while (this.columns.children.length > this.table.codeCell.column_names.length) { + this.columns.children.pop(); + } let x = 0; const codeCell = this.table.codeCell; codeCell.column_names.forEach((column, index) => { const width = this.table.sheet.offsets.getColumnWidth(codeCell.x + index); - this.columns.addChild( - new TableColumnHeader({ - table: this.table, - index, + if (index >= this.columns.children.length) { + // if this is a new column, then add it + this.columns.addChild( + new TableColumnHeader({ + table: this.table, + index, + x, + width, + height: this.headerHeight, + name: column.name, + sort: codeCell.sort?.find((s) => s.column_index === column.valueIndex), + onSortPressed: () => this.onSortPressed(column), + }) + ); + } else { + // otherwise, update the existing column header (this is needed to keep + // the sort button hover working properly) + this.columns.children[index].updateHeader( x, width, - height: this.headerHeight, - name: column.name, - sort: codeCell.sort?.find((s) => s.column_index === column.valueIndex), - onSortPressed: () => this.onSortPressed(column), - }) - ); + this.height, + column.name, + codeCell.sort?.find((s) => s.column_index === column.valueIndex) + ); + } x += width; }); } From 5e6c010e81564b2ab689ddd92ca87af67263d027 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 05:43:07 -0700 Subject: [PATCH 135/373] don't select the table title if the table is not active and the cursor is in the row above the table --- .../src/app/gridGL/cells/CellsArray.ts | 25 ++++++++++--------- .../src/app/gridGL/cells/CellsFills.ts | 3 +-- .../src/app/gridGL/cells/tables/Table.ts | 6 ++--- .../src/app/gridGL/cells/tables/Tables.ts | 21 +++++++++++++--- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts index 8c501ec5d7..02ae5260c8 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsArray.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsArray.ts @@ -1,3 +1,5 @@ +//! Holds borders for tables and code errors. + import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; @@ -10,7 +12,6 @@ import { colors } from '../../theme/colors'; import { generatedTextures } from '../generateTextures'; import { intersects } from '../helpers/intersects'; import { pixiApp } from '../pixiApp/PixiApp'; -import { pixiAppSettings } from '../pixiApp/PixiAppSettings'; import { CellsSheet } from './CellsSheet'; import { BorderCull, borderLineWidth, drawBorder, drawLine } from './drawBorders'; @@ -157,17 +158,17 @@ export class CellsArray extends Container { tint = colors.cellColorUserJavascript; } - if (!pixiAppSettings.showCellTypeOutlines) { - // only show the entire array if the cursor overlaps any part of the output - if (!intersects.rectanglePoint(overlapTest, new Point(cursor.x, cursor.y))) { - this.cellsSheet.cellsMarkers.add(start, codeCell, false); - return; - } - } - - if (!editingCell) { - this.cellsSheet.cellsMarkers.add(start, codeCell, true); - } + // if (!pixiAppSettings.showCellTypeOutlines) { + // // only show the entire array if the cursor overlaps any part of the output + // if (!intersects.rectanglePoint(overlapTest, new Point(cursor.x, cursor.y))) { + // this.cellsSheet.cellsMarkers.add(start, codeCell, false); + // return; + // } + // } + + // if (!editingCell) { + // this.cellsSheet.cellsMarkers.add(start, codeCell, true); + // } const end = this.sheet.getCellOffsets(Number(codeCell.x) + codeCell.w, Number(codeCell.y) + codeCell.h); if (codeCell.spill_error) { const cursorPosition = sheets.sheet.cursor.cursorPosition; diff --git a/quadratic-client/src/app/gridGL/cells/CellsFills.ts b/quadratic-client/src/app/gridGL/cells/CellsFills.ts index 23d81b2ef9..287f69a27d 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsFills.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsFills.ts @@ -36,12 +36,11 @@ export class CellsFills extends Container { super(); this.cellsSheet = cellsSheet; this.meta = this.addChild(new Graphics()); + this.alternatingColorsGraphics = this.addChild(new Graphics()); this.cellsContainer = this.addChild( new ParticleContainer(undefined, { vertices: true, tint: true }, undefined, true) ); - this.alternatingColorsGraphics = this.addChild(new Graphics()); - events.on('sheetFills', (sheetId, fills) => { if (sheetId === this.cellsSheet.sheetId) { this.cells = fills; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 68051bc502..49ba4f3fea 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -203,14 +203,14 @@ export class Table extends Container { return this.columnHeaders.getColumnHeaderBounds(index); } - pointerMove(world: Point): boolean { + pointerMove(world: Point): 'table-name' | boolean { const name = this.tableName.intersects(world); if (name?.type === 'dropdown') { this.tableCursor = 'pointer'; - return true; + return 'table-name'; } else if (name?.type === 'table-name') { this.tableCursor = undefined; - return true; + return 'table-name'; } const result = this.columnHeaders.pointerMove(world); if (result) { diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index a209302117..81d91a1dce 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -153,16 +153,21 @@ export class Tables extends Container
{ } } + private isTableActive(table: Table): boolean { + return this.activeTable === table || this.hoverTable === table || this.contextMenuTable === table; + } + // Returns true if the pointer down as handled (eg, a column header was - // clicked). Otherwise it handles TableName. + // clicked). Otherwise it handles TableName. We ignore the table name if the + // table is not active to allow the user to select the row above the table. pointerDown(world: Point): TablePointerDownResult | undefined { for (const table of this.children) { const result = table.intersectsTableName(world); - if (result) { + if (result && (result.type !== 'table-name' || this.isTableActive(table))) { return result; } const columnName = table.pointerDown(world); - if (columnName) { + if (columnName && columnName.type !== 'table-name') { return columnName; } } @@ -170,8 +175,16 @@ export class Tables extends Container
{ pointerMove(world: Point): boolean { for (const table of this.children) { - if (table.pointerMove(world)) { + const result = table.pointerMove(world); + if (result) { this.tableCursor = table.tableCursor; + // we don't make the title active unless we're already hovering over + // it--this ensures that the user can select the row above the table + // when the table is not active + if (result !== 'table-name' && !this.isTableActive(table)) { + this.hoverTable = table; + table.showActive(); + } return true; } } From 7ad35e345bb6fb2f41d0bd4ff2a34905f6444ff5 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 05:49:58 -0700 Subject: [PATCH 136/373] simplify CellsArray (the underlying border around code cells) --- .../src/app/gridGL/cells/CellsArray.ts | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts index 02ae5260c8..b9ed58d0d8 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsArray.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsArray.ts @@ -5,6 +5,7 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; import { Coordinate } from '@/app/gridGL/types/size'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { JsCodeCell, JsRenderCodeCell, RunError } from '@/app/quadratic-core-types'; import mixpanel from 'mixpanel-browser'; import { Container, Graphics, ParticleContainer, Point, Rectangle, Sprite, Texture } from 'pixi.js'; @@ -13,7 +14,7 @@ import { generatedTextures } from '../generateTextures'; import { intersects } from '../helpers/intersects'; import { pixiApp } from '../pixiApp/PixiApp'; import { CellsSheet } from './CellsSheet'; -import { BorderCull, borderLineWidth, drawBorder, drawLine } from './drawBorders'; +import { BorderCull, drawBorder } from './drawBorders'; const SPILL_HIGHLIGHT_THICKNESS = 1; const SPILL_HIGHLIGHT_COLOR = colors.cellColorError; @@ -149,14 +150,17 @@ export class CellsArray extends Container { overlapTest.height = 1; } - let tint = colors.independence; - if (codeCell.language === 'Python') { - tint = colors.cellColorUserPython; - } else if (codeCell.language === 'Formula') { - tint = colors.cellColorUserFormula; - } else if (codeCell.language === 'Javascript') { - tint = colors.cellColorUserJavascript; - } + const tint = getCSSVariableTint('primary'); + + // old code that draws a box around the code cell + // let tint = colors.independence; + // if (codeCell.language === 'Python') { + // tint = colors.cellColorUserPython; + // } else if (codeCell.language === 'Formula') { + // tint = colors.cellColorUserFormula; + // } else if (codeCell.language === 'Javascript') { + // tint = colors.cellColorUserJavascript; + // } // if (!pixiAppSettings.showCellTypeOutlines) { // // only show the entire array if the cursor overlaps any part of the output @@ -169,6 +173,7 @@ export class CellsArray extends Container { // if (!editingCell) { // this.cellsSheet.cellsMarkers.add(start, codeCell, true); // } + const end = this.sheet.getCellOffsets(Number(codeCell.x) + codeCell.w, Number(codeCell.y) + codeCell.h); if (codeCell.spill_error) { const cursorPosition = sheets.sheet.cursor.cursorPosition; @@ -225,34 +230,34 @@ export class CellsArray extends Container { right: true, }) ); - const right = end.x !== start.x + start.width; - if (right) { - this.lines.push( - drawLine({ - x: start.x + start.width - borderLineWidth / 2, - y: start.y + borderLineWidth / 2, - width: borderLineWidth, - height: start.height, - alpha: 0.5, - tint, - getSprite: this.getSprite, - }) - ); - } - const bottom = end.y !== start.y + start.height; - if (bottom) { - this.lines.push( - drawLine({ - x: start.x + borderLineWidth / 2, - y: start.y + start.height - borderLineWidth / 2, - width: start.width - borderLineWidth, - height: borderLineWidth, - alpha: 0.5, - tint, - getSprite: this.getSprite, - }) - ); - } + // const right = end.x !== start.x + start.width; + // if (right) { + // this.lines.push( + // drawLine({ + // x: start.x + start.width - borderLineWidth / 2, + // y: start.y + borderLineWidth / 2, + // width: borderLineWidth, + // height: start.height, + // alpha: 0.5, + // tint, + // getSprite: this.getSprite, + // }) + // ); + // } + // const bottom = end.y !== start.y + start.height; + // if (bottom) { + // this.lines.push( + // drawLine({ + // x: start.x + borderLineWidth / 2, + // y: start.y + start.height - borderLineWidth / 2, + // width: start.width - borderLineWidth, + // height: borderLineWidth, + // alpha: 0.5, + // tint, + // getSprite: this.getSprite, + // }) + // ); + // } } private drawDashedRectangle(rectangle: Rectangle, color: number) { From 6c76e69df797e28443ef2302fb77b355baef7de2 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 06:54:31 -0700 Subject: [PATCH 137/373] better spill errors --- .../src/app/gridGL/cells/CellsArray.ts | 70 ++----------------- .../src/app/gridGL/cells/tables/Table.ts | 4 +- .../app/gridGL/cells/tables/TableOutline.ts | 59 ++++++++++++++++ 3 files changed, 67 insertions(+), 66 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts index b9ed58d0d8..cda7f2e8ea 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsArray.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsArray.ts @@ -9,23 +9,18 @@ import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { JsCodeCell, JsRenderCodeCell, RunError } from '@/app/quadratic-core-types'; import mixpanel from 'mixpanel-browser'; import { Container, Graphics, ParticleContainer, Point, Rectangle, Sprite, Texture } from 'pixi.js'; -import { colors } from '../../theme/colors'; -import { generatedTextures } from '../generateTextures'; import { intersects } from '../helpers/intersects'; import { pixiApp } from '../pixiApp/PixiApp'; import { CellsSheet } from './CellsSheet'; import { BorderCull, drawBorder } from './drawBorders'; -const SPILL_HIGHLIGHT_THICKNESS = 1; -const SPILL_HIGHLIGHT_COLOR = colors.cellColorError; -const SPILL_FILL_ALPHA = 0.025; - export class CellsArray extends Container { private cellsSheet: CellsSheet; private codeCells: Map; private tables: Map; private particles: ParticleContainer; + // only used for the spill error indicators (lines are drawn using sprites in particles for performance) private graphics: Graphics; private lines: BorderCull[]; @@ -174,35 +169,11 @@ export class CellsArray extends Container { // this.cellsSheet.cellsMarkers.add(start, codeCell, true); // } - const end = this.sheet.getCellOffsets(Number(codeCell.x) + codeCell.w, Number(codeCell.y) + codeCell.h); - if (codeCell.spill_error) { - const cursorPosition = sheets.sheet.cursor.cursorPosition; - if (cursorPosition.x !== Number(codeCell.x) || cursorPosition.y !== Number(codeCell.y)) { - this.lines.push( - ...drawBorder({ - alpha: 0.5, - tint, - x: start.x, - y: start.y, - width: start.width, - height: start.height, - getSprite: this.getSprite, - top: true, - left: true, - bottom: true, - right: true, - }) - ); - } else { - this.drawDashedRectangle(new Rectangle(start.x, start.y, end.x - start.x, end.y - start.y), tint); - codeCell.spill_error?.forEach((error) => { - const rectangle = this.sheet.getCellOffsets(Number(error.x), Number(error.y)); - this.drawDashedRectangle(rectangle, SPILL_HIGHLIGHT_COLOR); - }); - } - } else { - this.drawBox(start, end, tint); - } + const end = this.sheet.getCellOffsets( + Number(codeCell.x) + (codeCell.spill_error ? 1 : codeCell.w), + Number(codeCell.y) + (codeCell.spill_error ? 1 : codeCell.h) + ); + this.drawBox(start, end, tint); // save the entire table for hover checks if (!codeCell.spill_error) { @@ -260,35 +231,6 @@ export class CellsArray extends Container { // } } - private drawDashedRectangle(rectangle: Rectangle, color: number) { - this.graphics.lineStyle(); - this.graphics.beginFill(color, SPILL_FILL_ALPHA); - this.graphics.drawRect(rectangle.left, rectangle.top, rectangle.width, rectangle.height); - this.graphics.endFill(); - - const minX = rectangle.left; - const minY = rectangle.top; - const maxX = rectangle.right; - const maxY = rectangle.bottom; - - const path = [ - [maxX, minY], - [maxX, maxY], - [minX, maxY], - [minX, minY], - ]; - - this.graphics.moveTo(minX, minY); - for (let i = 0; i < path.length; i++) { - this.graphics.lineStyle({ - width: SPILL_HIGHLIGHT_THICKNESS, - color, - texture: i % 2 === 0 ? generatedTextures.dashedHorizontal : generatedTextures.dashedVertical, - }); - this.graphics.lineTo(path[i][0], path[i][1]); - } - } - private getSprite = (): Sprite => { return this.particles.addChild(new Sprite(Texture.WHITE)); }; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 49ba4f3fea..8fd268a439 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -50,8 +50,8 @@ export class Table extends Container { this.tableBounds = this.sheet.getScreenRectangle( this.codeCell.x, this.codeCell.y, - this.codeCell.w - 1, - this.codeCell.h - 1 + this.codeCell.spill_error ? 0 : this.codeCell.w - 1, + this.codeCell.spill_error ? 0 : this.codeCell.h - 1 ); this.position.set(this.tableBounds.x, this.tableBounds.y); diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts index dd1e953d5f..ce783cc218 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts @@ -1,7 +1,15 @@ +//! Draws a table outline, including the spill error boundaries. + +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Table } from '@/app/gridGL/cells/tables/Table'; +import { generatedTextures } from '@/app/gridGL/generateTextures'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; +import { colors } from '@/app/theme/colors'; import { Graphics, Rectangle } from 'pixi.js'; +const SPILL_HIGHLIGHT_THICKNESS = 2; +const SPILL_FILL_ALPHA = 0.05; + export class TableOutline extends Graphics { private table: Table; @@ -12,8 +20,59 @@ export class TableOutline extends Graphics { update() { this.clear(); + + // draw the table selected outline this.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); this.drawShape(new Rectangle(0, 0, this.table.tableBounds.width, this.table.tableBounds.height)); + + // draw the spill error boundaries + if (this.table.codeCell.spill_error) { + const full = this.table.sheet.getScreenRectangle( + Number(this.table.codeCell.x), + Number(this.table.codeCell.y), + this.table.codeCell.w - 1, + this.table.codeCell.h - 1 + ); + + // draw outline around where the code cell would spill + this.lineStyle({ color: getCSSVariableTint('primary'), width: 1, alignment: 0 }); + this.drawRect(0, 0, full.width, full.height); + + // box and shade what is causing the spill errors + this.table.codeCell.spill_error.forEach((error) => { + const rectangle = this.table.sheet.getCellOffsets(Number(error.x), Number(error.y)); + this.drawDashedRectangle(rectangle, colors.cellColorError); + }); + } this.visible = false; } + + // draw a dashed and filled rectangle to identify the cause of the spill error + private drawDashedRectangle(rectangle: Rectangle, color: number) { + const minX = rectangle.left - this.table.tableBounds.x; + const minY = rectangle.top - this.table.tableBounds.y; + const maxX = rectangle.right - this.table.tableBounds.x; + const maxY = rectangle.bottom - this.table.tableBounds.y; + + const path = [ + [maxX, minY], + [maxX, maxY], + [minX, maxY], + [minX, minY], + ]; + + this.moveTo(minX, minY); + for (let i = 0; i < path.length; i++) { + this.lineStyle({ + width: SPILL_HIGHLIGHT_THICKNESS, + color, + texture: i % 2 === 0 ? generatedTextures.dashedHorizontal : generatedTextures.dashedVertical, + }); + this.lineTo(path[i][0], path[i][1]); + } + this.lineStyle(); + this.beginFill(colors.cellColorError, SPILL_FILL_ALPHA); + this.drawRect(minX, minY, maxX - minX, maxY - minY); + this.endFill(); + } } From 3d339afb450adf3242e0426ba9d2595f88d64169 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 07:06:36 -0700 Subject: [PATCH 138/373] fix rename table bug --- .../src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 0fdd697c79..2bcdb44836 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -46,7 +46,9 @@ export const TableContextMenu = () => { transform: `scale(${1 / pixiApp.viewport.scale.x})`, pointerEvents: 'auto', display: - contextMenu.type === ContextMenuType.Table && contextMenu.selectedColumn === undefined ? 'block' : 'none', + contextMenu.type === ContextMenuType.Table && contextMenu.selectedColumn === undefined && !contextMenu.rename + ? 'block' + : 'none', }} > Date: Fri, 25 Oct 2024 07:10:36 -0700 Subject: [PATCH 139/373] fix bug with hover table --- quadratic-client/src/app/gridGL/cells/tables/Tables.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 81d91a1dce..43d835f741 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -182,6 +182,9 @@ export class Tables extends Container
{ // it--this ensures that the user can select the row above the table // when the table is not active if (result !== 'table-name' && !this.isTableActive(table)) { + if (this.hoverTable !== table) { + this.hoverTable?.hideActive(); + } this.hoverTable = table; table.showActive(); } From 84c4996bbceec311cd6791ce8c5bc390af1e8f1e Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 25 Oct 2024 08:23:11 -0600 Subject: [PATCH 140/373] Test for not adding numbers on unique names --- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../execute_operation/execute_data_table.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 600438d05c..f0a803e230 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -63,7 +63,7 @@ export interface SheetId { id: string, } export interface SheetInfo { sheet_id: string, name: string, order: string, color: string | null, offsets: string, bounds: GridBounds, bounds_without_formatting: GridBounds, } export interface SheetPos { x: bigint, y: bigint, sheet_id: SheetId, } export interface SheetRect { min: Pos, max: Pos, sheet_id: SheetId, } -export type SortDirection = "Ascending" | "Descending" | "None"; +export type SortDirection = "asc" | "desc" | "none"; export interface DataTableSort { column_index: number, direction: SortDirection, } export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 55543f1924..b1b8ce22d7 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -754,6 +754,17 @@ mod tests { gc.execute_update_data_table_name(&mut transaction, op) .unwrap(); let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); - assert_eq!(&data_table.name, "My Table1") + assert_eq!(&data_table.name, "My Table1"); + + // ensure numbers aren't added for unique names + let op = Operation::UpdateDataTableName { + sheet_pos, + name: "ABC".into(), + }; + let mut transaction = PendingTransaction::default(); + gc.execute_update_data_table_name(&mut transaction, op) + .unwrap(); + let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); + assert_eq!(&data_table.name, "ABC"); } } From 79f5e82b63581adc743e1bdc8a143d131b78c4b7 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 08:35:16 -0700 Subject: [PATCH 141/373] fix bugs with table sort dialog --- .../contextMenus/tableSort/TableSort.tsx | 22 +++++++++++------- .../contextMenus/tableSort/TableSortEntry.tsx | 23 +++++++++++++------ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index 28c5bb4b10..d631b1a172 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -73,6 +73,8 @@ export const TableSort = () => { ref.current.style.display = 'block'; setDisplay('block'); } + } else { + setDisplay('none'); } }; const viewportChanged = () => { @@ -96,20 +98,24 @@ export const TableSort = () => { return availableColumns.map((column) => column.name); }, [columnNames, sort]); - const handleChange = (index: number, column: string, direction: SortDirection) => { + const handleChange = (index: number, column: string | undefined, direction: SortDirection) => { setSort((prev) => { const columnIndex = columnNames.findIndex((c) => c.name === column); - if (columnIndex === -1) return prev; - // remove new entry from old sort - const newSort = [...prev.filter((value) => value.column_index !== -1)]; + // remove any additional entries + const newSort = [...prev.filter((item) => item.column_index !== -1)]; - if (index === -1) { - newSort.push({ column_index: columnIndex, direction }); + if (columnIndex === -1) { + newSort.splice(index, 1); } else { - newSort[index] = { column_index: columnIndex, direction }; + if (index === -1) { + newSort.push({ column_index: columnIndex, direction }); + } else { + newSort[index] = { column_index: columnIndex, direction }; + } } - if (sort.length !== columnNames.length) { + + if (newSort.length !== columnNames.length) { newSort.push({ column_index: -1, direction: 'Ascending' }); } return newSort; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx index 5edbd4ff48..13fe46c823 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx @@ -10,7 +10,7 @@ interface Props { availableColumns: string[]; name: string; direction: SortDirection; - onChange: (index: number, column: string, direction: SortDirection) => void; + onChange: (index: number, column: string | undefined, direction: SortDirection) => void; onDelete: (index: number) => void; onReorder: (index: number, direction: 'up' | 'down') => void; last: boolean; @@ -20,18 +20,27 @@ export const TableSortEntry = (props: Props) => { const { index, availableColumns, direction, name, onChange, onDelete, onReorder, last } = props; const [newColumn, setNewColumn] = useState(name); - const [newDirection, setNewDirection] = useState(direction ?? 'Descending'); + const [newDirection, setNewDirection] = useState(direction ?? 'Ascending'); const updateValues = useCallback( (column?: string, direction?: string) => { + if (column === 'blank') { + column = ''; + } if (column !== undefined) setNewColumn(column); if (direction !== undefined) setNewDirection(direction as SortDirection); // only update if the new column and direction are valid - if (column || newColumn) { - console.log(column, newColumn); - onChange(index, column ?? newColumn!, (direction as SortDirection) ?? newDirection!); - } + onChange( + index, + column === undefined ? newColumn ?? undefined : column, + (direction as SortDirection) ?? newDirection + ); + console.log( + index, + column === undefined ? newColumn ?? '' : column, + (direction as SortDirection) ?? newDirection! + ); }, [index, onChange, newColumn, newDirection] ); @@ -39,7 +48,7 @@ export const TableSortEntry = (props: Props) => { return (
Date: Fri, 25 Oct 2024 08:37:07 -0700 Subject: [PATCH 142/373] fix table name bug --- .../src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx index 8f79d2d9a2..83f35bdbe6 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx @@ -44,6 +44,7 @@ export const PixiRename = (props: Props) => { const saveAndClose = useCallback(() => { if (closed.current === true) return; + closed.current = true; if (ref.current?.value !== defaultValue && validate(ref.current?.value ?? '')) { onSave(ref.current?.value ?? ''); } From 081ead7153ac6297848001b3bdb1367e6e4ab6cb Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 25 Oct 2024 09:44:26 -0600 Subject: [PATCH 143/373] Start DataTableMeta operation --- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../execute_operation/execute_data_table.rs | 71 +++++++++++++++++++ .../execution/execute_operation/mod.rs | 3 + .../src/controller/operations/operation.rs | 20 +++++- 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index f0a803e230..600438d05c 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -63,7 +63,7 @@ export interface SheetId { id: string, } export interface SheetInfo { sheet_id: string, name: string, order: string, color: string | null, offsets: string, bounds: GridBounds, bounds_without_formatting: GridBounds, } export interface SheetPos { x: bigint, y: bigint, sheet_id: SheetId, } export interface SheetRect { min: Pos, max: Pos, sheet_id: SheetId, } -export type SortDirection = "asc" | "desc" | "none"; +export type SortDirection = "Ascending" | "Descending" | "None"; export interface DataTableSort { column_index: number, direction: SortDirection, } export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index b1b8ce22d7..0911283466 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -365,6 +365,77 @@ impl GridController { bail!("Expected Operation::UpdateDataTableName in execute_update_data_table_name"); } + pub(super) fn execute_data_table_meta( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::DataTableMeta { + sheet_pos, + ref name, + ref alternating_colors, + ref columns, + } = op + { + // get unique name first since it requires an immutable reference to the grid + let unique_name = name + .as_ref() + .map(|name| self.grid.unique_data_table_name(name, false)); + + let sheet_id = sheet_pos.sheet_id; + let sheet = self.try_sheet_mut_result(sheet_id)?; + let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; + let data_table = sheet.data_table_mut(data_table_pos)?; + + let old_name = unique_name.map(|name| { + let old_name = data_table.name.to_owned(); + data_table.name = name; + + old_name + }); + + let old_alternating_colors = alternating_colors.map(|alternating_colors| { + let old_alternating_colors = data_table.alternating_colors.to_owned(); + data_table.alternating_colors = alternating_colors; + + old_alternating_colors + }); + + let old_columns = columns.as_ref().and_then(|columns| { + let old_columns = data_table.columns.to_owned(); + data_table.columns = Some(columns.to_owned()); + + old_columns + }); + + let data_table_rect = data_table + .output_rect(sheet_pos.into(), true) + .to_sheet_rect(sheet_id); + + self.send_to_wasm(transaction, &data_table_rect)?; + transaction.add_code_cell(sheet_id, data_table_pos.into()); + + let forward_operations = vec![op]; + let reverse_operations = vec![Operation::DataTableMeta { + sheet_pos, + name: old_name, + alternating_colors: old_alternating_colors, + columns: old_columns, + }]; + + self.data_table_operations( + transaction, + &data_table_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::DataTableMeta in execute_data_table_meta"); + } + pub(super) fn execute_sort_data_table( &mut self, transaction: &mut PendingTransaction, diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 9c5332bd8d..69e1c6e2b1 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -47,6 +47,9 @@ impl GridController { Operation::UpdateDataTableName { .. } => Self::handle_execution_operation_result( self.execute_update_data_table_name(transaction, op), ), + Operation::DataTableMeta { .. } => Self::handle_execution_operation_result( + self.execute_data_table_meta(transaction, op), + ), Operation::SortDataTable { .. } => Self::handle_execution_operation_result( self.execute_sort_data_table(transaction, op), ), diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 56321faa0e..487b98e9ff 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use crate::{ cell_values::CellValues, grid::{ - data_table::sort::DataTableSort, + data_table::{column::DataTableColumn, sort::DataTableSort}, file::sheet_schema::SheetSchema, formats::Formats, formatting::CellFmtArray, @@ -60,6 +60,12 @@ pub enum Operation { sheet_pos: SheetPos, name: String, }, + DataTableMeta { + sheet_pos: SheetPos, + name: Option, + alternating_colors: Option, + columns: Option>, + }, SortDataTable { sheet_pos: SheetPos, sort: Option>, @@ -244,6 +250,18 @@ impl fmt::Display for Operation { sheet_pos, name ) } + Operation::DataTableMeta { + sheet_pos, + name, + alternating_colors, + columns, + } => { + write!( + fmt, + "DataTableMeta {{ sheet_pos: {} name: {:?} alternating_colors: {:?} columns: {:?} }}", + sheet_pos, name, alternating_colors, columns + ) + } Operation::SortDataTable { sheet_pos, sort } => { write!( fmt, From 31e3ad995923266029449b4cb281597d70b13650 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 25 Oct 2024 09:54:10 -0600 Subject: [PATCH 144/373] Remove auto_gen_path.rs --- .gitignore | 2 ++ quadratic-rust-shared/build.rs | 2 +- quadratic-rust-shared/src/auto_gen_path.rs | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 quadratic-rust-shared/src/auto_gen_path.rs diff --git a/.gitignore b/.gitignore index fabad2af45..7fc4040921 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,9 @@ quadratic-core/target/ quadratic-core/tmp.txt quadratic-files/target/ quadratic-multiplayer/target/ + quadratic-multiplayer/updateAlertVersion.json +quadratic-rust-shared/src/auto_gen_path.rs # Generated JS files quadratic-api/node_modules/ diff --git a/quadratic-rust-shared/build.rs b/quadratic-rust-shared/build.rs index ba068e747c..8bcfba08e0 100644 --- a/quadratic-rust-shared/build.rs +++ b/quadratic-rust-shared/build.rs @@ -6,7 +6,7 @@ fn main() { let fixtures_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("fixtures"); let data_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("data"); let output = format!( - "const FIXTURES_PATH: &str = \"{}\";\nconst DATA_PATH: &str = \"{}\";", + "pub const FIXTURES_PATH: &str = \"{}\";\npub const DATA_PATH: &str = \"{}\";", fixtures_path.display(), data_path.display() ); diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs deleted file mode 100644 index a0dea21ae2..0000000000 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ /dev/null @@ -1,2 +0,0 @@ -const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From c07ad2541e1e6cadbfafb2401f3adf777f9074b9 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 08:57:46 -0700 Subject: [PATCH 145/373] reworking cellsMarker --- quadratic-rust-shared/src/auto_gen_path.rs | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 quadratic-rust-shared/src/auto_gen_path.rs diff --git a/quadratic-rust-shared/src/auto_gen_path.rs b/quadratic-rust-shared/src/auto_gen_path.rs deleted file mode 100644 index a0dea21ae2..0000000000 --- a/quadratic-rust-shared/src/auto_gen_path.rs +++ /dev/null @@ -1,2 +0,0 @@ -const FIXTURES_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/fixtures"; -const DATA_PATH: &str = "/Users/daviddimaria/work/quadratic/quadratic/quadratic-rust-shared/data"; \ No newline at end of file From e9966c68fdcc9dfd860acaa3198782f36696d012 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 08:58:20 -0700 Subject: [PATCH 146/373] reworking markers --- .../src/app/gridGL/cells/CellsArray.ts | 6 +-- .../src/app/gridGL/cells/CellsMarkers.ts | 41 +++++++++---------- .../src/app/gridGL/generateTextures.ts | 2 +- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts index cda7f2e8ea..540758d310 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsArray.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsArray.ts @@ -165,9 +165,9 @@ export class CellsArray extends Container { // } // } - // if (!editingCell) { - // this.cellsSheet.cellsMarkers.add(start, codeCell, true); - // } + if (!editingCell) { + this.cellsSheet.cellsMarkers.add(start, codeCell, true); + } const end = this.sheet.getCellOffsets( Number(codeCell.x) + (codeCell.spill_error ? 1 : codeCell.w), diff --git a/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts b/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts index 2fe3270747..ef4aea3ddf 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts @@ -3,10 +3,9 @@ import { CodeCellLanguage, JsRenderCodeCell } from '@/app/quadratic-core-types'; import { Container, Point, Rectangle, Sprite, Texture } from 'pixi.js'; import { colors } from '../../theme/colors'; import { generatedTextures } from '../generateTextures'; -import { pixiAppSettings } from '../pixiApp/PixiAppSettings'; import { ErrorMarker } from './CellsSheet'; -const INDICATOR_SIZE = 4; +// const INDICATOR_SIZE = 4; export const TRIANGLE_SCALE = 0.1; export type CellsMarkerTypes = 'CodeIcon' | 'FormulaIcon' | 'AIIcon' | 'ErrorIcon'; @@ -74,29 +73,29 @@ export class CellsMarkers extends Container { if (isError) { triangle = this.addChild(new Sprite(generatedTextures.triangle)); triangle.scale.set(TRIANGLE_SCALE); - triangle.position.set(box.x, box.y); + triangle.anchor.set(1, 0); + triangle.position.set(box.x + box.width - 0.5, box.y + 0.5); triangle.tint = colors.cellColorError; } - if (isError || selected || pixiAppSettings.showCellTypeOutlines) { - const symbol = getLanguageSymbol(codeCell.language, isError); - if (symbol) { - this.addChild(symbol); - symbol.height = INDICATOR_SIZE; - symbol.width = INDICATOR_SIZE; - symbol.position.set(box.x + 1.25, box.y + 1.25); - if (isError) { - symbol.x -= 1; - symbol.y -= 1; - } - this.markers.push({ - bounds: new Rectangle(box.x, box.y, box.width, box.height), - codeCell, - triangle, - symbol, - }); - } + if (isError) { + // const symbol = getLanguageSymbol(codeCell.language, isError); + // if (symbol) { + // this.addChild(symbol); + // symbol.height = INDICATOR_SIZE; + // symbol.width = INDICATOR_SIZE; + // symbol.position.set(box.x + 1.25, box.y + 1.25); + // if (isError) { + // symbol.x -= 1; + // symbol.y -= 1; + // } + this.markers.push({ + bounds: new Rectangle(box.x, box.y, box.width, box.height), + codeCell, + triangle, + }); } + // } } intersectsCodeInfo(point: Point): JsRenderCodeCell | undefined { diff --git a/quadratic-client/src/app/gridGL/generateTextures.ts b/quadratic-client/src/app/gridGL/generateTextures.ts index 6f0007f0c5..78d665bd55 100644 --- a/quadratic-client/src/app/gridGL/generateTextures.ts +++ b/quadratic-client/src/app/gridGL/generateTextures.ts @@ -74,7 +74,7 @@ function createTriangle() { context.fillStyle = 'white'; context.moveTo(0, 0); context.lineTo(TRIANGLE_SIZE, 0); - context.lineTo(0, TRIANGLE_SIZE); + context.lineTo(TRIANGLE_SIZE, TRIANGLE_SIZE); context.closePath(); context.fill(); return Texture.from(canvas); From 8783d6d223285481e1322ae1f7657d59d35a69b8 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 09:28:32 -0700 Subject: [PATCH 147/373] fix overflow issues with table columns and name --- quadratic-client/src/app/gridGL/cells/tables/Table.ts | 8 ++++++++ .../src/app/gridGL/cells/tables/TableColumnHeader.ts | 2 +- quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts | 9 ++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 8fd268a439..f545211c66 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -77,6 +77,14 @@ export class Table extends Container { } else { this.tableName.y = this.tableBounds.top; } + const headingWidth = pixiApp.headings.headingSize.width / pixiApp.viewport.scaled; + if (this.tableBounds.x < bounds.left + headingWidth) { + this.tableName.x = bounds.left + headingWidth; + this.tableName.visible = this.tableName.x + this.tableName.width <= this.tableBounds.right; + } else { + this.tableName.x = this.tableBounds.x; + this.tableName.visible = true; + } } }; diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 4d158be9c4..13fb987336 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -87,7 +87,7 @@ export class TableColumnHeader extends Container { } private drawSortButton(width: number, height: number, sort?: DataTableSort) { - this.sortButtonStart = this.columnHeaderBounds.right - SORT_BUTTON_RADIUS - SORT_BUTTON_PADDING; + this.sortButtonStart = this.columnHeaderBounds.right - SORT_BUTTON_RADIUS * 2 - SORT_BUTTON_PADDING * 2; this.sortButton = this.addChild(new Graphics()); this.sortButton.beginFill(0, SORT_BACKGROUND_ALPHA); this.sortButton.drawCircle(0, 0, SORT_BUTTON_RADIUS); diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index f7e8b04242..6ccdb34860 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -58,7 +58,6 @@ export class PixiApp { // this is used to display content over the headings (eg, table name when off // the screen) - private overHeadings: Container; overHeadingsColumnsHeaders: Container; overHeadingsTableNames: Container; @@ -92,9 +91,8 @@ export class PixiApp { this.cellsSheets = new CellsSheets(); this.cellImages = new UICellImages(); this.validations = new UIValidations(); - this.overHeadings = new Container(); - this.overHeadingsColumnsHeaders = this.overHeadings.addChild(new Container()); - this.overHeadingsTableNames = this.overHeadings.addChild(new Container()); + this.overHeadingsColumnsHeaders = new Container(); + this.overHeadingsTableNames = new Container(); this.viewport = new Viewport(); } @@ -155,6 +153,7 @@ export class PixiApp { this.debug = this.viewportContents.addChild(new Graphics()); this.cellsSheets = this.viewportContents.addChild(this.cellsSheets); + this.viewportContents.addChild(this.overHeadingsColumnsHeaders); this.gridLines = this.viewportContents.addChild(new GridLines()); this.axesLines = this.viewportContents.addChild(new AxesLines()); this.boxCells = this.viewportContents.addChild(new BoxCells()); @@ -167,7 +166,7 @@ export class PixiApp { this.cellMoving = this.viewportContents.addChild(new UICellMoving()); this.validations = this.viewportContents.addChild(this.validations); this.headings = this.viewportContents.addChild(new GridHeadings()); - this.viewportContents.addChild(this.overHeadings); + this.viewportContents.addChild(this.overHeadingsTableNames); this.reset(); From c22e2c9471fb28cdc952ca16b8c759c2012855f3 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 09:53:26 -0700 Subject: [PATCH 148/373] fix bug with table name not disappearing when inactive --- .../src/app/gridGL/cells/tables/Table.ts | 19 +++++++++++-------- .../src/app/gridGL/cells/tables/TableName.ts | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index f545211c66..7560d2af60 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -70,6 +70,7 @@ export class Table extends Container { } }; + // todo: this probably belongs in TableName private tableNamePosition = (bounds: Rectangle, gridHeading: number) => { if (this.visible) { if (this.tableBounds.y < bounds.top + gridHeading) { @@ -78,12 +79,14 @@ export class Table extends Container { this.tableName.y = this.tableBounds.top; } const headingWidth = pixiApp.headings.headingSize.width / pixiApp.viewport.scaled; - if (this.tableBounds.x < bounds.left + headingWidth) { - this.tableName.x = bounds.left + headingWidth; - this.tableName.visible = this.tableName.x + this.tableName.width <= this.tableBounds.right; - } else { - this.tableName.x = this.tableBounds.x; - this.tableName.visible = true; + if (!this.tableName.hidden) { + if (this.tableBounds.x < bounds.left + headingWidth) { + this.tableName.x = bounds.left + headingWidth; + this.tableName.visible = this.tableName.x + this.tableName.width <= this.tableBounds.right; + } else { + this.tableName.x = this.tableBounds.x; + this.tableName.visible = true; + } } } }; @@ -154,13 +157,13 @@ export class Table extends Container { hideActive() { this.outline.visible = false; - this.tableName.visible = false; + this.tableName.hide(); pixiApp.setViewportDirty(); } showActive() { this.outline.visible = true; - this.tableName.visible = true; + this.tableName.show(); pixiApp.setViewportDirty(); } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts index 5ec1d892f4..1e780218f4 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts @@ -25,6 +25,9 @@ export class TableName extends Container { private dropdown: Sprite; private backgroundWidth = 0; + // hidden by Tables + hidden: boolean = true; + constructor(table: Table) { super(); this.table = table; @@ -40,6 +43,7 @@ export class TableName extends Container { if (sheets.sheet.id === this.table.sheet.id) { pixiApp.overHeadingsTableNames.addChild(this); } + this.visible = false; } private drawBackground() { @@ -144,4 +148,14 @@ export class TableName extends Container { return { table: this.table.codeCell, type: 'dropdown' }; } } + + hide() { + this.visible = false; + this.hidden = true; + } + + show() { + this.visible = true; + this.hidden = false; + } } From bdae8dc3090e96886bad6be8e550c13514849181 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 11:24:50 -0700 Subject: [PATCH 149/373] fixed bug with gridlines appearing on top of the column names again --- .../gridGL/UI/gridHeadings/GridHeadings.ts | 24 ++++++++++++------- .../UI/gridHeadings/GridHeadingsRows.ts | 16 +++++++++++++ .../src/app/gridGL/pixiApp/PixiApp.ts | 10 ++++++-- 3 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadingsRows.ts diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts index 15d31b5001..55a4f652d4 100644 --- a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts @@ -1,4 +1,5 @@ import { events } from '@/app/events/events'; +import { GridHeadingRows } from '@/app/gridGL/UI/gridHeadings/GridHeadingsRows'; import { CELL_HEIGHT, CELL_WIDTH } from '@/shared/constants/gridConstants'; import { BitmapText, Container, Graphics, Point, Rectangle } from 'pixi.js'; import { sheets } from '../../../grid/controller/Sheets'; @@ -44,6 +45,10 @@ export class GridHeadings extends Container { private columnRect: Rectangle | undefined; private cornerRect: Rectangle | undefined; + // this needs to be a child of viewportContents so it it is placed over the + // grid lines + gridHeadingsRows: GridHeadingRows; + dirty = true; constructor() { @@ -51,6 +56,7 @@ export class GridHeadings extends Container { this.headingsGraphics = this.addChild(new Graphics()); this.labels = this.addChild(new GridHeadingsLabels()); this.corner = this.addChild(new Graphics()); + this.gridHeadingsRows = new GridHeadingRows(); } // calculates static character size (used in overlap calculations) @@ -279,11 +285,11 @@ export class GridHeadings extends Container { this.rowWidth = Math.max(this.rowWidth, CELL_HEIGHT / viewport.scale.x); // draw background of vertical bar - this.headingsGraphics.lineStyle(0); - this.headingsGraphics.beginFill(colors.headerBackgroundColor); + this.gridHeadingsRows.headingsGraphics.lineStyle(0); + this.gridHeadingsRows.headingsGraphics.beginFill(colors.headerBackgroundColor); this.columnRect = new Rectangle(bounds.left, bounds.top, this.rowWidth, bounds.height); - this.headingsGraphics.drawShape(this.columnRect); - this.headingsGraphics.endFill(); + this.gridHeadingsRows.headingsGraphics.drawShape(this.columnRect); + this.gridHeadingsRows.headingsGraphics.endFill(); this.rowRect = new Rectangle(bounds.left, bounds.top, this.rowWidth, bounds.height); // fill the entire viewport if all cells are selected @@ -383,15 +389,15 @@ export class GridHeadings extends Container { for (let y = topOffset; y <= bottomOffset; y += currentHeight) { currentHeight = offsets.getRowHeight(row); if (gridAlpha !== 0) { - this.headingsGraphics.lineStyle( + this.gridHeadingsRows.headingsGraphics.lineStyle( 1, colors.gridLines, colors.headerSelectedRowColumnBackgroundColorAlpha * gridAlpha, 0.5, true ); - this.headingsGraphics.moveTo(bounds.left, y); - this.headingsGraphics.lineTo(bounds.left + this.rowWidth, y); + this.gridHeadingsRows.headingsGraphics.moveTo(bounds.left, y); + this.gridHeadingsRows.headingsGraphics.lineTo(bounds.left + this.rowWidth, y); this.gridLinesRows.push({ row: row - 1, y, height: offsets.getRowHeight(row - 1) }); } @@ -474,9 +480,11 @@ export class GridHeadings extends Container { } this.dirty = false; this.labels.clear(); - this.headingsGraphics.clear(); + this.gridHeadingsRows.labels.clear(); + this.gridHeadingsRows.headingsGraphics.clear(); + if (!pixiAppSettings.showHeadings) { this.visible = false; this.rowRect = undefined; diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadingsRows.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadingsRows.ts new file mode 100644 index 0000000000..54cd2a8891 --- /dev/null +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadingsRows.ts @@ -0,0 +1,16 @@ +//! This is the container for row headings. It needs to be separate from GridHeadings because +//! it needs to be placed over the grid lines, while the column headings needs to be under them. + +import { GridHeadingsLabels } from '@/app/gridGL/UI/gridHeadings/GridHeadingsLabels'; +import { Container, Graphics } from 'pixi.js'; + +export class GridHeadingRows extends Container { + headingsGraphics: Graphics; + labels: GridHeadingsLabels; + + constructor() { + super(); + this.headingsGraphics = this.addChild(new Graphics()); + this.labels = this.addChild(new GridHeadingsLabels()); + } +} diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index 6ccdb34860..ab61018120 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -153,8 +153,14 @@ export class PixiApp { this.debug = this.viewportContents.addChild(new Graphics()); this.cellsSheets = this.viewportContents.addChild(this.cellsSheets); - this.viewportContents.addChild(this.overHeadingsColumnsHeaders); this.gridLines = this.viewportContents.addChild(new GridLines()); + this.viewportContents.addChild(this.overHeadingsColumnsHeaders); + + // this is a hack to ensure that table column names appears over the column + // headings, but under the row headings + const gridHeadings = new GridHeadings(); + this.viewportContents.addChild(gridHeadings.gridHeadingsRows); + this.axesLines = this.viewportContents.addChild(new AxesLines()); this.boxCells = this.viewportContents.addChild(new BoxCells()); this.cellImages = this.viewportContents.addChild(this.cellImages); @@ -165,7 +171,7 @@ export class PixiApp { this.cellHighlights = this.viewportContents.addChild(new CellHighlights()); this.cellMoving = this.viewportContents.addChild(new UICellMoving()); this.validations = this.viewportContents.addChild(this.validations); - this.headings = this.viewportContents.addChild(new GridHeadings()); + this.headings = this.viewportContents.addChild(gridHeadings); this.viewportContents.addChild(this.overHeadingsTableNames); this.reset(); From 04b84a5ebde54615d3a45b4c52403ee6689fd46f Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 25 Oct 2024 11:34:36 -0700 Subject: [PATCH 150/373] fixed bug with sort button not disappearing when moving into table name --- .../src/app/gridGL/cells/tables/Table.ts | 4 +++- .../gridGL/cells/tables/TableColumnHeaders.ts | 22 +++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 7560d2af60..a7bde59fac 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -70,7 +70,7 @@ export class Table extends Container { } }; - // todo: this probably belongs in TableName + // todo: this probably belongs in TableName.ts private tableNamePosition = (bounds: Rectangle, gridHeading: number) => { if (this.visible) { if (this.tableBounds.y < bounds.top + gridHeading) { @@ -217,10 +217,12 @@ export class Table extends Container { pointerMove(world: Point): 'table-name' | boolean { const name = this.tableName.intersects(world); if (name?.type === 'dropdown') { + this.columnHeaders.clearSortButtons(); this.tableCursor = 'pointer'; return 'table-name'; } else if (name?.type === 'table-name') { this.tableCursor = undefined; + this.columnHeaders.clearSortButtons(); return 'table-name'; } const result = this.columnHeaders.pointerMove(world); diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index c2258cbe02..394c2de102 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -100,7 +100,7 @@ export class TableColumnHeaders extends Container { }); } - // update when there is an updated code cell + // update appearance when there is an updated code cell update() { if (this.table.codeCell.show_header) { this.visible = true; @@ -112,6 +112,17 @@ export class TableColumnHeaders extends Container { } } + clearSortButtons(current?: TableColumnHeader) { + this.columns.children.forEach((column) => { + if (column !== current) { + if (column.sortButton?.visible) { + column.sortButton.visible = false; + pixiApp.setViewportDirty(); + } + } + }); + } + pointerMove(world: Point): boolean { const adjustedWorld = world.clone(); // need to adjust the y position in the case of sticky headers @@ -124,14 +135,7 @@ export class TableColumnHeaders extends Container { } // ensure we clear the sort button on any other column header - this.columns.children.forEach((column) => { - if (column !== found) { - if (column.sortButton?.visible) { - column.sortButton.visible = false; - pixiApp.setViewportDirty(); - } - } - }); + this.clearSortButtons(found); return !!found; } From bb8ab7c4bb24cef86efa80abc5b4db5158498da6 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 25 Oct 2024 15:00:09 -0600 Subject: [PATCH 151/373] merge latest --- .../src/app/actions/dataTableSpec.ts | 15 +- .../HTMLGrid/contextMenus/GridContextMenu.tsx | 140 +++++++----------- .../contextMenus/TableColumnContextMenu.tsx | 104 ++++--------- .../contextMenus/TableContextMenu.tsx | 62 +------- .../HTMLGrid/contextMenus/TableMenu.tsx | 67 ++++----- .../HTMLGrid/contextMenus/contextMenu.tsx | 138 ++++++++--------- .../src/app/helpers/codeCellLanguage.ts | 2 +- .../src/app/ui/components/LanguageIcon.tsx | 2 +- .../src/shared/shadcn/ui/dropdown-menu.tsx | 9 +- 9 files changed, 210 insertions(+), 329 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index f1e6f5ab50..5bcf49e447 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -15,7 +15,6 @@ import { ShowIcon, SortIcon, TableConvertIcon, - TableIcon, UpArrowIcon, } from '@/shared/components/Icons'; import { Rectangle } from 'pixi.js'; @@ -58,7 +57,7 @@ const isAlternatingColorsShowing = (): boolean => { export const dataTableSpec: DataTableSpec = { [Action.FlattenTable]: { - label: 'Flatten table to grid', + label: 'Flatten to sheet', Icon: FlattenTableIcon, run: async () => { const table = getTable(); @@ -68,7 +67,7 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.GridToDataTable]: { - label: 'Convert values to data table', + label: 'Convert to table', Icon: TableConvertIcon, run: async () => { quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); @@ -91,7 +90,7 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.RenameTable]: { - label: 'Rename table', + label: 'Rename', defaultOption: true, Icon: FileRenameIcon, run: () => { @@ -114,7 +113,7 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.DeleteDataTable]: { - label: 'Delete table', + label: 'Delete', Icon: DeleteIcon, run: () => { const table = getTable(); @@ -125,8 +124,8 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.CodeToDataTable]: { - label: 'Convert to data table', - Icon: TableIcon, + label: 'Convert from code to data', + Icon: TableConvertIcon, run: () => { const table = getTable(); if (table) { @@ -135,7 +134,7 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.SortTable]: { - label: 'Sort table', + label: 'Sort', Icon: SortIcon, run: () => { setTimeout(() => { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index 94e2094a2f..f44b69c98f 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -1,41 +1,24 @@ //! This shows the grid heading context menu. import { Action } from '@/app/actions/actions'; -import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; -import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; +import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenu'; +import { ContextMenuItem, ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; -import { focusGrid } from '@/app/helpers/focusGrid'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { TableIcon } from '@/shared/components/Icons'; -import { ControlledMenu, MenuDivider, SubMenu } from '@szhsin/react-menu'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useRecoilState } from 'recoil'; +import { + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, +} from '@/shared/shadcn/ui/dropdown-menu'; +import { useEffect, useState } from 'react'; import { pixiApp } from '../../pixiApp/PixiApp'; export const GridContextMenu = () => { - const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); - - const onClose = useCallback(() => { - if (contextMenu.type === ContextMenuType.Grid) { - setContextMenu({}); - focusGrid(); - } - }, [contextMenu.type, setContextMenu]); - - useEffect(() => { - pixiApp.viewport.on('moved', onClose); - pixiApp.viewport.on('zoomed', onClose); - - return () => { - pixiApp.viewport.off('moved', onClose); - pixiApp.viewport.off('zoomed', onClose); - }; - }, [onClose]); - - const ref = useRef(null); - const [columnRowAvailable, setColumnRowAvailable] = useState(false); const [canConvertToDataTable, setCanConvertToDataTable] = useState(false); const [table, setTable] = useState(); @@ -56,68 +39,53 @@ export const GridContextMenu = () => { }, []); return ( -
- - - - - - - - + + {({ contextMenu }) => ( + <> + + + + + + + - {contextMenu.column === null ? null : ( - <> - - {columnRowAvailable && } - {columnRowAvailable && } - - - )} + {contextMenu.column === null ? null : ( + <> + + {columnRowAvailable && } + {columnRowAvailable && } + + + )} - {contextMenu.row === null ? null : ( - <> - {columnRowAvailable && } - {columnRowAvailable && } - {columnRowAvailable && } - - - )} + {contextMenu.row === null ? null : ( + <> + {columnRowAvailable && } + {columnRowAvailable && } + {columnRowAvailable && } + + + )} - {canConvertToDataTable && } - {canConvertToDataTable && } + {canConvertToDataTable && } + {canConvertToDataTable && } - {table && } - {table && ( - - -
{table?.language === 'Import' ? 'Data' : 'Code'} Table
-
- } - > - - - )} - -
+ {table && ( + <> + + + + } text={'Table'} /> + + + + + + + )} + + )} + ); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx index 6b2ad2e6bb..975f6f33c4 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -1,83 +1,43 @@ //! This shows the table column's header context menu. import { Action } from '@/app/actions/actions'; -import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; -import { events } from '@/app/events/events'; -import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; +import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenu'; +import { ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; -import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { focusGrid } from '@/app/helpers/focusGrid'; import { TableIcon } from '@/shared/components/Icons'; -import { ControlledMenu, MenuDivider, SubMenu } from '@szhsin/react-menu'; -import { useCallback, useEffect, useRef } from 'react'; -import { useRecoilState } from 'recoil'; +import { + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, +} from '@/shared/shadcn/ui/dropdown-menu'; +import { DropdownMenuItem } from '@radix-ui/react-dropdown-menu'; export const TableColumnContextMenu = () => { - const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); - - const onClose = useCallback(() => { - if (contextMenu.type === ContextMenuType.TableColumn) { - setContextMenu({}); - events.emit('contextMenuClose'); - focusGrid(); - } - }, [contextMenu.type, setContextMenu]); - - useEffect(() => { - pixiApp.viewport.on('moved', onClose); - pixiApp.viewport.on('zoomed', onClose); - - return () => { - pixiApp.viewport.off('moved', onClose); - pixiApp.viewport.off('zoomed', onClose); - }; - }, [onClose]); - - const ref = useRef(null); - - const display = - contextMenu.type === ContextMenuType.Table && contextMenu.selectedColumn !== undefined && !contextMenu.rename - ? 'block' - : 'none'; - return ( -
- - - - - - - - - + + {({ contextMenu }) => ( + <> + + + + + + + + + -
{contextMenu.table?.language === 'Import' ? 'Data' : 'Code'} Table
-
- } - > - - - - + {contextMenu.table?.language === 'Import' ? 'Data' : 'Code'} Table + + + Test + + + + + )} + ); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 2bcdb44836..5e6f2762a2 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -1,65 +1,13 @@ //! This shows the table context menu. -import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; -import { events } from '@/app/events/events'; +import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenu'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; -import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { focusGrid } from '@/app/helpers/focusGrid'; -import { ControlledMenu } from '@szhsin/react-menu'; -import { useCallback, useEffect, useRef } from 'react'; -import { useRecoilState } from 'recoil'; export const TableContextMenu = () => { - const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); - - const onClose = useCallback(() => { - if ( - contextMenu.type !== ContextMenuType.Table || - (contextMenu.type === ContextMenuType.Table && !contextMenu.rename) - ) { - return; - } - setContextMenu({}); - events.emit('contextMenuClose'); - focusGrid(); - }, [contextMenu.rename, contextMenu.type, setContextMenu]); - - useEffect(() => { - pixiApp.viewport.on('moved', onClose); - pixiApp.viewport.on('zoomed', onClose); - - return () => { - pixiApp.viewport.off('moved', onClose); - pixiApp.viewport.off('zoomed', onClose); - }; - }, [onClose]); - - const ref = useRef(null); - return ( -
- - - -
+ + {({ contextMenu }) => } + ); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index 49d3ac464f..e1f90dd88b 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -1,7 +1,9 @@ import { Action } from '@/app/actions/actions'; -import { MenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/contextMenu'; +import { ContextMenuItem, ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; +import { getCodeCell } from '@/app/helpers/codeCellLanguage'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; -import { MenuDivider, MenuHeader } from '@szhsin/react-menu'; +import { LanguageIcon } from '@/app/ui/components/LanguageIcon'; +import { DropdownMenuItem, DropdownMenuSeparator } from '@/shared/shadcn/ui/dropdown-menu'; import { useMemo } from 'react'; interface Props { @@ -11,27 +13,8 @@ interface Props { export const TableMenu = (props: Props) => { const { defaultRename, codeCell } = props; - - const isCodeTable = codeCell?.language === 'Import' ? 'Data' : 'Code'; - - const header = useMemo(() => { - if (!codeCell) { - return ''; - } - if (codeCell.language === 'Import') { - return 'Data Table'; - } else if (codeCell.language === 'Formula') { - return 'Formula Table'; - } else if (codeCell.language === 'Python') { - return 'Python Table'; - } else if (codeCell.language === 'Javascript') { - return 'JavaScript Table'; - } else if (typeof codeCell.language === 'object') { - return codeCell.language.Connection?.kind; - } else { - throw new Error(`Unknown language: ${codeCell.language}`); - } - }, [codeCell]); + const cell = getCodeCell(codeCell?.language); + const isCodeCell = cell && cell.id !== 'Import'; const hasHiddenColumns = useMemo(() => { console.log('TODO: hasHiddenColumns', codeCell); @@ -45,18 +28,32 @@ export const TableMenu = (props: Props) => { return ( <> - {header} - - - {hasHiddenColumns && } - - - - - - {isCodeTable && } - - + {isCodeCell && ( + <> + + + // + } + text={`${cell.label} code`} + /> + + + + )} + + + + {hasHiddenColumns && } + + + + + + {isCodeCell && } + + ); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx index 66d6df78e7..9b3a8a1a29 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx @@ -1,74 +1,80 @@ -import { Action } from '@/app/actions/actions'; -import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; -import { keyboardShortcutEnumToDisplay } from '@/app/helpers/keyboardShortcutsDisplay'; -import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; -import { CheckBoxEmptyIcon, CheckBoxIcon, IconComponent } from '@/shared/components/Icons'; -import { MenuItem } from '@szhsin/react-menu'; +import { contextMenuAtom, ContextMenuState, ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { events } from '@/app/events/events'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { focusGrid } from '@/app/helpers/focusGrid'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/shared/shadcn/ui/dropdown-menu'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useRecoilState } from 'recoil'; -interface Props { - action: Action; +/** + * Wrapper component for any context menu on the grid. + */ +export const ContextMenu = ({ + children, + contextMenuType, +}: { + children: ({ contextMenu }: { contextMenu: ContextMenuState }) => React.ReactNode; + contextMenuType: ContextMenuType; +}) => { + const ref = useRef(null); + const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); + const open = contextMenu.type === contextMenuType && !contextMenu.rename; - // allows overriding of the default option (which sets the menu item to bold) - overrideDefaultOption?: boolean; -} + // Local state for the dropdown menu + const [isOpen, setIsOpen] = useState(false); + useEffect(() => { + setIsOpen(open); + }, [open]); -export const MenuItemAction = (props: Props): JSX.Element | null => { - const { overrideDefaultOption } = props; - const { label, Icon, run, isAvailable, checkbox, defaultOption } = defaultActionSpec[props.action]; - const isAvailableArgs = useIsAvailableArgs(); - const keyboardShortcut = keyboardShortcutEnumToDisplay(props.action); + const onClose = useCallback(() => { + setContextMenu({}); + events.emit('contextMenuClose'); + focusGrid(); + }, [setContextMenu]); - if (isAvailable && !isAvailable(isAvailableArgs)) { - return null; - } + useEffect(() => { + pixiApp.viewport.on('moved', onClose); + pixiApp.viewport.on('zoomed', onClose); - return ( - - {label} - - ); -}; + return () => { + pixiApp.viewport.off('moved', onClose); + pixiApp.viewport.off('zoomed', onClose); + }; + }, [onClose]); -function MenuItemShadStyle({ - children, - Icon, - checkbox, - onClick, - keyboardShortcut, -}: { - children: JSX.Element; - Icon?: IconComponent; - onClick: any; - checkbox?: boolean | (() => boolean); - keyboardShortcut?: string; -}) { - const menuItemClassName = - 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; - - const icon = Icon ? : null; - let checkboxElement: JSX.Element | null = null; - if (!Icon && checkbox !== undefined) { - let checked: boolean; - if (typeof checkbox === 'function') { - checked = checkbox(); - } else { - checked = checkbox === true; - } - if (checked) { - checkboxElement = ; - } else { - checkboxElement = ; - } - } return ( - - - {icon} - {checkboxElement} {children} - - {keyboardShortcut && ( - {keyboardShortcut} - )} - +
+ { + setIsOpen(dropdownOpen); + if (!dropdownOpen) onClose(); + }} + > + {/* Radix wants the trigger for positioning the content, so we hide it visibly */} + Menu + e.preventDefault()} + > + {children({ contextMenu })} + + +
); -} +}; diff --git a/quadratic-client/src/app/helpers/codeCellLanguage.ts b/quadratic-client/src/app/helpers/codeCellLanguage.ts index 04b6881862..0d6ee14227 100644 --- a/quadratic-client/src/app/helpers/codeCellLanguage.ts +++ b/quadratic-client/src/app/helpers/codeCellLanguage.ts @@ -8,7 +8,7 @@ const codeCellsById = { POSTGRES: { id: 'POSTGRES', label: 'Postgres', type: 'connection' }, MYSQL: { id: 'MYSQL', label: 'MySQL', type: 'connection' }, MSSQL: { id: 'MSSQL', label: 'MS SQL Server', type: 'connection' }, - SNOWFLAKE: { id: 'SNOWFLAKE', label: 'SNOWFLAKE', type: 'connection' }, + SNOWFLAKE: { id: 'SNOWFLAKE', label: 'Snowflake', type: 'connection' }, } as const; export type CodeCellIds = keyof typeof codeCellsById; // type CodeCell = (typeof codeCellsById)[CodeCellIds]; diff --git a/quadratic-client/src/app/ui/components/LanguageIcon.tsx b/quadratic-client/src/app/ui/components/LanguageIcon.tsx index 830148c5e9..605c0df189 100644 --- a/quadratic-client/src/app/ui/components/LanguageIcon.tsx +++ b/quadratic-client/src/app/ui/components/LanguageIcon.tsx @@ -14,7 +14,7 @@ export function LanguageIcon({ language, ...props }: LanguageIconProps) { ) : language === 'Formula' ? ( ) : language === 'Javascript' ? ( - + ) : language === 'POSTGRES' ? ( ) : language === 'MYSQL' ? ( diff --git a/quadratic-client/src/shared/shadcn/ui/dropdown-menu.tsx b/quadratic-client/src/shared/shadcn/ui/dropdown-menu.tsx index 30cf6f0d59..827bee269d 100644 --- a/quadratic-client/src/shared/shadcn/ui/dropdown-menu.tsx +++ b/quadratic-client/src/shared/shadcn/ui/dropdown-menu.tsx @@ -54,15 +54,18 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam const DropdownMenuContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + animate?: boolean; + } +>(({ className, sideOffset = 4, animate = true, ...props }, ref) => ( Date: Fri, 25 Oct 2024 15:00:24 -0600 Subject: [PATCH 152/373] Create ContextMenuItem.tsx --- .../HTMLGrid/contextMenus/ContextMenuItem.tsx | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem.tsx diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem.tsx new file mode 100644 index 0000000000..e894f37de1 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem.tsx @@ -0,0 +1,125 @@ +import { Action } from '@/app/actions/actions'; +import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; +import { keyboardShortcutEnumToDisplay } from '@/app/helpers/keyboardShortcutsDisplay'; +import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; +import { CheckIcon } from '@/shared/components/Icons'; +import { DropdownMenuItem, DropdownMenuShortcut } from '@/shared/shadcn/ui/dropdown-menu'; + +export const ContextMenuItemAction = (props: { + action: Action; + + // allows overriding of the default option (which sets the menu item to bold) + overrideDefaultOption?: boolean; +}): JSX.Element | null => { + const { overrideDefaultOption } = props; + const { label, Icon, run, isAvailable, checkbox, defaultOption } = defaultActionSpec[props.action]; + const isAvailableArgs = useIsAvailableArgs(); + const keyboardShortcut = keyboardShortcutEnumToDisplay(props.action); + + if (isAvailable && !isAvailable(isAvailableArgs)) { + return null; + } + + let icon = Icon ? : null; + + if (!Icon && checkbox !== undefined) { + const checked = typeof checkbox === 'function' ? checkbox() : checkbox === true; + if (checked) { + icon = ; + } + // else { + // checkboxElement = ; + // } + } + + return ( + { + // @ts-expect-error + run(); + }} + > + + {icon} + {checkbox === true && } + + } + text={label} + textBold={overrideDefaultOption ?? defaultOption} + shortcut={keyboardShortcut} + /> + + ); + + // return ( + // + // {label} + // + // ); +}; + +export const ContextMenuItem = ({ + icon, + text, + textBold, + shortcut, +}: { + icon: React.ReactNode; + text: React.ReactNode; + textBold?: boolean; + shortcut?: React.ReactNode; +}): JSX.Element => { + return ( + <> + {icon} + {text} + {shortcut && {shortcut}} + + ); +}; + +// function MenuItemShadStyle({ +// children, +// Icon, +// checkbox, +// onClick, +// keyboardShortcut, +// }: { +// children: JSX.Element; +// Icon?: IconComponent; +// onClick: any; +// checkbox?: boolean | (() => boolean); +// keyboardShortcut?: string; +// }) { +// const menuItemClassName = +// 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; + +// const icon = Icon ? : null; +// let checkboxElement: JSX.Element | null = null; +// if (!Icon && checkbox !== undefined) { +// let checked: boolean; +// if (typeof checkbox === 'function') { +// checked = checkbox(); +// } else { +// checked = checkbox === true; +// } +// if (checked) { +// checkboxElement = ; +// } else { +// checkboxElement = ; +// } +// } +// return ( +// +// +// {icon} +// {checkboxElement} {children} +// +// {keyboardShortcut && ( +// {keyboardShortcut} +// )} +// +// ); +// } From eedf4c1a324fc23262e6dc13189a94d089666ac0 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 25 Oct 2024 15:00:51 -0600 Subject: [PATCH 153/373] Update ContextMenuItem.tsx --- .../HTMLGrid/contextMenus/ContextMenuItem.tsx | 54 ------------------- 1 file changed, 54 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem.tsx index e894f37de1..b8e1a50042 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem.tsx @@ -7,7 +7,6 @@ import { DropdownMenuItem, DropdownMenuShortcut } from '@/shared/shadcn/ui/dropd export const ContextMenuItemAction = (props: { action: Action; - // allows overriding of the default option (which sets the menu item to bold) overrideDefaultOption?: boolean; }): JSX.Element | null => { @@ -27,9 +26,6 @@ export const ContextMenuItemAction = (props: { if (checked) { icon = ; } - // else { - // checkboxElement = ; - // } } return ( @@ -52,12 +48,6 @@ export const ContextMenuItemAction = (props: { /> ); - - // return ( - // - // {label} - // - // ); }; export const ContextMenuItem = ({ @@ -79,47 +69,3 @@ export const ContextMenuItem = ({ ); }; - -// function MenuItemShadStyle({ -// children, -// Icon, -// checkbox, -// onClick, -// keyboardShortcut, -// }: { -// children: JSX.Element; -// Icon?: IconComponent; -// onClick: any; -// checkbox?: boolean | (() => boolean); -// keyboardShortcut?: string; -// }) { -// const menuItemClassName = -// 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; - -// const icon = Icon ? : null; -// let checkboxElement: JSX.Element | null = null; -// if (!Icon && checkbox !== undefined) { -// let checked: boolean; -// if (typeof checkbox === 'function') { -// checked = checkbox(); -// } else { -// checked = checkbox === true; -// } -// if (checked) { -// checkboxElement = ; -// } else { -// checkboxElement = ; -// } -// } -// return ( -// -// -// {icon} -// {checkboxElement} {children} -// -// {keyboardShortcut && ( -// {keyboardShortcut} -// )} -// -// ); -// } From 83402139cff177b6eb733dcf143c4338b63d775b Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 25 Oct 2024 16:09:49 -0600 Subject: [PATCH 154/373] make code table editalb --- quadratic-client/src/app/actions/actions.ts | 1 + .../src/app/actions/dataTableSpec.ts | 19 +++++++++++++++++++ .../HTMLGrid/contextMenus/TableMenu.tsx | 8 +++----- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index ad95eca4a3..0a0f302467 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -154,4 +154,5 @@ export enum Action { SortTableColumnDescending = 'sort_table_column_descending', HideTableColumn = 'hide_table_column', ShowAllColumns = 'show_all_columns', + EditTableCode = 'edit_table_code', } diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 5bcf49e447..bfefb356d3 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -2,6 +2,7 @@ import { Action } from '@/app/actions/actions'; import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { createSelection } from '@/app/grid/sheet/selection'; +import { doubleClickCell } from '@/app/gridGL/interaction/pointer/doubleClickCell'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; @@ -9,6 +10,7 @@ import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { DeleteIcon, DownArrowIcon, + EditIcon, FileRenameIcon, FlattenTableIcon, HideIcon, @@ -37,6 +39,7 @@ type DataTableSpec = Pick< | Action.SortTableColumnDescending | Action.HideTableColumn | Action.ShowAllColumns + | Action.EditTableCode >; const isFirstRowHeader = (): boolean => { @@ -197,4 +200,20 @@ export const dataTableSpec: DataTableSpec = { console.log('TODO: show all columns'); }, }, + [Action.EditTableCode]: { + label: 'Edit code', + Icon: EditIcon, + run: () => { + const table = getTable(); + if (table) { + const column = table.x; + const row = table.y; + quadraticCore.getCodeCell(sheets.sheet.id, column, row).then((code) => { + if (code) { + doubleClickCell({ column: Number(code.x), row: Number(code.y), language: code.language, cell: '' }); + } + }); + } + }, + }, }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index e1f90dd88b..2d5f4c0c34 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -1,4 +1,5 @@ import { Action } from '@/app/actions/actions'; +import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; import { ContextMenuItem, ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; import { getCodeCell } from '@/app/helpers/codeCellLanguage'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; @@ -30,12 +31,9 @@ export const TableMenu = (props: Props) => { <> {isCodeCell && ( <> - + defaultActionSpec[Action.EditTableCode].run()}> - // - } + icon={} text={`${cell.label} code`} /> From b3de1eef1c98be81fe3d31e0303dac59f0bf8006 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 25 Oct 2024 17:01:07 -0600 Subject: [PATCH 155/373] tweak label --- .../src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index 2d5f4c0c34..aed2084894 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -34,7 +34,7 @@ export const TableMenu = (props: Props) => { defaultActionSpec[Action.EditTableCode].run()}> } - text={`${cell.label} code`} + text={`Edit ${cell.label}`} /> From 61ecaff6fe199fbd6cf36377b93c88a18c0d0b2b Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 25 Oct 2024 17:16:53 -0600 Subject: [PATCH 156/373] udpated icons --- quadratic-client/public/images/icon-formula.png | Bin 0 -> 664 bytes .../public/images/icon-javascript.png | Bin 0 -> 523 bytes quadratic-client/public/images/icon-mysql.png | Bin 0 -> 580 bytes .../public/images/icon-postgres.png | Bin 0 -> 1028 bytes quadratic-client/public/images/icon-python.png | Bin 0 -> 636 bytes .../public/images/javascript-icon.png | Bin 16357 -> 0 bytes .../public/images/postgres-icon.svg | 9 --------- quadratic-client/public/images/python-icon.png | Bin 2535 -> 0 bytes .../src/app/gridGL/cells/CellsMarkers.ts | 14 +++++++------- quadratic-client/src/app/gridGL/loadAssets.ts | 10 +++++----- 10 files changed, 12 insertions(+), 21 deletions(-) create mode 100644 quadratic-client/public/images/icon-formula.png create mode 100644 quadratic-client/public/images/icon-javascript.png create mode 100644 quadratic-client/public/images/icon-mysql.png create mode 100644 quadratic-client/public/images/icon-postgres.png create mode 100644 quadratic-client/public/images/icon-python.png delete mode 100644 quadratic-client/public/images/javascript-icon.png delete mode 100644 quadratic-client/public/images/postgres-icon.svg delete mode 100644 quadratic-client/public/images/python-icon.png diff --git a/quadratic-client/public/images/icon-formula.png b/quadratic-client/public/images/icon-formula.png new file mode 100644 index 0000000000000000000000000000000000000000..d09e4e4f25e7f7234a3724467a215ad6d54019e2 GIT binary patch literal 664 zcmV;J0%!e+P)F;0ARA*`RI!oj^MQZeTkB&k1k?>;#N(O-fRaBauTZ`QFQG6EDfn z!%;V7-)C?>Lq)7xsu8ii|6JbOp?X z^@O8>*sx>SKunl-3@OCK91&FmF|h$lso?=UPq5|3^F21frv6&D<6^Pc2W*tJNp$-d zTQIdj+;|FLOFnCOs*^eutV?=q&DO2Q1f9r+95ZHWjwy^K_UlpUv9H+LOOK5_E6VCSB?u>FF~%j~!llM;bTkb*<^R#$AZD-Ti; z58htG^yCd&Sms(7)UK9#(8m+VP^^CfIY;upiJ`qN0R`MC=!NyqAZO3|3ho72hQUOz zUpD@-?N%{7FehCZIJ}Sz!M91tk@0L0#}9VJGWX1Rgei_`tKL{*o487*yg&Gl641lBw*v-rAreZ*xhf21F_+;RNMG;mvAA` z!!zttAllJj|H4f${FUh-6_cVMesK6U&{8qPuJDNY^%O|J&ixb>u-h=tqyf7G`{H*v zr6#c_szz)T3NK9TF6-&V5&N-`L2U36gbl=o1#||nA%Q9@SpT@Od%w*+V@)Wt9x13{ yDO=&?D~^J>L4WnEqlmGBR(Sb2&Uh#4FB(4{8ov|k1|%Oc%$NbBI14-?iy0WiR6&^0Gf3qFP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBxdomsjv*C{Z^JGYHXHDW?&UkckrS|sF~>n& zWA1{IrV{2^oC?w@%^MtdF_tu?Fl({yWbaz}(4x5J$KAb$r|JH_IW2XM7^Czd<{Tcs z37lwXw(u8@(-#axotCVBS?($~(f-cAmL7-pGRYvrFx^}FCsI=GF;q@tzGb)R#^*hY z*D=23{Uq5PdOt5Xpi{VRHJ9E!_qHv@i|#Xq36%(ZHJ;pOn%ro4t-EgdQb$|PJ=%`v zw>agv&Bq;$~;~@3s-&6J1Z2T`10;e?rP;%jP;&>*!^5S3O2`0scT#-+4*9Ap-##n zxqpwHum9n$OuFx}#r;%a*8&-}2KAs7wN=Ne{q6#(Oo!PC{xWt~$( F69Ad*+`Iq) literal 0 HcmV?d00001 diff --git a/quadratic-client/public/images/icon-mysql.png b/quadratic-client/public/images/icon-mysql.png new file mode 100644 index 0000000000000000000000000000000000000000..1e3c2cdecd20a3551a4c7811e6d5ed8b59805663 GIT binary patch literal 580 zcmV-K0=xZ*P)fO~)^x{s$ZEvghL~bYxu9``-H$pB9BHGLqZ}BkQZV0P zO&(H6|ElM^?X4rqN(!;WeaH3-*@o1Sr_MlR+=e0{YZh4oJqT_`k-(kClV75^5NnJ4 zP}sC~$nm$560y!hrP0|s)@e;fK&8sClpcD(xuV0j zl3G(Lpp>UX1ZsW7Wq!P0QN|h}{=akZikSa{IxpR~id@lLC zrt({rA^YtnF`ri|zc^n=7-QG$n<=p@NJxo9+RVAx%pqd2<$FTKFWflhhL~UKMqBC| zY4eq9F!sv%nES}S2d~9IMkR-JRE8=W;|Kaj_$(?Mk5|eGo}|^i2Us)? zj3*QjxL{w#K3I3vy3$m)aKg4N{VzRzQxisR>f3!}oXSE%;N%HgAEqSG+~^pLxkpOL zr3=Qnd5yo$F;s@}jL#!;9Clo1tuyBVbGG~FbA8B03w#2;^z7kWNabr!QiU`8EmJ`) zQ+Ay9S?c=|+53fk-=wO`QfaYL(qbjsw_$%~4y(+p!$L9VV(pR^O3g9;WGX*Gc{*&6 z8If@05VvDNLe61RlC%1-er)${yX`SyLla-I?+4qA;|Spy-@D2do2wojU3kZ5zjs6b z+_##(1iy$$41(_r-?BIMnxr<;U9=S>03FCjsL8KTM>q3O=MP2e{q}l**`=;&+@da$ z_PPiToTAB>&l&8Y}oN|+Fc zxHtXZ1#?Ogi2D5vaEz`~SjYlaC_Md$Dn#K-{uNApK~#7F#hCGR z!XOaFzx4Tk-Jsb(M@TntgW3ty2^uF*Cx~v44NNCcCooP>H=uU{7Of_L(Ei@L1Y>|N zz+EmR@I(mFr4w6fTt1T3Ns=7#7qAGjG1*!)vmqb0ua(ryP+MEPE@R-i%9IvIWgRee-4!k3#jayz$>&7!loc`zT&x zK}vRxQzt>BaDdB~WM~d8|8qFWfS(b{5SPm0o|7YKK|RD{N{hBqlF53w4iN6!&@Cmh0OkP#G&Sc_Oe6UjcR0a#A9a)DlIBxh)czXlOW}qRZE2w z@!(IV@+?U44mCahz*HB`Ud9WT*2yas$GD8#pNI#0Fh}y`FF{(A66C`I(pz09uY;^l zPJNFW4O4Es&%$sgxpHA>(~s+SnJqjG6S5T?2{~mZ6TMJrWH{x>Y%9hbppzM;fQ6mQM2$8B1KtcBd%a{9(g@Bpf_6qlJ`O?LF%*82to W@eF?Od>$kK0000u;~gaKU8*jW@%$9?}4z?^ia_<_pmb;wP2HyB$aRz0}P;+&ZaDG zPY!cFJCe6|Ff_8*jWA!akhi7y;N3Xc?L&VvIz1D@;+pfBxR96SXhZ^$UOh& zWbg^XX5;MaAjZe%>gvktD!>azSo86Vii+|*e8l(Y5f2!_|tr>Pkn=?r0G19X;uGIzGM`Zsr&(?7)rir_n=@bU9Lsl$IYSXuf1c{mjMAH;BSmURJ& z_@`3;!=C>40!~`)4wifxmQHXa!rW5U#S-Rx=dUKt^dhDJHMO>UVF|N#wt=t-J$(3o zQFLuB{uAMhl|Az8EVN;^VDU%)p7ZkGGzgpUBVYw*C-iqAz5kMQrfe}aTQ^Jlmom0c zOPCYj`tN=Go2vKUQJ=x>;RsD&+LjPD{=cjJ>nz0n(v;yAfF$sLkrbXiQ$xV5Z0!Nf zNkdMWMM3tN5WlF95YHptf3N`uCkE!)TDi-ZI)hP<9zJ@^^YAebzmOI`zZm~RF~NsC z4~514sjks@0-@hCQ z`0y`RWC;WQ2LT+-!x-I6BBFDCw%}O)({T_H-H$*<)F@qedOZ*&!!8;0BOpNblZ~a! zor`7HKYk!(V|~6pa5paQlTujCMM{I8I%?OUUet%QHe4^s-hLx7OV;ms-Xv;${)(AZ zd@|g(Mqs7;EhJh)E6s(d-&u!3<-jOJse@N8MPJ`G&9A4>%x1=~fm?fZi!zaV4Wg5` zv~W`P-KJBmL5EM`yN!pAo4%V1BuB571N-%Ellm0dl%x^w{o_}-2(|?tm750`Gsw*( zBF~&cF7=-gq&kt+54ibS^S&b#agu{d3Ff!}_n@(9qgxl_8^c3x--~Z(Wx4a{Xniun zL`SQQdA}?5&Yh~@PCEL<+z1whSV?s!4ahxyUXK^ggRkYB`X0S*4yhk&FDcH5g z=@t;3-j`HpFX~lId@6+N+gGBtJq%nbFI7A`o^>e76?Xb-uNx-QNx10 za-FwIfX>%0zAHpTmwQL!ynpoSi$h3=h{B4Jn81!WD5Z(s^pLO;HQ4xHCwlc*nuO>f z9qUD+TcOv_6McM6a*pWv9e*OCwEr3QKMMID3;CbB@c%dG(6zQrL^KKE3tcf{>=hJr zL56~VrwYR`Vl;7XhOIN-WTM$)*(Fm+54Bmo6u*7IVK8T5P$l|6xOK_*4zJXlo>t; zsh&KJ<72y0F{6j!Xb#zf`uEqojvjB9+0co&olZ{hTC9FX_+KBs6ONr|nnOL%SDJZW zzr~w6z0EQ%uss2ZJ-trAxBmXswIMGfH^asjn{Btc+dYVX4b^*aOHEWX+nwt4C(m0R z3eptu&!-NWZ?B)P**orQp@5XN0e0m)ffbzz>+KSm#2fG949Yi(OoEt|m9a4`+6}_;R>%|MgZ~~`& z2?eNYr=_e+p|90}oB8Y8 zK3+fn+rm7M8f~ZshNNeY4vt0n7$}9;t!mpa^eJisczUd3sALxYTg^N5M&Q$U_C951Nr6FN`^J42y z-I#S}fpH1173OyFMQonTJXg+2Rfu$|H?@zQVxSLXQ?20JAUCQt#a2uH2l&@jiEEAVN@@q+yF!B(rH~!v79`QS{5p91h~K?1HQiqzZCE z5_&QUIjt5~OiKBkaTOy%^hpn2Uu8FxpC@(nfFUHRfw3go9?Rf66Q7X=5Jq34lB}&w z)qB?gtrx4MGg44zQe-b1Xl?R&d0T|H@#)0|^KQvHf@j}@XQmHc+68%js5hj;V`{nt zPFLr)MPNT2FE$9I9Kg}j#?bo1du+%Pw!2ZB?DQ4Qa?1DX2=DV%(9@*;*WUiS*hIa@-&tKz#pksS-l)sxmr8pMZl4rxBErFs<+q~BvSG>p<(>vVSeo2<${rCoiqoaA7LLDJ8x zeE6QKp+VJ)Mi~noI=*A7;P5BHS~hU5>g3H;e=(1a%zh2ojVY!LOhhnqvR<5IqfP2F zZE3$R{;gX>v7KwBhe|P->8`Q}mZ-AjnlAFhOyM(W_0wrVdyeE+%_X}Gjzos0zt2oG z$Hjy6Seo;|6uFHR5IAMD2U^`SrW-lRYl|uR+~+h|!L3BYM`q$FYn@4~zys{cea4t8 zC%3rj>92t3$au2sKrChq_f+EpQ0MRjF;c%w=vT(mq2SiJ)wY=1$B#6~Aw()3OH(OZ zQhAhvZ+X8*Yw2Ln%G^#KpR|CnByArOQy1N0bQaIB7#JP?5+WuBkyixT4XZltRaq9= zxO^L4$gQ_8yEt{^DFTt-1n9Ke4c|5$VB*jHV3Dz$2dJf8F9#^ z*H1Y)c}fJ<+N8mT&ryu`5o(y-ViNXV#(EK;`>C!jV0DY>hZiT`*p}qj>%1Gcz-&%^ ze2|$be@~i(Wei`FhdESs%qREInvTKJCPj(Trvo=o?fp2_HP|E3vTZtJ>MMoS@4dYn z(nPk%y)GkR=v2fNbjZjizW-{~yXfoZ<#Vc1`$Zt>$*C6e7Xy+aPPvD zuk%D@{M@VeYKm5To&H8Ju%x#YUv?2RGw{hH8ycHiI7Q$a^w1ba&GdW;gIqmo{<)eq4?sD(T^^|`cQDAa;;jC==n z5tYHkcH0H1W>-}9VK6p>TZvmKdi1d1N107eIZu$UOsR$<=f<{bLpeL2?#|+8*VAXr zq7bD=K*_j=_VVbijChB{7t^DkHKzBJ2y7-%&riTR6S{P?G_*Chpg*G5zMxs2m&PCT z9BEl*WNaE2+gcIbQV~5uFWlPB1;vaXa=0yKel7d(o0;NDii?-&=@y45L?!Z!f8lya zLi5N(&5bME4+?kf==QgmOJG&5hsMXO1t0MxD>dtp@H;t!&y_l$syBPeGkhWjGL^>j zPCrj=b8|i=IhWoP%$&w6HMaJ4&`htlviW#abrev?s+$z-5PvhPf790_lkxSPI<)>$8VA098To_lyCxFf#beJ-!1JOjMO4JAwT8c$7SNm#B4#d*L@ zw!y3V>!Z?MK#@{@ibvZ3qNPEFOp>g{Osq?)w3)PWrbqPjcEXkLwlz^5AnWpkt84hL z+HLk)eI<;}2(DmKs3|4h`*7oudsVE;P&kj{vz4bj=XH~GCrU|4xul7*b_v-Zpdsjm zZ3fThcA|&q5!FyD%&J|#4VQ_n}fFsV}CAR7J6L%7GRz9(f3{aa{&UO zgS}r;*5-n?l9^_O$B$k;E%|k#wGuH}-p^I4B3{29aAiwHl&{a2Mw81v^t}96+7uik zshuh}NlYYoPm@y2h{xUGKl!~exf3^42a_v;T4%#0xb2*uOWi{))UJzOdt;raPp^z& zWJa1|5Q{&5(&I-HE52v^Y{?wrYFBNn6F6LGokfdgd?1B21z&$cN~pZI6r}s{@_B?k z+AA}2IIE90#ZbS;Zd)m13fxIeQ@bmdwG!`?1VRMFv-;ebfb5*5KAKn@r3<=zj)M3y znsH?bK1A=<)eknfhbQlC?xQ>s+@O#sVWK{UoSn;$d$U*rjor^^4#pF{Rvf3xT0XH= zu0CdC2K}r||K@1Y6v2F$TD2E|r#o9gi%M~CS^c$=9p{5>BcxJL-@BCv>*r|O z^0gF6wW3x21C+_1H>CLU09Z(WEyU>h>nn9!xS*n{PL`Rf_BlDj#dGE(RLLg=DZO?a z1n-oxo188EIIn%C(ini{$mOv0Y&RpM2&^4A%GXoi}L6VeLw znXRRZ8KrUDGkCs*;Nh#+otrf$O`+4JI~u@rLV0{r8IW$fzpafnf<-9K>|8tB&fama z{>NZs@*JMhb-EFy3f+oZX*Lqj56R1LyfEQ@VM+aGUBek?nQu&PSs&$|-DhihkLJY> zSZUfyh%1#r9(gYINw8GLy`%)fy~NIuNmKF#s>yh`v_pIPr51BNv~ zn)d5}E3)O1gvtS%4n}7hGTokwSZjUqml#Ry?D2L?T`}8kg*I@{#pAk25MUUSWUpjc zfA{7Uxj6voGBe*q0raTV)~$i}{bn5GwPn9; zddkWMkWTK0T8ZrNzIP3`fgGfkmtv0K4Ac=0GAMzvMzG>luwnqhsn#*Fi2LBR-abyQ zQp1J~&X7hVk*)5{R}0sYk@@E6PIyKWaPQ2M+Dv#K<9(wa{?sUjJv)vh1CKaKFUX?E zf4z6K59xghLYCPmzGoH2LoK+cpLEXFH1g=uLQUXUU)yeWW*@=V=C^OkGlONzmZ%R6 zPhJpQrXAhw5OW0AKhl~&psazy6!fkZxau}9P@-AW<;x>?1kX%dKa~CuvT=W^fRnwb z(o3Z7^<)56Jn8_bv0rSq;SE{ljy>JMP6j02VZsvker_pQ9KP zJEV_g=hBs9rY{S^SUk zwnR+g)VZjS>Z=Nn%UNXU12-C?Txk~zLQiAv_`bt5(ZndIQQ_n_1IGWg33Q1<{n;w@hu-SeI5~ zrDtZRS0*GhmA6^%J-WX79)ETV(5WY%=5QR#dr2DK{>Buj^lZDK!FRk%`_t>=Dii^X z;3#@CJp-}1ub#uhPjsd?O|{D)r+t5X@Kxp`SBlKQ#$6!ywkTn`EpUao5SqCl zUm!z19uVn z*;PtD+ts^_rd4$tpKIBbc1+coBS<43T%H_p{ zOp2Xi>WHF(Xu{5CAy@2gpDcloQn${fkIf0Ih<~Ya)FpNORV*Vj2>*x@FDo{v7<|Wv zdz?b(-3FI~!QsW8mwCZWyxO#5Nc#g%{I>)-Ihf>(ma%G-BtY92E*p#Q6iQhX-N&k! zp&sApQHqm$u6!SoZ-ViJnW7%gfndTq30uL!f%#%hx@X81$X0A+w9=&z1Mw)WpWp0O zp@?Za8?%jirI9%(di(TOS#=?|`dJtNVpefi$-~Gk@y)TotS2PrMCPTQSqnl}G6QO2 zU$PAPZ#VKuKa+!t0t3IpG}Dn137nEcZ(Hlli^sS3Apr^%JDa92d(4*}CV0 zXUU6c9($bQ8yxCea$M=-wP%_P?5A6NrnwQzAS7?cvmx7RH`HqEQ3I&ZK&%q^-enNh z)Rqt{iScp&d3~0 z6E2JIARx9C09vVVGizRXMn1`yjx^(>jL^0-H#gxFFY~QYf9C$jY!8f}b$6dW{uSy~ zS{N36BX16~mZlocRX3SLPSv{W=)tB73=})}t#65X-e4tSFxgV!aXtu>kJkkf^O>-1 zI7e&^a3Br0#QJ#l|3=SaaR|0uV3wLOjnSw+#qBxYNBI|`9CH1 zngEUQA_>WTs*rq^5Sn{2XW?#%R6foK`<{cG3W%~-eG-PtWHk94rA!vZS)^HA^;q(x zYjdl6X>wbn8wg^!I*L*1UNwnMZLJefh#lr6nAuj!g%6%qcPwVGf<3y#8A+}73M>zB zmAchG#BsCFQSaug#iaU{S1COMvDAfbMhVNM(txhSL+D^YTddpR$yPyAT?j_v zt0NKMaT|^*nfws@bXQ)J)p5XKB_~7I$O?Nc1K2~{M|!COgClHnTpwFi^bJgGJlSht zR^2$g-r(!!*FVdpenu`i22_a#E4|P95}muxI$C1A+#0y;;1T=yZ))LtP39@#_%gu8 zCA=>#bwm~;j;ey@DvTyt01rRFBVB+u`FK#AOzhZ%9Fq>1$fO+-BC|u#%Zngx0C(SQ znCgyIs*kj(`Z-mdyQ#o0aqBaPmHECmd2|;fFIL}+XR31PxxMW8LZZ|(TN2G}a{KHq zUOmwO3SgnCeP}w)>C1Efx3%qOFU7WmoubxOd!{|DqP~*!i@tYz{l1T`6WQyGBBdYP+aOPvEd~+sh`BMZ=V8PP&!ek= zhiyK}{4l+Z+0{0Riex?4^bse>Ana}x#Du#&P8$pWJ|1MGwpY0Lew?rT2pym zu~TtXUa3lSBs$I?UYlLg_pbj681PNAJ8`>nQFMsHei;8_WrZ72xfdM?{MPB-D~e3w z5l^s(s>4(%98TQ{cB{Wob2*7YNNG=pn-}h-hTFdB84k8E#<`$OC*-x;86V=|+^Osp zaY;0nI>W-r_PA3MBM$j(4BT+xfNx+|f==D{ek~l@sDdAk(Lv(Fx1x^0HLpe zau~{S;#2te>Ew)n>H{pQYGQ$kml@BRXL*Rtj+1j=&&bdBFVoDzb_i&}ZMOT?o0ESNz4XJkhoo8ZU+jZVims_cxAi3FKGzNyb zoDCa4Q{5xv6oAP12`pUC3{#s8&HUb(i}3It1?FCu39Ex2(!+UfIlB<ngn zxkN{HW$bFlArT&*^udFtCJr16a%IcO8m-@-W)4$lJ<7mnD$;o0_E;T}RG}!&JX>phlA_&Sq!atIntPPfRHDWo=k83}+=VkBpgiHF9-+wY+Sws~X8 zQtpGp#KJz!6-uD4HrG9rRqfXJTQXHHvF*uM(>sqZ^Io@;K3w|AwErGtTZpnY_YxKF z)jnEF2%#?*eIzd2TGVgHDHpD>Wveske0?SNV16{+y;Iq4!OHh2H&(#S}^mrLtCwZsLk8xeyOi2C8R zWs}%*vOEcgG3}jpfw7T%an*;Sq(B7nn!~|J;R$1hUqJ!NyBpTyd5;y_KyIBju*TID zB!k{?VI3B>T_;Q&G$xKcWb3bb{$aZ@km@}*KBf+)^CI?8k_lovGq6>s8wEUFw>os6 zCo!3x9b09$xoUU)(znYX6CZ5R*!!p0Vn>x9grb~a9?bc$CCbDDIj-PMwz-vi^(Ec= z#zT##_Rp?-`bCuE>BfCG*qzjdC{7{XOG0PW;G3g)9O;4GjrCBtbE6rSYA+=R;6}dI!a%LF`UU{%E7F8{VK@e4QodHupeEi&iuRDaMim^ zhVe&>F|uE6jEtwM_0G0nFp+aVy8D}D>zRKT=oF<^H>8`^TCsnb&ND0mKe9 zmZk^V-L(% zgh@GT5H1JYtMjxnIBjL^Fbt)3| zt2LTjIoUhmcHd6xFZ-0E27uz66#*4! zYJQKz6EbQnz5jIum$n1ORCm*ka62x)tlt2#XA=5?19I1a`)1K6cN;4=FL#09Af{qkH9^LB6}^TS-N{xKtfj>|V>)GnT( zir3K4&NDe`$jQv6P2tSX-e^2%(byra+otyh;#Ipq32oC3-1*C!MClsES&w}ViTe0x zPD&+dLPR0WcpDy{y|FZ0h6#OT7ooy+d8B0LSoK-NFigcFmfggc-e=p7$u@yI3lz?_ znumOkxJ4I=Ie}Z3V>*!wM=|xyH{I6O9&phZKldftYs-7KI7FO%3cZ;-$8|>~yig8- zt?*l`_M5x{>UDckS!2x<58B(I9iwT=zL;3oOzrPp#W&a64_c^H`=s_PHb5L*xBh&j zI@Hri)PXiS@>1#m*4=z4oe*SP;$_(!IyB+K!8e4|^qA5m=5=bxNKWyBb-3oZRMS>J zQ*3*$ypcofxt1RfVg$St$|mAT!>T5EedSDHkpXzO9~9>hU))O=1O>V5QxB(^uXrI? zPz{0PdXN65lhF|4(6x)gM|)#qp0pM5b%pn;YwG?mYADWo>u;?@wQ%0z321*H;ow8b zBYO!?K-MpR&xd%HrX1Pcr^EEg-Px6%{uQq~!6UqWueSz<*VkV43-l_LH}yl~ILjC6 zb7CDb7E*bA2=9VHl!at8w#kb2Z#o=s?LgBf^-pQw{KRXthtk)v$=v!%X@{_5B$H73 zf+u@S&QOciYn?N@&$inpD~MaLpiOnH63+j8ucb9L5NiSwGkNqkDW%>10639ws&}7Z z5Q+L>uD3apFpSi7_OTewy{jCjpUQWo#@9sa2CMyK{#J)lng?pbw54}0!bD0p;j*HF zYz*WuL!;xtZu{6X7049mcEl%RM^f(QJ86 zpd+h&Ur+XhaHS&VK^e&A!1L;_iQRm<6!_!C4_ziaA^nzQrXZ=4LB0EXBLgitRph;2 zCxCQ`;ego>vi*F681PE~DLV1p-rwU?69xObM<1XZzH(eQ1`28od~jt7C+1Q3^5vR- z|E28@2BR0xx3hCK662SIkUBfxdn+@NY9j_TG;~bE6V&KJcdPf8vckF|Jod&%kvpp6 zFUf;rbH9jf^|q;?iy+b;7%O*!ySF5>vaYb3@;L`aNA4s}FC_JtV)v`sPWz&)rxsY@ zNyeoX)_Sx?xOJQla82X!8dV5nHjFfnCnLSKmuU-Ot0%j+jQ@<^7Rj2#$8~A@t@h87)~Vws0mAJ< zFqO@yZt=9}7pc0C*%UnIY2t4CNB5M2FL`B;a~FmW=Dz3DIIntabA&WGVnQi)xY#!; zYo+R5ud~PRj>rFub2i*~)*m3XWn|OycygOWykYRZj0~(cyCX?920fWJL84uCb>)Rf* zhEu_r^(P)L5_jk!B@8c;Osvqx(d8(|KeEY^gaibs z7vJ(@Ytzs?$i|yxK6`@%;SCf4e;x!g=XCtvSI>>|3eeh*ks*&SODUb*o!8Q~mfC2P zIKgjY``qdf^rQHRz=)H+(mQ7(mfGd{eDE^F=?=%v`Tg}MrLmwfz;nlbtVU@ZdR7dR zDRbJzt%54`x9HXf_u2FPnC;1GBNqa(){0Ws@L+CE(c zh-IycH2A|HVss;*0-G2XVmxF#US$D8UiNpO$HYE)Lco2R= z)R|fhsDyT$JGx}-+m}(XQY@Hokm8fRfYE%Qdpet6_s0mtLAKuZ&HKFvsV)pDhZ$3r zw62ERmiKA4eoH8Xr@6rsJ@LuE7WAd>>)a=SxPI#nl z4UxlZ@;ho(?aHG^rXwb>Mn_G4ykoS~I{vrUK!VPvzBxx6G}aurXAew`v?On;i27>u zo*vs3EY>b|sJUmXR;~VlPM15|#T}tCdk?jl#;bG6%WqgWH=bYqbm1*Dyt97g(0QUx zaMQPLo;&wozD6EQ${yP84?st9@!31zU&X;BsTw~b(L6><^ID$XaTG#m2rRog9y={K-50)nypppX%zOv*D?trveJoBA274>kpf#g={u2QV$+OKbQU&@B{xj{G9;~xulgToWfCZ{J|7vhX`hhnDf(7Ojd z+S`ExJp)oLgZUvkfu+!p`*GvHb!f|Zyldtl`-?BYZRE_Sw91t_<+mH>Uau7SxN>kBGQKq@vvg!MNb?Am_Jt?u*qusf*3+J@8 zUJdYVQd7S?aM$rkiAlD!yrlh*MKZORJEN?8=0+&aL1xH*CHu9J@ohey3gNh49S>G) zwB*din1&QNErZ%Wkolo9%Pg#O%JUFOOcuH%roWajJf|V-QweI{le2Gdkma%87>M|h zpBsy$pT?}&9UoV2l>547G%s}IGW&l(bAa2q`rCw1LB`Q_TDp}UKM=nwFqUERt#Wle zr&u>HRKcuIy7Wd7x24`UnHzzx%mnVhLI(8}rcpjy>fBr89ji7GAlUbF{@;C zcZ+@?2%ilo4ZEU7GuHy|Ht5_QQyK)q@!W zn|pb4;ejGvQipGR0`#6nsVx0q&&yE1ka}d|s6rxOh|v-Ce|YE8g>0upLu+f1(_qeI zeWy33nWeF5mtqGnVU* z7>rCzv+8!n*$w!FQk%{%_ppYOTG6qu8amOB>v2%e%@tk%kvA&!K`V<5$voC~G;5(&YYrDUQsgo4f?W#tW?(lIF2T*@yRauEjrB+MIeDrRx z_mB^!M}7}gJpT4@)K_9?I(rLk|l>=yeZ@OeSjly{r_{J5VuY_h?(o*VjoP4|RE@c9VstHzQ9t zzeYmvbvLLizem0Ae{zOlCDGbyBwzh58;O^B62p^Ji$e1BTr~Dt6oTzZa=CvZj7wzBOV^i}kWVR{Hs*!i zW@6kLWdV4j+b*_tDg+hbF@Q(l0+QJzC;eh}` z+LWjK$>HVaI*`+7M~9yz-G4Jj$BOykzfLYc&;%zCV}-Ia^|Z_LS#JP)cn z$d%oY<2}BPI|kE*9QFn$$E}l#3-h313jxsPS@wcrT&kh^&oyqPL+7ro#XEX6f{2wQ zkh9}a+Mk?L*Y5ybK$3gYpleitE}wE*_%O8}U&EYc*y8wUu(t}@{U`I0X{#;miMMM- z#XVi%wp&_kRg0jawU+A#SK>9Fvik4WX&zM^xmZT`McuJC2osaA+pfGi;y7AnoqC^X zPk-dSvIK1HcF^)$^GEpoyZ*y|pn>spK$sGLM_C|m4m6!Cv|9IDua1cwecV;554GW8 zfWyAaM$3bi7}e12r5Z*TKUK3~KSv#oNep=4I@d6bFX4b)rYh5F;&&v_6!;OFq z?WQ^@PU*e-E&@>^rIcRzHOvug;#FtQ$ULC=#BM{cx6B)VCmfCNpsBb8+Ej%(wC%B- z8}kqIS~Pk=w`>I3>O}6b^alu{JD#!rUh92vk)mYaBsZbG-E3CWyc|%CfcumGI2bkh{OziV(Ue?K4Zc{OuAXk;~ z{G?!ro>?bXpPp*!0rK$fTZi=%Kaf7Bb9nl_ZrH|}g!T2*2-vVsjn{xGe};(gDtn%6 zG=qnl!*R#2F_vmtd{qW!yGlD_PAJfj?k+wAH_p12dV28tZ%OFeV18BY7FHD`qwrs{ z1TnE#hJhT-aVe%p%QBjBzRbh8sFtrwba~uD3z}?>RfnoPB`Kain+@W?wtszY^{fNs zwQxK0MyEfX_clBSzr~+^qzdk45E;Qa)>=;wOM>3P9@M~cYccG)^xc;Isk(`H{VHc!5jtwb=TC0#prOVO<$|tRIqW=^X1A`B z$lmMM8wf#N(bYn5b)_iYEi{f809{(Aw-wJj6W#W;u!+h?A`2^N3(;-aSD%s za<&_!^cB#UVK91lBF`HidiP-+h|ZkGIRv?UKJ@#SlE`8eL4)DN3&p-APg>KQ@=;Mu z2x1ae4Ktu(O>156s=J29wsy%=))~z%nh(E_k4pW?LWUJpj8`=S+tRD$u66O|rlI05 z`w(QV!I`CNL%>n!CF?IFIqBU)ahHYxR?8-?$kY9;1dyBw8nU$PJ@1IA`tB34Rw#AR z)4B5q6Y#w@sEP24Vh!S|-X%OCCuAQiYtE(^eGdLZfgy@b;}g8hW*$EDU}4EXi6Nzp zjD)q>oOR=dWK27} zyvF|abqJZJC(w*|L4kM;usZ(>k z2a|ENhFnrZ?Hwhgo4FnVI(5<}?>y?Ah@BpPr)KqEs1y6Hdf#5o+zcbeM2i_zY~JKd zWsg-o@QzHjjNhV8bNB - - - - - - - - diff --git a/quadratic-client/public/images/python-icon.png b/quadratic-client/public/images/python-icon.png deleted file mode 100644 index a4b042b0235ef6e19346a124db4b89832ee8eaae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2535 zcmVM@@e|T5GUIpkXfUDp_1;L6!>ODu}6Ip@R4-04kW%tP~Qmwr6b1 zw(QLJo?f!C*G{aFW;B|S0K+g0!!Rz9M_3R<-Jm! z@*GE7qlvzDPBL;4fIpC)rx=+y-e)0@<^^6-CbmE>0^qO^q7b|lPU|z8NJ=gOFvP?7 zj*0L>@D8#n$ig|4ivSGK9Zo$xD>;1}GGKB67*H3Mn3h5~KDv!kMGif{Py#S@4^UzT zWH%XG_(#l`y*kbshV5nK>+7l5WLA49=Xhk=HY!Fqt`;&nJq^whQ1OKrjT zK``-siDNF}>EGYa{w`C;6ebmb7=`mEf?l@l7(GsVqJPGRyu|l02&EUjqk>~g|J#=} z=0C5`E$lZWAclSIF)Ss*216|O92~-r++dlp{JGl$a+NdPD$Z0`8x zFe^LFi4nojM0OG~(VBLML+$YtxQ3q-Ph!_0;>+*BQA*f~* zs$pQX^ml0A@_EQo;5equeWGT}?{`1qh|jjEFuc~0gI?{jYy3I}r}l^1rET$o4ja1g zR^KUb;6((zSlRiMf5x7+L`0J>Dtm=LNb#!NVl6hVg)r1b`iKk}BAt3l#K>gnGDf zX^bHN@je=(uvQH;`2p6Dk&+;d;n58BYSloK07xm@w-$#tJuwKj5KgPD%Sr)=U4~@X zWw+v0W{f!;{#Q8Ox(8UH8D43tfYF2#j;HiG-U`RrSaekfx(dK27!rR9$CC*_R0E}O z+N$cqIsuSUmkSuR#MZVFPAicXNexu0^v7*ZJ|Ssf;sSXISp|GSTBJJBt;3`l|HG(- zbS_$Eg@lmoclfHkkn{uYU{n&Hoo13|kYG{8z1{i$RH*=|`~MHdPf*;{0wHEXGyE$s z45e^gLz;54s3Ii4pOY&sj$s zEF&28)xh&7Qt$la{DUqwz$iTcg&-52ZeB{T$*U3E=ThCMY=B0_Sb`i8Dpx?B4?nNv!-0<_%W@PnCb1!;EP*0x;9cKP>9N z#uq!tK4R{hOOqT1?vNO2mwRx^&X*Ttxd=ay9+>6=d*K>Mt#S&J7}7nfD&E2P3F;7t zT4e;5XvQHeZ(3aQU{Pni9qWWR;rIj9RAmE5lRXZJh^=_7JiSEb`|xH!YcKB9J-{2~ zc_pm!HAw2YJpZ7J03_MzqDAX3VU@2%s+$MBdzAo4{^V3f;0xt>R{46Qx+x$*A-F-p zp(muMDb;ZUSn@jknBsTe4cf40aCRxXsBeZYCkd9HdwV^-`c?lvERy$vj;Jz)?TQn2)}v>nN7Dcs>rjdb{0NyX+1&i-C|iFi zg{3tLK>X(d&D^geY(g*|@mXt@e-oW)ny6rnN6z_jN493*;82Lj(k$Gqa;Awvq1+4j zoBo5aFHjTQ6RzWC5FTi!C%OKy?-MF(7e)~tj-LI8y3EXkvp&31kkif4U0yF^#u9C38@2F4rwj*-*EjYhaZzUnfC-YGdWb}=jj7PK)v1wsPqEjrLSJD3f_XsPp7tL$zp zVT?(E5HQ;-XtsB9XuSEF-$y6K3Sqd|6r${4dk->2i*vb~=df-z#y_S?Kuww*97hUb zFb9Qb-%FoKEVX0xA<1rm2xDAK04|)YRtOmZ)|*|rUFXXU7PxKhMG1g%dzGJvhV3FN zpD`8C0A0X_*(LzZaLadCFeU&s#JGXQ$2*z;)FUNZEP`AGAP1eBbTO=4nQ9bJ3dfVI zLedkbJdZt}2RAK$?^y{>t^&aM2G62vId{5Nc!>;{N(E>tc1zPP06DbqmB@fe1VHJK zxetGyFyCP4t#L1V^G(ukueP5ml-u9;0}Y46r9--fq7H1Qcxe8f?mSt9SUj4~Mw@R!JLUkd**1u_88D&ScDe6w*w*n)yJ%*xJ*) z*^%k|e$YGxf%W}0jC4>CgdOD#S3DcDiwz;0910fsE#=iK>?n*9W>@jq8G&t};0RhH zY$fk7E(OAbPBDiKBI}s70GvX{B~y6`TYjH<%*|H(CnV0zo0`Q4z&WuyK8)iXj!{PZ x{t6Gr_T|Ydm+uIb&81vQg&T%p7=~dm{{zNmfdf+V5Rd=>002ovPDHLkV1gFPwD$l2 diff --git a/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts b/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts index ef4aea3ddf..85fca7c634 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts @@ -20,16 +20,16 @@ interface Marker { export const getLanguageSymbol = (language: CodeCellLanguage, isError: boolean): Sprite | undefined => { const symbol = new Sprite(); if (language === 'Python') { - symbol.texture = Texture.from('/images/python-icon.png'); + symbol.texture = Texture.from('icon-python'); symbol.tint = isError ? 0xffffff : colors.cellColorUserPython; return symbol; } else if (language === 'Formula') { - symbol.texture = Texture.from('/images/formula-fx-icon.png'); + symbol.texture = Texture.from('icon-formula'); symbol.tint = isError ? 0xffffff : colors.cellColorUserFormula; return symbol; } else if (language === 'Javascript') { - symbol.texture = Texture.from('/images/javascript-icon.png'); - symbol.tint = isError ? colors.cellColorError : colors.cellColorUserJavascript; + symbol.texture = Texture.from('icon-javascript'); + symbol.tint = isError ? colors.cellColorError : 0xffffff; return symbol; } else if (typeof language === 'object') { switch (language.Connection?.kind) { @@ -39,13 +39,13 @@ export const getLanguageSymbol = (language: CodeCellLanguage, isError: boolean): return symbol; case 'POSTGRES': - symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languagePostgres); - symbol.texture = Texture.from('postgres-icon'); + symbol.tint = isError ? colors.cellColorError : 0xffffff; + symbol.texture = Texture.from('icon-postgres'); return symbol; case 'MYSQL': symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languageMysql); - symbol.texture = Texture.from('/images/mysql-icon.svg'); + symbol.texture = Texture.from('icon-mysql'); return symbol; case 'SNOWFLAKE': diff --git a/quadratic-client/src/app/gridGL/loadAssets.ts b/quadratic-client/src/app/gridGL/loadAssets.ts index 5222b4fe74..f3b4d2835c 100644 --- a/quadratic-client/src/app/gridGL/loadAssets.ts +++ b/quadratic-client/src/app/gridGL/loadAssets.ts @@ -42,16 +42,16 @@ export function loadAssets(): Promise { addResourceOnce('OpenSans-BoldItalic', '/fonts/opensans/OpenSans-BoldItalic.fnt'); // CellsMarker - addResourceOnce('formula-fx-icon', '/images/formula-fx-icon.png'); - addResourceOnce('python-icon', '/images/python-icon.png'); - addResourceOnce('javascript-icon', '/images/javascript-icon.png'); + addResourceOnce('icon-formula', '/images/icon-formula.png'); + addResourceOnce('icon-python', '/images/icon-python.png'); + addResourceOnce('icon-javascript', '/images/icon-javascript.png'); + addResourceOnce('icon-postgres', '/images/icon-postgres.png'); + addResourceOnce('icon-mysql', '/images/icon-mysql.png'); addResourceOnce('checkbox-icon', '/images/checkbox.png'); addResourceOnce('checkbox-checked-icon', '/images/checkbox-checked.png'); addResourceOnce('dropdown-icon', '/images/dropdown.png'); addResourceOnce('dropdown-white-icon', '/images/dropdown-white.png'); addResourceOnce('mssql-icon', '/images/mssql-icon.svg'); - addResourceOnce('postgres-icon', '/images/postgres-icon.svg'); - addResourceOnce('mysql-icon', '/images/mysql-icon.svg'); addResourceOnce('snowflake-icon', '/images/snowflake-icon.svg'); addResourceOnce('arrow-up', '/images/arrow-up.svg'); addResourceOnce('arrow-down', '/images/arrow-down.svg'); From 937b04c8216ca1257b518d399a2ea09a08b5607b Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 26 Oct 2024 06:38:00 -0700 Subject: [PATCH 157/373] fixing capitalization --- .../contextMenus/{contextMenu.tsx => ContextMenuNew.tsx} | 0 .../src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx | 2 +- .../app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx | 2 +- .../src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/{contextMenu.tsx => ContextMenuNew.tsx} (100%) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuNew.tsx similarity index 100% rename from quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/contextMenu.tsx rename to quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuNew.tsx diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index f44b69c98f..aa77a0a453 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -4,8 +4,8 @@ import { Action } from '@/app/actions/actions'; import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; -import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenu'; import { ContextMenuItem, ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; +import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuNew'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { TableIcon } from '@/shared/components/Icons'; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx index 975f6f33c4..50e64b4212 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -2,8 +2,8 @@ import { Action } from '@/app/actions/actions'; import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; -import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenu'; import { ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; +import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuNew'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { TableIcon } from '@/shared/components/Icons'; import { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 5e6f2762a2..0a83fd444c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -1,7 +1,7 @@ //! This shows the table context menu. import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; -import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenu'; +import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuNew'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; export const TableContextMenu = () => { From 8cb426ca4845652ddad82d6084a870385a964bd4 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 26 Oct 2024 06:40:20 -0700 Subject: [PATCH 158/373] clean up file names --- .../contextMenus/{ContextMenuNew.tsx => ContextMenuBase.tsx} | 0 .../src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx | 2 +- .../app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx | 2 +- .../src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/{ContextMenuNew.tsx => ContextMenuBase.tsx} (100%) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuNew.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx similarity index 100% rename from quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuNew.tsx rename to quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index aa77a0a453..dea5aebdab 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -4,8 +4,8 @@ import { Action } from '@/app/actions/actions'; import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase'; import { ContextMenuItem, ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; -import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuNew'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { TableIcon } from '@/shared/components/Icons'; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx index 50e64b4212..9fa2ffc2fc 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -2,8 +2,8 @@ import { Action } from '@/app/actions/actions'; import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase'; import { ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; -import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuNew'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { TableIcon } from '@/shared/components/Icons'; import { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 0a83fd444c..61c3906f8b 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -1,7 +1,7 @@ //! This shows the table context menu. import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; -import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuNew'; +import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; export const TableContextMenu = () => { From f60c03819156b0fd5e6c8fc70874e16178e2348b Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 26 Oct 2024 06:44:58 -0700 Subject: [PATCH 159/373] ContextMenu is not correct --- .../app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx | 2 +- .../app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx | 6 +++--- .../gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx | 6 +++--- .../app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx index 9b3a8a1a29..6cd3d9bde0 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx @@ -9,7 +9,7 @@ import { useRecoilState } from 'recoil'; /** * Wrapper component for any context menu on the grid. */ -export const ContextMenu = ({ +export const ContextMenuBase = ({ children, contextMenuType, }: { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index dea5aebdab..8ecb974a34 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -4,7 +4,7 @@ import { Action } from '@/app/actions/actions'; import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; -import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase'; +import { ContextMenuBase } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase'; import { ContextMenuItem, ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; @@ -39,7 +39,7 @@ export const GridContextMenu = () => { }, []); return ( - + {({ contextMenu }) => ( <> @@ -86,6 +86,6 @@ export const GridContextMenu = () => { )} )} - + ); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx index 9fa2ffc2fc..f773ea310e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -2,7 +2,7 @@ import { Action } from '@/app/actions/actions'; import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; -import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase'; +import { ContextMenuBase } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase'; import { ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; import { TableIcon } from '@/shared/components/Icons'; @@ -16,7 +16,7 @@ import { DropdownMenuItem } from '@radix-ui/react-dropdown-menu'; export const TableColumnContextMenu = () => { return ( - + {({ contextMenu }) => ( <> @@ -38,6 +38,6 @@ export const TableColumnContextMenu = () => { )} - + ); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index 61c3906f8b..d862eaeb40 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -1,13 +1,13 @@ //! This shows the table context menu. import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; -import { ContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase'; +import { ContextMenuBase } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; export const TableContextMenu = () => { return ( - + {({ contextMenu }) => } - + ); }; From 964f1eda7c2be415271eeb6d92223c898f367c97 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 26 Oct 2024 07:58:42 -0700 Subject: [PATCH 160/373] fix a few bugs --- .../src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index aed2084894..b91ec7156c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -15,6 +15,7 @@ interface Props { export const TableMenu = (props: Props) => { const { defaultRename, codeCell } = props; const cell = getCodeCell(codeCell?.language); + console.log(cell); const isCodeCell = cell && cell.id !== 'Import'; const hasHiddenColumns = useMemo(() => { From 3d77a72ac7e415cb4a7f1c29cdb2352215da3558 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 27 Oct 2024 05:56:44 -0700 Subject: [PATCH 161/373] fix visual bug with table outline and floating column headers --- .../src/app/gridGL/cells/tables/Table.ts | 16 ++++++++++------ .../gridGL/cells/tables/TableColumnHeaders.ts | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index a7bde59fac..25440bd0a5 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -93,19 +93,23 @@ export class Table extends Container { // places column headers back into the table (instead of the overHeadings container) private columnHeadersHere() { - this.columnHeaders.x = 0; - this.columnHeaders.y = 0; + if (this.inOverHeadings) { + this.columnHeaders.x = 0; + this.columnHeaders.y = 0; - // need to keep columnHeaders in the same position in the z-order - this.addChildAt(this.columnHeaders, 0); + // need to keep columnHeaders in the same position in the z-order + this.addChildAt(this.columnHeaders, 0); - this.gridLines.visible = false; - this.inOverHeadings = false; + this.gridLines.visible = false; + this.inOverHeadings = false; + this.columnHeaders.drawBackground(); + } } private columnHeadersInOverHeadings(bounds: Rectangle, gridHeading: number) { this.columnHeaders.x = this.tableBounds.x; this.columnHeaders.y = this.tableBounds.y + bounds.top + gridHeading - this.tableBounds.top; + this.columnHeaders.drawBackground(); pixiApp.overHeadingsColumnsHeaders.addChild(this.columnHeaders); this.gridLines.visible = true; this.inOverHeadings = true; diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 394c2de102..792f10e534 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -26,12 +26,23 @@ export class TableColumnHeaders extends Container { this.columns = this.addChild(new Container()); } - private drawBackground = () => { + drawBackground = () => { this.background.clear(); const color = getCSSVariableTint('table-column-header-background'); this.background.beginFill(color); - this.background.drawShape(new Rectangle(0, 0, this.table.tableBounds.width, this.headerHeight)); + // need to adjust so the outside border is still visible + this.background.drawShape(new Rectangle(0.5, 0, this.table.tableBounds.width - 1, this.headerHeight)); this.background.endFill(); + + // draw borders on the top and bottom of the column headers (either active or inactive) + if (this.table.inOverHeadings && pixiApp.cellsSheet().tables.isActive(this.table)) { + this.background.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 1 }); + this.background.moveTo(0, 0); + this.background.lineTo(0, this.headerHeight); + this.background.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); + this.background.moveTo(this.table.tableBounds.width, 0); + this.background.lineTo(this.table.tableBounds.width, this.headerHeight); + } }; private onSortPressed(column: JsDataTableColumn) { From 312ee4ff6dfb6571ce89e30bee777278ead6b6fd Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 27 Oct 2024 06:31:21 -0700 Subject: [PATCH 162/373] fix bug with table menu in grid menu --- quadratic-client/src/app/actions/dataTableSpec.ts | 15 +++++++++------ .../HTMLGrid/contextMenus/GridContextMenu.tsx | 2 +- .../gridGL/HTMLGrid/contextMenus/TableMenu.tsx | 1 - 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index bfefb356d3..76d7abd08a 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -42,20 +42,23 @@ type DataTableSpec = Pick< | Action.EditTableCode >; -const isFirstRowHeader = (): boolean => { - return !!pixiAppSettings.contextMenu?.table?.first_row_header; +const getTable = (): JsRenderCodeCell | undefined => { + return pixiAppSettings.contextMenu?.table ?? pixiApp.cellsSheet().cursorOnDataTable(); }; const isHeadingShowing = (): boolean => { - return !!pixiAppSettings.contextMenu?.table?.show_header; + const table = getTable(); + return !!table?.show_header; }; -const getTable = (): JsRenderCodeCell | undefined => { - return pixiAppSettings.contextMenu?.table ?? pixiApp.cellsSheet().cursorOnDataTable(); +const isFirstRowHeader = (): boolean => { + const table = getTable(); + return !!table?.first_row_header; }; const isAlternatingColorsShowing = (): boolean => { - return !!pixiAppSettings.contextMenu?.table?.alternating_colors; + const table = getTable(); + return !!table?.alternating_colors; }; export const dataTableSpec: DataTableSpec = { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index 8ecb974a34..06a24b135f 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -37,7 +37,7 @@ export const GridContextMenu = () => { events.off('cursorPosition', updateCursor); }; }, []); - + console.log(table); return ( {({ contextMenu }) => ( diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index b91ec7156c..aed2084894 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -15,7 +15,6 @@ interface Props { export const TableMenu = (props: Props) => { const { defaultRename, codeCell } = props; const cell = getCodeCell(codeCell?.language); - console.log(cell); const isCodeCell = cell && cell.id !== 'Import'; const hasHiddenColumns = useMemo(() => { From 6b7b770317b05a28d698b3351da25665f56e0ce6 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 27 Oct 2024 06:49:51 -0700 Subject: [PATCH 163/373] fix positioning when menu goes off screen --- .../HTMLGrid/contextMenus/ContextMenuBase.tsx | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx index 6cd3d9bde0..86f271585c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx @@ -17,6 +17,7 @@ export const ContextMenuBase = ({ contextMenuType: ContextMenuType; }) => { const ref = useRef(null); + const refContent = useRef(null); const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const open = contextMenu.type === contextMenuType && !contextMenu.rename; @@ -42,13 +43,52 @@ export const ContextMenuBase = ({ }; }, [onClose]); + const left = contextMenu.world?.x ?? 0; + const [top, setTop] = useState(contextMenu.world?.y ?? 0); + useEffect(() => { + const updateAfterRender = () => { + const content = refContent.current; + if (open && content) { + let newTop = contextMenu.world?.y ?? 0; + if (open && content) { + // we use the screen bounds in world coordinates to determine if the + // menu is going to be cut off + const viewportTop = pixiApp.viewport.toWorld(0, 0).y; + const viewportBottom = pixiApp.viewport.toWorld(0, window.innerHeight).y; + + // we use the viewport bounds to determine the direction the menu is + // opening + const bounds = pixiApp.viewport.getVisibleBounds(); + + // menu is opening downwards + if (newTop < bounds.y + bounds.height / 2) { + if (newTop + content.offsetHeight > viewportBottom) { + newTop = viewportBottom - content.offsetHeight; + } + } + + // menu is opening upwards + else { + if (newTop - content.offsetHeight < viewportTop) { + newTop = viewportTop + content.offsetHeight; + } + } + } + setTop(newTop); + } + }; + + // need to wait for the next render to update the position + setTimeout(updateAfterRender); + }, [contextMenu.world, open]); + return (
Menu Date: Mon, 28 Oct 2024 05:14:01 -0600 Subject: [PATCH 164/373] remove console log --- .../src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx index 06a24b135f..8ecb974a34 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/GridContextMenu.tsx @@ -37,7 +37,7 @@ export const GridContextMenu = () => { events.off('cursorPosition', updateCursor); }; }, []); - console.log(table); + return ( {({ contextMenu }) => ( From fb0b1a984b424700f5b6c74aedbc3a7c4e1bbb5b Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 29 Oct 2024 06:34:36 -0600 Subject: [PATCH 165/373] charts cover up chart cell --- quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index 372e3d87cb..6f212f5466 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -44,7 +44,7 @@ export class HtmlCell { // the 0.5 is adjustment for the border this.div.style.left = `${offset.x - 0.5}px`; - this.div.style.top = `${offset.y + offset.height - 0.5}px`; + this.div.style.top = `${offset.y - 0.5}px`; this.right = document.createElement('div'); this.right.className = 'html-resize-control-right'; From b6ed5ca8adf64b0d98b73c7f31cdb06d53a88780 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 29 Oct 2024 06:47:59 -0600 Subject: [PATCH 166/373] use table name instead of [CHART] for charts --- .../HTMLGrid/htmlCells/htmlCellsHandler.ts | 19 +++++++++++++++++++ .../src/app/gridGL/cells/tables/Tables.ts | 15 +++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts index 7aeb67f828..9c9aacad3e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts @@ -1,6 +1,9 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { Coordinate } from '@/app/gridGL/types/size'; import { JsHtmlOutput } from '@/app/quadratic-core-types'; +import { Point, Rectangle } from 'pixi.js'; import { HtmlCell } from './HtmlCell'; class HTMLCellsHandler { @@ -119,6 +122,22 @@ class HTMLCellsHandler { this.cells.delete(cell); this.cells.add(cell); } + + checkHover(world: Point): Coordinate | undefined { + const cells = this.getCells(); + for (const cell of cells) { + if (cell.sheet.id !== sheets.sheet.id) continue; + const bounds = new Rectangle( + cell.div.offsetLeft, + cell.div.offsetTop, + cell.div.offsetWidth, + cell.div.offsetHeight + ); + if (intersects.rectanglePoint(bounds, world)) { + return { x: cell.x, y: cell.y }; + } + } + } } export const htmlCellsHandler = new HTMLCellsHandler(); diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 43d835f741..4f74a28e25 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -7,6 +7,7 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; import { Table } from '@/app/gridGL/cells/tables/Table'; +import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Coordinate } from '@/app/gridGL/types/size'; import { JsCodeCell, JsRenderCodeCell } from '@/app/quadratic-core-types'; @@ -191,6 +192,20 @@ export class Tables extends Container
{ return true; } } + const hover = htmlCellsHandler.checkHover(world); + const table = hover + ? this.children.find((table) => table.codeCell.x === hover?.x && table.codeCell.y === hover?.y) + : undefined; + if (table) { + if (!this.isTableActive(table)) { + if (this.hoverTable !== table) { + this.hoverTable?.hideActive(); + } + this.hoverTable = table; + table.showActive(); + } + return true; + } this.tableCursor = undefined; return false; } From 6e703bf3255f17548f5912540fe2f64897c27583 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 29 Oct 2024 07:11:18 -0600 Subject: [PATCH 167/373] images use table name --- .../gridGL/cells/cellsImages/CellsImage.ts | 13 ++++++++-- .../gridGL/cells/cellsImages/CellsImages.ts | 12 ++++++++- .../src/app/gridGL/cells/tables/Tables.ts | 14 ++++++++++ .../interaction/pointer/PointerImages.ts | 26 +++++++++---------- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts index 0adaeed50a..0630459afb 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts @@ -1,7 +1,9 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { Coordinate } from '@/app/gridGL/types/size'; import { CoreClientImage } from '@/app/web-workers/quadraticCore/coreClientMessages'; -import { Container, Graphics, Rectangle, Sprite, Texture } from 'pixi.js'; +import { Container, Graphics, Point, Rectangle, Sprite, Texture } from 'pixi.js'; import { IMAGE_BORDER_OFFSET, IMAGE_BORDER_WIDTH } from '../../UI/UICellImages'; import { pixiApp } from '../../pixiApp/PixiApp'; import { CellsSheet } from '../CellsSheet'; @@ -116,8 +118,15 @@ export class CellsImage extends Container { }; reposition() { - const screen = this.sheet.getCellOffsets(this.column, this.row + 1); + const screen = this.sheet.getCellOffsets(this.column, this.row); this.position.set(screen.x, screen.y); this.resizeImage(); } + + hoverPoint(world: Point): Coordinate | undefined { + if (intersects.rectanglePoint(this.viewBounds, world)) { + return { x: this.column, y: this.row }; + } + return undefined; + } } diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts index d540a67312..7b012b7529 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts @@ -1,6 +1,7 @@ import { events } from '@/app/events/events'; +import { Coordinate } from '@/app/gridGL/types/size'; import { CoreClientImage } from '@/app/web-workers/quadraticCore/coreClientMessages'; -import { Container, Rectangle } from 'pixi.js'; +import { Container, Point, Rectangle } from 'pixi.js'; import { intersects } from '../../helpers/intersects'; import { pixiApp } from '../../pixiApp/PixiApp'; import { CellsSheet } from '../CellsSheet'; @@ -54,4 +55,13 @@ export class CellsImages extends Container { pixiApp.setViewportDirty(); } }; + + hoverPoint(world: Point): Coordinate | undefined { + for (const child of this.children) { + const result = child.hoverPoint(world); + if (result) { + return result; + } + } + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 4f74a28e25..2c4effeb42 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -206,6 +206,20 @@ export class Tables extends Container
{ } return true; } + const hoverImage = pixiApp.cellsSheet().cellsImages.hoverPoint(world); + const tableImage = this.children.find( + (table) => table.codeCell.x === hoverImage?.x && table.codeCell.y === hoverImage?.y + ); + if (tableImage) { + if (!this.isTableActive(tableImage)) { + if (this.hoverTable !== tableImage) { + this.hoverTable?.hideActive(); + } + this.hoverTable = tableImage; + tableImage.showActive(); + } + return true; + } this.tableCursor = undefined; return false; } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts index fed86d8b8d..1e30540df8 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts @@ -72,20 +72,18 @@ export class PointerImages { } const search = this.findImage(point); - if (search) { - if (search.side) { - pixiApp.cellImages.activate(search.image); - switch (search.side) { - case 'bottom': - this.cursor = 'ns-resize'; - break; - case 'right': - this.cursor = 'ew-resize'; - break; - case 'corner': - this.cursor = 'nwse-resize'; - break; - } + if (search?.side) { + pixiApp.cellImages.activate(search.image); + switch (search.side) { + case 'bottom': + this.cursor = 'ns-resize'; + break; + case 'right': + this.cursor = 'ew-resize'; + break; + case 'corner': + this.cursor = 'nwse-resize'; + break; } return true; } From 0748191f564243feb26941ef4a5c46b93cfbe2a3 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 30 Oct 2024 04:16:59 -0700 Subject: [PATCH 168/373] cell images --- .../HTMLGrid/contextMenus/ContextMenuBase.tsx | 133 +++++++++--------- .../HTMLGrid/contextMenus/TableMenu.tsx | 28 ++-- .../HTMLGrid/htmlCells/htmlCellsHandler.ts | 5 + .../gridGL/cells/cellsImages/CellsImages.ts | 4 + 4 files changed, 97 insertions(+), 73 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx index 86f271585c..607e1f4714 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { contextMenuAtom, ContextMenuState, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; @@ -16,7 +17,7 @@ export const ContextMenuBase = ({ children: ({ contextMenu }: { contextMenu: ContextMenuState }) => React.ReactNode; contextMenuType: ContextMenuType; }) => { - const ref = useRef(null); + const ref = useRef(null); const refContent = useRef(null); const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); const open = contextMenu.type === contextMenuType && !contextMenu.rename; @@ -45,77 +46,81 @@ export const ContextMenuBase = ({ const left = contextMenu.world?.x ?? 0; const [top, setTop] = useState(contextMenu.world?.y ?? 0); - useEffect(() => { - const updateAfterRender = () => { - const content = refContent.current; - if (open && content) { - let newTop = contextMenu.world?.y ?? 0; - if (open && content) { - // we use the screen bounds in world coordinates to determine if the - // menu is going to be cut off - const viewportTop = pixiApp.viewport.toWorld(0, 0).y; - const viewportBottom = pixiApp.viewport.toWorld(0, window.innerHeight).y; + // useEffect(() => { + // const updateAfterRender = () => { + // const content = refContent.current; + // if (open && content) { + // let newTop = contextMenu.world?.y ?? 0; + // if (open && content) { + // // we use the screen bounds in world coordinates to determine if the + // // menu is going to be cut off + // const viewportTop = pixiApp.viewport.toWorld(0, 0).y; + // const viewportBottom = pixiApp.viewport.toWorld(0, window.innerHeight).y; - // we use the viewport bounds to determine the direction the menu is - // opening - const bounds = pixiApp.viewport.getVisibleBounds(); + // // we use the viewport bounds to determine the direction the menu is + // // opening + // const bounds = pixiApp.viewport.getVisibleBounds(); - // menu is opening downwards - if (newTop < bounds.y + bounds.height / 2) { - if (newTop + content.offsetHeight > viewportBottom) { - newTop = viewportBottom - content.offsetHeight; - } - } + // // menu is opening downwards + // if (newTop < bounds.y + bounds.height / 2) { + // if (newTop + content.offsetHeight > viewportBottom) { + // newTop = viewportBottom - content.offsetHeight; + // } + // } - // menu is opening upwards - else { - if (newTop - content.offsetHeight < viewportTop) { - newTop = viewportTop + content.offsetHeight; - } - } - } - setTop(newTop); - } - }; + // // menu is opening upwards + // else { + // if (newTop - content.offsetHeight < viewportTop) { + // newTop = viewportTop + content.offsetHeight; + // } + // } + // } + // setTop(newTop); + // } + // }; - // need to wait for the next render to update the position - setTimeout(updateAfterRender); - }, [contextMenu.world, open]); + // // need to wait for the next render to update the position + // setTimeout(updateAfterRender); + // }, [contextMenu.world, open]); return ( -
+ { + setIsOpen(dropdownOpen); + if (!dropdownOpen) onClose(); }} > - { - setIsOpen(dropdownOpen); - if (!dropdownOpen) onClose(); - }} + {/* Radix wants the trigger for positioning the content, so we hide it visibly */} + + e.preventDefault()} > - {/* Radix wants the trigger for positioning the content, so we hide it visibly */} - Menu - e.preventDefault()} - > - {children({ contextMenu })} - - -
+ {children({ contextMenu })} + + + // ); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index aed2084894..6cb95ffb42 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -1,6 +1,8 @@ import { Action } from '@/app/actions/actions'; import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; import { ContextMenuItem, ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; +import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { getCodeCell } from '@/app/helpers/codeCellLanguage'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; import { LanguageIcon } from '@/app/ui/components/LanguageIcon'; @@ -23,6 +25,14 @@ export const TableMenu = (props: Props) => { // return codeCell?.; }, [codeCell]); + const isImageOrHtmlCell = useMemo(() => { + if (!codeCell) return false; + return ( + htmlCellsHandler.isHtmlCell(codeCell.x, codeCell.y) || + pixiApp.cellsSheet().cellsImages.isImageCell(codeCell.x, codeCell.y) + ); + }, [codeCell]); + if (!codeCell) { return null; } @@ -41,16 +51,16 @@ export const TableMenu = (props: Props) => { )} - + {!isImageOrHtmlCell && } - {hasHiddenColumns && } - - - - - - {isCodeCell && } - + {!isImageOrHtmlCell && hasHiddenColumns && } + {!isImageOrHtmlCell && } + {!isImageOrHtmlCell && } + {!isImageOrHtmlCell && } + {!isImageOrHtmlCell && } + {!isImageOrHtmlCell && } + {!isImageOrHtmlCell && isCodeCell && } + {!isImageOrHtmlCell && } ); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts index 9c9aacad3e..0baa81a9d7 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts @@ -138,6 +138,11 @@ class HTMLCellsHandler { } } } + + // returns true if the cell is an html cell + isHtmlCell(x: number, y: number): boolean { + return this.getCells().some((cell) => cell.x === x && cell.y === y && cell.sheet.id === sheets.sheet.id); + } } export const htmlCellsHandler = new HTMLCellsHandler(); diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts index 7b012b7529..d46fa9c085 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts @@ -64,4 +64,8 @@ export class CellsImages extends Container { } } } + + isImageCell(x: number, y: number): boolean { + return this.children.some((child) => child.column === x && child.row === y); + } } From 9b8b04636fb94ca9f573a097d5208fe44bf20664 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 30 Oct 2024 05:04:42 -0700 Subject: [PATCH 169/373] clicking on JS image selects the code cell --- .../gridGL/cells/cellsImages/CellsImage.ts | 2 +- .../gridGL/cells/cellsImages/CellsImages.ts | 4 +- .../src/app/gridGL/cells/tables/Table.ts | 23 +----------- .../src/app/gridGL/cells/tables/TableName.ts | 20 ++++++++++ .../src/app/gridGL/cells/tables/Tables.ts | 37 ++++++++++++++++++- .../interaction/pointer/PointerImages.ts | 8 ++-- .../interaction/pointer/PointerTable.ts | 14 +++++-- 7 files changed, 74 insertions(+), 34 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts index 0630459afb..60be49cd7b 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts @@ -123,7 +123,7 @@ export class CellsImage extends Container { this.resizeImage(); } - hoverPoint(world: Point): Coordinate | undefined { + contains(world: Point): Coordinate | undefined { if (intersects.rectanglePoint(this.viewBounds, world)) { return { x: this.column, y: this.row }; } diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts index d46fa9c085..0fa0bfe09d 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts @@ -56,9 +56,9 @@ export class CellsImages extends Container { } }; - hoverPoint(world: Point): Coordinate | undefined { + contains(world: Point): Coordinate | undefined { for (const child of this.children) { - const result = child.hoverPoint(world); + const result = child.contains(world); if (result) { return result; } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 25440bd0a5..2a16f0cd8c 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -70,27 +70,6 @@ export class Table extends Container { } }; - // todo: this probably belongs in TableName.ts - private tableNamePosition = (bounds: Rectangle, gridHeading: number) => { - if (this.visible) { - if (this.tableBounds.y < bounds.top + gridHeading) { - this.tableName.y = bounds.top + gridHeading; - } else { - this.tableName.y = this.tableBounds.top; - } - const headingWidth = pixiApp.headings.headingSize.width / pixiApp.viewport.scaled; - if (!this.tableName.hidden) { - if (this.tableBounds.x < bounds.left + headingWidth) { - this.tableName.x = bounds.left + headingWidth; - this.tableName.visible = this.tableName.x + this.tableName.width <= this.tableBounds.right; - } else { - this.tableName.x = this.tableBounds.x; - this.tableName.visible = true; - } - } - } - }; - // places column headers back into the table (instead of the overHeadings container) private columnHeadersHere() { if (this.inOverHeadings) { @@ -152,7 +131,7 @@ export class Table extends Container { this.headingPosition(bounds, gridHeading); if (this.isShowingTableName()) { this.tableName.scale.set(1 / pixiApp.viewport.scale.x); - this.tableNamePosition(bounds, gridHeading); + this.tableName.updatePosition(bounds, gridHeading); } if (this.visible && this.inOverHeadings) { this.gridLines.update(); diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts index 1e780218f4..0ac2728b8a 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts @@ -103,6 +103,26 @@ export class TableName extends Container { ); } + updatePosition = (bounds: Rectangle, gridHeading: number) => { + if (this.table.visible) { + if (this.table.tableBounds.y < bounds.top + gridHeading) { + this.y = bounds.top + gridHeading; + } else { + this.y = this.table.tableBounds.top; + } + const headingWidth = pixiApp.headings.headingSize.width / pixiApp.viewport.scaled; + if (!this.hidden) { + if (this.table.tableBounds.x < bounds.left + headingWidth) { + this.x = bounds.left + headingWidth; + this.visible = this.x + this.width <= this.table.tableBounds.right; + } else { + this.x = this.table.tableBounds.x; + this.visible = true; + } + } + } + }; + update() { this.position.set(this.table.tableBounds.x, this.table.tableBounds.y); this.visible = false; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 2c4effeb42..ba999906fc 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -206,7 +206,7 @@ export class Tables extends Container
{ } return true; } - const hoverImage = pixiApp.cellsSheet().cellsImages.hoverPoint(world); + const hoverImage = pixiApp.cellsSheet().cellsImages.contains(world); const tableImage = this.children.find( (table) => table.codeCell.x === hoverImage?.x && table.codeCell.y === hoverImage?.y ); @@ -320,4 +320,39 @@ export class Tables extends Container
{ isActive(table: Table): boolean { return this.activeTable === table || this.hoverTable === table || this.contextMenuTable === table; } + + // Ensures that the code cell at the given coordinate is active. + ensureActiveCoordinate(coordinate: Coordinate) { + const table = this.children.find((table) => table.codeCell.x === coordinate.x && table.codeCell.y === coordinate.y); + if (!table) { + return; + } + sheets.sheet.cursor.changePosition({ cursorPosition: coordinate }); + this.ensureActive(table.codeCell); + } + + // Ensures that the JsRenderCodeCell is active. + ensureActive(codeCell: JsRenderCodeCell) { + const table = this.children.find((table) => table.codeCell === codeCell); + if (!table) { + return; + } + sheets.sheet.cursor.changePosition({ cursorPosition: { x: table.codeCell.x, y: table.codeCell.y } }); + if (this.activeTable !== table) { + if (this.activeTable) { + this.activeTable.hideActive(); + } + if (this.hoverTable === table) { + this.hoverTable = undefined; + } + if (this.contextMenuTable === table) { + this.contextMenuTable = undefined; + } + if (this.actionDataTable === table) { + this.actionDataTable = undefined; + } + this.activeTable = table; + table.showActive(); + } + } } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts index 1e30540df8..2cfa67ea7a 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts @@ -96,11 +96,9 @@ export class PointerImages { if (!hasPermissionToEditFile(pixiAppSettings.editorInteractionState.permissions)) return false; const search = this.findImage(point); - if (search) { - if (search.side) { - this.resizing = { point, image: search.image, side: search.side }; - pixiApp.cellImages.activate(search.image); - } + if (search && search.side) { + this.resizing = { point, image: search.image, side: search.side }; + pixiApp.cellImages.activate(search.image); return true; } return false; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index d6bde24497..87e80c48e2 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -17,6 +17,7 @@ export class PointerTable { private doubleClickTimeout: number | undefined; private pointerDownTableName(world: Point, tableDown: TablePointerDownResult) { + pixiApp.cellsSheet().tables.ensureActive(tableDown.table); if (this.doubleClickTimeout) { events.emit('contextMenu', { type: ContextMenuType.Table, @@ -72,9 +73,16 @@ export class PointerTable { } pointerDown(world: Point, event: PointerEvent): boolean { - const tableDown = pixiApp.cellsSheet().tables.pointerDown(world); - if (!tableDown?.table) return false; - + let tableDown = pixiApp.cellsSheet().tables.pointerDown(world); + if (!tableDown?.table) { + const image = pixiApp.cellsSheet().cellsImages.contains(world); + if (image) { + pixiApp.cellsSheet().tables.ensureActiveCoordinate(image); + sheets.sheet.cursor.changePosition({ cursorPosition: image }); + return true; + } + return false; + } if (event.button === 2 || (isMac && event.button === 0 && event.ctrlKey)) { events.emit('contextMenu', { type: ContextMenuType.Table, From f84e085cbccee4697c5c2d26caa5ee5ceebc9612 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 30 Oct 2024 08:31:50 -0700 Subject: [PATCH 170/373] removing cellsArray --- .../app/grid/sheet/GridOverflowLines.test.ts | 11 ++- .../src/app/grid/sheet/GridOverflowLines.ts | 87 ++++++++++++++++--- quadratic-client/src/app/grid/sheet/Sheet.ts | 2 +- .../inlineEditor/inlineEditorHandler.ts | 2 - .../src/app/gridGL/UI/GridLines.ts | 34 +++++--- .../src/app/gridGL/UI/UICellImages.ts | 47 +++++----- .../src/app/gridGL/cells/CellsArray.ts | 38 +------- .../src/app/gridGL/cells/CellsSheet.ts | 16 +--- .../src/app/gridGL/cells/CellsSheets.ts | 10 +-- .../gridGL/cells/cellsImages/CellsImage.ts | 6 ++ .../gridGL/cells/cellsImages/CellsImages.ts | 13 ++- .../src/app/gridGL/cells/tables/Table.ts | 11 ++- .../app/gridGL/cells/tables/TableOutline.ts | 14 ++- .../src/app/gridGL/cells/tables/Tables.ts | 11 +++ .../interaction/pointer/PointerImages.ts | 4 +- .../src/app/gridGL/pixiApp/PixiApp.ts | 12 --- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 14 +-- quadratic-core/src/grid/js_types.rs | 27 ++---- 18 files changed, 201 insertions(+), 158 deletions(-) diff --git a/quadratic-client/src/app/grid/sheet/GridOverflowLines.test.ts b/quadratic-client/src/app/grid/sheet/GridOverflowLines.test.ts index 68e046f811..2300e56df6 100644 --- a/quadratic-client/src/app/grid/sheet/GridOverflowLines.test.ts +++ b/quadratic-client/src/app/grid/sheet/GridOverflowLines.test.ts @@ -1,11 +1,14 @@ import { GridOverflowLines } from '@/app/grid/sheet/GridOverflowLines'; +import { Sheet } from '@/app/grid/sheet/Sheet'; import { describe, expect, it } from 'vitest'; +// todo... + describe('GridOverflowLines', () => { it('getLinesInRange', () => { - const gridOverflowLines = new GridOverflowLines(); + const gridOverflowLines = new GridOverflowLines({} as Sheet); gridOverflowLines.updateHash('0,0', [{ x: 1, y: 1 }]); - let lines = gridOverflowLines.getLinesInRange(1, [0, 5]); + let lines = gridOverflowLines.getColumnVerticalRange(1, [0, 5]); expect(lines).toEqual([ [0, 0], [2, 5], @@ -15,7 +18,7 @@ describe('GridOverflowLines', () => { { x: 1, y: 1 }, { x: 1, y: 2 }, ]); - lines = gridOverflowLines.getLinesInRange(1, [0, 5]); + lines = gridOverflowLines.getColumnVerticalRange(1, [0, 5]); expect(lines).toEqual([ [0, 0], [3, 5], @@ -26,7 +29,7 @@ describe('GridOverflowLines', () => { { x: 1, y: 2 }, { x: 1, y: 4 }, ]); - lines = gridOverflowLines.getLinesInRange(1, [0, 5]); + lines = gridOverflowLines.getColumnVerticalRange(1, [0, 5]); expect(lines).toEqual([ [0, 0], [3, 3], diff --git a/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts b/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts index fef9335f0c..f2c1851168 100644 --- a/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts +++ b/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts @@ -1,12 +1,23 @@ -//! Keeps track of which grid lines should not be drawn within the sheet because of overflow. +//! Keeps track of which grid lines should not be drawn within the sheet because +//! of overflow of text, images, and html tables.. +import { Sheet } from '@/app/grid/sheet/Sheet'; import { Coordinate } from '@/app/gridGL/types/size'; +import { Rectangle } from 'pixi.js'; export class GridOverflowLines { + private sheet: Sheet; + + // hold overflow lines (we do not draw grid lines for cells that overlap neighbors) private overflowLines: Map; - constructor() { + // holds the rectangles of images and html tables so we don't draw grid lines over them + private overflowImageHtml: Map; + + constructor(sheet: Sheet) { + this.sheet = sheet; this.overflowLines = new Map(); + this.overflowImageHtml = new Map(); } // Updates a hash with a list of overflow lines @@ -23,32 +34,88 @@ export class GridOverflowLines { }); } + updateImageHtml(column: number, row: number, width?: number, height?: number) { + if (width === undefined || height === undefined) { + this.overflowImageHtml.delete(`${column},${row}`); + return; + } + const start = this.sheet.offsets.getCellOffsets(column, row); + const end = this.sheet.getColumnRow(start.x + width, start.y + height); + this.overflowImageHtml.set(`${column},${row}`, new Rectangle(column, row, end.x - column, end.y - row)); + } + // returns a list of ranges of y-values that need to be drawn (excluding the // ones that are in the overflowLines list) - getLinesInRange(column: number, row: [number, number]): [number, number][] | undefined { + getColumnVerticalRange(column: number, rows: [number, number]): [number, number][] | undefined { // if no overflow lines, then draw the entire screen - if (this.overflowLines.size === 0) { + if (this.overflowLines.size === 0 && this.overflowImageHtml.size === 0) { return; } // create a list of y coordinates that need removing - const inRangeSorted = []; - for (let y = row[0]; y <= row[1]; y++) { + const inRange = []; + for (let y = rows[0]; y <= rows[1]; y++) { const key = `${column},${y}`; if (this.overflowLines.has(key)) { - inRangeSorted.push(y); + inRange.push(y); } } + this.overflowImageHtml.forEach((rect) => { + if (column >= rect.left && column <= rect.right) { + for (let y = rect.top; y <= rect.bottom; y++) { + inRange.push(y); + } + } + }); + // if there are no gaps, then draw the entire screen - if (inRangeSorted.length === 0) { + if (inRange.length === 0) { return undefined; } // now create a list of numbers that need to be drawn const drawnLines: number[] = []; - for (let i = row[0]; i <= row[1]; i++) { - if (!inRangeSorted.includes(i)) { + for (let y = rows[0]; y <= rows[1]; y++) { + if (!inRange.includes(y)) { + drawnLines.push(y); + } + } + + // finally, create a list of ranges to draw + const results: [number, number][] = []; + for (let i = 0; i < drawnLines.length; i++) { + let start = drawnLines[i]; + while (drawnLines[i + 1] - drawnLines[i] === 1) { + i++; + } + results.push([start, drawnLines[i]]); + } + return results; + } + + // returns a list of ranges of x-values that need to be drawn (excluding the + // ones that are in the overflowImageHtml list) + getRowHorizontalRange(row: number, columns: [number, number]): [number, number][] | undefined { + // if no overflow lines, then draw the entire screen + if (this.overflowLines.size === 0 && this.overflowImageHtml.size === 0) { + return; + } + + // create a list of x coordinates that need removing + const inRange: number[] = []; + this.overflowImageHtml.forEach((rect) => { + if (row >= rect.top && row <= rect.bottom) { + for (let x = rect.left; x <= rect.right; x++) { + inRange.push(x); + } + } + }); + + // now create a list of numbers that need to be drawn + const drawnLines: number[] = []; + for (let i = columns[0]; i <= columns[1]; i++) { + if (!inRange.includes(i)) { drawnLines.push(i); } } diff --git a/quadratic-client/src/app/grid/sheet/Sheet.ts b/quadratic-client/src/app/grid/sheet/Sheet.ts index 8dc1f36a68..99e35a5921 100644 --- a/quadratic-client/src/app/grid/sheet/Sheet.ts +++ b/quadratic-client/src/app/grid/sheet/Sheet.ts @@ -35,7 +35,7 @@ export class Sheet { this.cursor = new SheetCursor(this); this.bounds = info.bounds; this.boundsWithoutFormatting = info.bounds_without_formatting; - this.gridOverflowLines = new GridOverflowLines(); + this.gridOverflowLines = new GridOverflowLines(this); events.on('sheetBounds', this.updateBounds); events.on('sheetValidations', this.sheetValidations); } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts index 1fd528a21e..80985faae4 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts @@ -88,7 +88,6 @@ class InlineEditorHandler { inlineEditorFormula.clearDecorations(); window.removeEventListener('keydown', inlineEditorKeyboard.keyDown); multiplayer.sendEndCellEdit(); - pixiApp.cellsSheets.updateCellsArray(); this.hideDiv(); } @@ -208,7 +207,6 @@ class InlineEditorHandler { this.formatSummary.fillColor ? convertColorStringToHex(this.formatSummary.fillColor) : '#ffffff' ); this.updateFont(); - pixiApp.cellsSheets.updateCellsArray(); this.sendMultiplayerUpdate(); this.showDiv(); this.changeToFormula(changeToFormula); diff --git a/quadratic-client/src/app/gridGL/UI/GridLines.ts b/quadratic-client/src/app/gridGL/UI/GridLines.ts index f1dd7272c9..b42ceccabe 100644 --- a/quadratic-client/src/app/gridGL/UI/GridLines.ts +++ b/quadratic-client/src/app/gridGL/UI/GridLines.ts @@ -26,13 +26,6 @@ export class GridLines extends Graphics { gridLinesX: GridLine[] = []; gridLinesY: GridLine[] = []; - draw(bounds: Rectangle): void { - this.lineStyle({ width: 1, color: colors.gridLines, alpha: 0.2, alignment: 0.5, native: false }); - const range = this.drawHorizontalLines(bounds); - this.drawVerticalLines(bounds, range); - this.dirty = false; - } - update(bounds = pixiApp.viewport.getVisibleBounds(), scale = pixiApp.viewport.scale.x, forceRefresh = false) { if (this.dirty || forceRefresh) { this.dirty = false; @@ -63,11 +56,17 @@ export class GridLines extends Graphics { this.lineStyle(this.currentLineStyle); this.gridLinesX = []; this.gridLinesY = []; - const range = this.drawHorizontalLines(bounds); + + const range = this.drawHorizontalLines(bounds, this.getColumns(bounds)); this.drawVerticalLines(bounds, range); } } + private getColumns(bounds: Rectangle): [number, number] { + const sheet = sheets.sheet; + return [sheet.offsets.getXPlacement(bounds.left).index, sheet.offsets.getXPlacement(bounds.right).index]; + } + private drawVerticalLines(bounds: Rectangle, range: [number, number]) { const offsets = sheets.sheet.offsets; const columnPlacement = offsets.getXPlacement(bounds.left); @@ -81,7 +80,7 @@ export class GridLines extends Graphics { for (let x = bounds.left; x <= bounds.right + size - 1; x += size) { // don't draw grid lines when hidden if (size !== 0) { - const lines = gridOverflowLines.getLinesInRange(column, range); + const lines = gridOverflowLines.getColumnVerticalRange(column, range); if (lines) { for (const [y0, y1] of lines) { const start = offsets.getRowPlacement(y0).position; @@ -101,11 +100,12 @@ export class GridLines extends Graphics { } // @returns the vertical range of [rowStart, rowEnd] - private drawHorizontalLines(bounds: Rectangle): [number, number] { + private drawHorizontalLines(bounds: Rectangle, columns: [number, number]): [number, number] { const offsets = sheets.sheet.offsets; const rowPlacement = offsets.getYPlacement(bounds.top); const index = rowPlacement.index; const position = rowPlacement.position; + const gridOverflowLines = sheets.sheet.gridOverflowLines; let row = index; const offset = bounds.top - position; @@ -113,8 +113,18 @@ export class GridLines extends Graphics { for (let y = bounds.top; y <= bounds.bottom + size - 1; y += size) { // don't draw grid lines when hidden if (size !== 0) { - this.moveTo(bounds.left, y - offset); - this.lineTo(bounds.right, y - offset); + const lines = gridOverflowLines.getRowHorizontalRange(row, columns); + if (lines) { + for (const [x0, x1] of lines) { + const start = offsets.getColumnPlacement(x0).position; + const end = offsets.getColumnPlacement(x1 + 1).position; + this.moveTo(start, y - offset); + this.lineTo(end, y - offset); + } + } else { + this.moveTo(bounds.left, y - offset); + this.lineTo(bounds.right, y - offset); + } this.gridLinesY.push({ row, x: bounds.left, y: y - offset, w: bounds.right - bounds.left, h: 1 }); } size = offsets.getRowHeight(row); diff --git a/quadratic-client/src/app/gridGL/UI/UICellImages.ts b/quadratic-client/src/app/gridGL/UI/UICellImages.ts index d230eb7723..af9086d12d 100644 --- a/quadratic-client/src/app/gridGL/UI/UICellImages.ts +++ b/quadratic-client/src/app/gridGL/UI/UICellImages.ts @@ -2,7 +2,6 @@ import { events } from '@/app/events/events'; import { convertColorStringToTint } from '@/app/helpers/convertColor'; import { Container, Graphics } from 'pixi.js'; import { CellsImage } from '../cells/cellsImages/CellsImage'; -import { pixiApp } from '../pixiApp/PixiApp'; // These should be consistent with ResizeControl.tsx export const IMAGE_BORDER_WIDTH = 5; @@ -19,7 +18,7 @@ export class UICellImages extends Container { private animationTime = 0; private animationLastTime = 0; - dirtyBorders = false; + // dirtyBorders = false; dirtyResizing = false; constructor() { @@ -31,7 +30,7 @@ export class UICellImages extends Container { private changeSheet = () => { this.active = undefined; - this.dirtyBorders = true; + // this.dirtyBorders = true; this.dirtyResizing = true; }; @@ -51,27 +50,26 @@ export class UICellImages extends Container { } } - drawBorders(): boolean { - if (this.dirtyBorders) { - this.dirtyBorders = false; - this.borders.clear(); - const images = pixiApp.cellsSheets.current?.getCellsImages(); - if (!images) return true; - const hslColorFromCssVar = window.getComputedStyle(document.documentElement).getPropertyValue('--primary'); - const color = convertColorStringToTint(`hsl(${hslColorFromCssVar})`); - this.borders.lineStyle({ color, width: 1 }); - images.forEach((image) => { - this.borders.drawRect(image.x, image.y, image.width, image.height); - }); - return true; - } - return false; - } + // drawBorders(): boolean { + // if (this.dirtyBorders) { + // this.dirtyBorders = false; + // this.borders.clear(); + // const images = pixiApp.cellsSheets.current?.getCellsImages(); + // if (!images) return true; + // const hslColorFromCssVar = window.getComputedStyle(document.documentElement).getPropertyValue('--primary'); + // const color = convertColorStringToTint(`hsl(${hslColorFromCssVar})`); + // this.borders.lineStyle({ color, width: 1 }); + // images.forEach((image) => { + // this.borders.drawRect(image.x, image.y, image.width, image.height); + // }); + // return true; + // } + // return false; + // } drawResizing(): boolean { if (this.dirtyResizing) { this.dirtyResizing = false; - this.resizing.clear(); if (this.active) { const hslColorFromCssVar = window.getComputedStyle(document.documentElement).getPropertyValue('--primary'); @@ -103,7 +101,7 @@ export class UICellImages extends Container { } get dirty(): boolean { - return !!this.animationState || this.dirtyBorders || this.dirtyResizing; + return !!this.animationState || /*this.dirtyBorders ||*/ this.dirtyResizing; } update(): boolean { @@ -132,13 +130,14 @@ export class UICellImages extends Container { this.resizing.alpha = (1 - this.easeInOutSine(this.animationTime, TRANSITION_TIME_MS)) as number; } } - this.drawBorders(); + // this.drawBorders(); this.drawResizing(); return true; } else { - let rendered = this.drawBorders(); + // let rendered = this.drawBorders(); if (this.drawResizing()) return true; - return rendered; + // return rendered; + return false; } } } diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts index 540758d310..b572096f0f 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsArray.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsArray.ts @@ -1,5 +1,7 @@ //! Holds borders for tables and code errors. +// todo: this should move to TableOutline + import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; @@ -8,7 +10,7 @@ import { Coordinate } from '@/app/gridGL/types/size'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { JsCodeCell, JsRenderCodeCell, RunError } from '@/app/quadratic-core-types'; import mixpanel from 'mixpanel-browser'; -import { Container, Graphics, ParticleContainer, Point, Rectangle, Sprite, Texture } from 'pixi.js'; +import { Container, Graphics, ParticleContainer, Rectangle, Sprite, Texture } from 'pixi.js'; import { intersects } from '../helpers/intersects'; import { pixiApp } from '../pixiApp/PixiApp'; import { CellsSheet } from './CellsSheet'; @@ -234,38 +236,4 @@ export class CellsArray extends Container { private getSprite = (): Sprite => { return this.particles.addChild(new Sprite(Texture.WHITE)); }; - - isCodeCell(x: number, y: number): boolean { - return this.codeCells.has(this.key(x, y)); - } - - isCodeCellOutput(x: number, y: number): boolean { - return [...this.codeCells.values()].some((codeCell) => { - let rect = new Rectangle(codeCell.x, codeCell.y, codeCell.x + codeCell.w, codeCell.y + codeCell.h); - - return rect.contains(x, y); - }); - } - - getCodeCellWorld(point: Point): JsRenderCodeCell | undefined { - for (const [index, tableRect] of this.tables.entries()) { - if (tableRect.contains(point.x, point.y)) { - return this.codeCells.get(index); - } - } - } - - getTableCursor(point: Coordinate): JsRenderCodeCell | undefined { - for (const codeCell of this.codeCells.values()) { - if ( - !codeCell.spill_error && - codeCell.x <= point.x && - codeCell.x + codeCell.w > point.x && - codeCell.y <= point.y && - codeCell.y + codeCell.h > point.y - ) { - return codeCell; - } - } - } } diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts index 83703784b2..f454af26ff 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts @@ -3,9 +3,7 @@ import { Tables } from '@/app/gridGL/cells/tables/Tables'; import { JsRenderCodeCell, JsValidationWarning } from '@/app/quadratic-core-types'; import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; import { Container, Rectangle, Sprite } from 'pixi.js'; -import { pixiApp } from '../pixiApp/PixiApp'; import { Borders } from './borders/Borders'; -import { CellsArray } from './CellsArray'; import { CellsFills } from './CellsFills'; import { CellsImage } from './cellsImages/CellsImage'; import { CellsImages } from './cellsImages/CellsImages'; @@ -28,7 +26,6 @@ export interface ErrorValidation { export class CellsSheet extends Container { private borders: Borders; cellsFills: CellsFills; - cellsArray: CellsArray; cellsImages: CellsImages; cellsMarkers: CellsMarkers; @@ -47,14 +44,11 @@ export class CellsSheet extends Container { this.addChild(new CellsSearch(sheetId)); this.cellsLabels = this.addChild(new CellsLabels(this)); + this.cellsImages = this.addChild(new CellsImages(this)); this.tables = this.addChild(new Tables(this)); - // todo: this should go away... - this.cellsArray = this.addChild(new CellsArray(this)); - this.borders = this.addChild(new Borders(this)); this.cellsMarkers = this.addChild(new CellsMarkers()); - this.cellsImages = new CellsImages(this); this.visible = false; events.on('renderValidationWarnings', this.renderValidations); @@ -69,11 +63,8 @@ export class CellsSheet extends Container { show(bounds: Rectangle): void { this.visible = true; this.cellsLabels.show(bounds); - this.cellsArray.visible = true; - this.cellsArray.cheapCull(bounds); this.cellsFills.cheapCull(bounds); this.cellsImages.cheapCull(bounds); - pixiApp.changeCellImages(this.cellsImages); } hide(): void { @@ -81,7 +72,6 @@ export class CellsSheet extends Container { } toggleOutlines(off?: boolean) { - this.cellsArray.visible = off ?? true; this.cellsMarkers.visible = off ?? true; } @@ -98,10 +88,6 @@ export class CellsSheet extends Container { this.tables.sheetOffsets(this.sheetId); } - updateCellsArray() { - this.cellsArray.updateCellsArray(); - } - getCellsImages(): CellsImage[] { return this.cellsImages.children; } diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts index 694fd9359a..dcf7d105d0 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts @@ -130,7 +130,6 @@ export class CellsSheets extends Container { pixiApp.gridLines.dirty = true; pixiApp.cursor.dirty = true; pixiApp.headings.dirty = true; - this.updateCellsArray(); } } @@ -144,11 +143,6 @@ export class CellsSheets extends Container { return this.current.cellsLabels.getCellsContentMaxHeight(row); } - updateCellsArray(): void { - if (!this.current) throw new Error('Expected current to be defined in CellsSheets.updateCellsArray'); - this.current.updateCellsArray(); - } - adjustOffsetsBorders(sheetId: string): void { const cellsSheet = this.getById(sheetId); cellsSheet?.adjustOffsets(); @@ -183,14 +177,14 @@ export class CellsSheets extends Container { const cellsSheet = this.current; if (!cellsSheet) return false; const cursor = sheets.sheet.cursor.cursorPosition; - return cellsSheet.cellsArray.isCodeCell(cursor.x, cursor.y); + return cellsSheet.tables.isTable(cursor.x, cursor.y); } isCursorOnCodeCellOutput(): boolean { const cellsSheet = this.current; if (!cellsSheet) return false; const cursor = sheets.sheet.cursor.cursorPosition; - return cellsSheet.cellsArray.isCodeCellOutput(cursor.x, cursor.y); + return cellsSheet.tables.isTable(cursor.x, cursor.y); } update(dirtyViewport: boolean) { diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts index 60be49cd7b..9a11a49d25 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts @@ -112,6 +112,12 @@ export class CellsImage extends Container { this.sprite.width, IMAGE_BORDER_WIDTH ); + const sheet = sheets.getById(this.sheetId); + if (!sheet) { + throw new Error(`Expected sheet to be defined in CellsImage.resizeImage`); + } + sheet.gridOverflowLines.updateImageHtml(this.column, this.row, this.sprite.width, this.sprite.height); + this.cellsSheet.tables.resizeTable(this.column, this.row, this.sprite.width, this.sprite.height); if (this.cellsSheet.sheetId === sheets.current) { pixiApp.setViewportDirty(); } diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts index 0fa0bfe09d..38274a4ab9 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts @@ -1,4 +1,7 @@ +//! Draw the cell images (an Image output from a code cell) + import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; import { Coordinate } from '@/app/gridGL/types/size'; import { CoreClientImage } from '@/app/web-workers/quadraticCore/coreClientMessages'; import { Container, Point, Rectangle } from 'pixi.js'; @@ -27,7 +30,7 @@ export class CellsImages extends Container { if (this.cellsSheet.sheetId === sheetId) { this.children.forEach((sprite) => sprite.reposition()); } - pixiApp.cellImages.dirtyBorders = true; + // pixiApp.cellImages.dirtyBorders = true; }; cheapCull(bounds: Rectangle) { @@ -46,12 +49,18 @@ export class CellsImages extends Container { sprite.updateMessage(message); } else { this.removeChild(sprite); + + // remove the image from the overflow lines + const sheet = sheets.getById(this.cellsSheet.sheetId); + if (!sheet) throw new Error(`Expected sheet to be defined in CellsImages.updateImage`); + sheet.gridOverflowLines.updateImageHtml(message.x, message.y); + sprite = undefined; } } else if (message.image) { this.addChild(new CellsImage(this.cellsSheet, message)); } - pixiApp.cellImages.dirtyBorders = true; + // pixiApp.cellImages.dirtyBorders = true; pixiApp.setViewportDirty(); } }; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 2a16f0cd8c..a78d10db7a 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -139,13 +139,13 @@ export class Table extends Container { } hideActive() { - this.outline.visible = false; + this.outline.activate(false); this.tableName.hide(); pixiApp.setViewportDirty(); } showActive() { - this.outline.visible = true; + this.outline.activate(true); this.tableName.show(); pixiApp.setViewportDirty(); } @@ -244,4 +244,11 @@ export class Table extends Container { getColumnHeaderLines(): { y0: number; y1: number; lines: number[] } { return this.columnHeaders.getColumnHeaderLines(); } + + // resizes an image or html table to its overlapping size + resize(width: number, height: number) { + this.tableBounds.width = width; + this.tableBounds.height = height; + this.outline.update(); + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts index ce783cc218..f1e5ee44b6 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts @@ -12,21 +12,30 @@ const SPILL_FILL_ALPHA = 0.05; export class TableOutline extends Graphics { private table: Table; + private active = false; constructor(table: Table) { super(); this.table = table; } + activate(active: boolean) { + if (active === this.active) return; + this.active = active; + this.update(); + } + update() { this.clear(); // draw the table selected outline - this.lineStyle({ color: getCSSVariableTint('primary'), width: 2, alignment: 0 }); + const width = this.active ? 2 : 1; + + this.lineStyle({ color: getCSSVariableTint('primary'), width, alignment: 0 }); this.drawShape(new Rectangle(0, 0, this.table.tableBounds.width, this.table.tableBounds.height)); // draw the spill error boundaries - if (this.table.codeCell.spill_error) { + if (this.active && this.table.codeCell.spill_error) { const full = this.table.sheet.getScreenRectangle( Number(this.table.codeCell.x), Number(this.table.codeCell.y), @@ -44,7 +53,6 @@ export class TableOutline extends Graphics { this.drawDashedRectangle(rectangle, colors.cellColorError); }); } - this.visible = false; } // draw a dashed and filled rectangle to identify the cause of the spill error diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index ba999906fc..99c23deb80 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -154,6 +154,10 @@ export class Tables extends Container
{ } } + isTable(x: number, y: number): boolean { + return this.children.some((table) => table.codeCell.x === x && table.codeCell.y === y); + } + private isTableActive(table: Table): boolean { return this.activeTable === table || this.hoverTable === table || this.contextMenuTable === table; } @@ -355,4 +359,11 @@ export class Tables extends Container
{ table.showActive(); } } + + resizeTable(x: number, y: number, width: number, height: number) { + const table = this.children.find((table) => table.codeCell.x === x && table.codeCell.y === y); + if (table) { + table.resize(width, height); + } + } } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts index 2cfa67ea7a..7d406b3bec 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts @@ -66,7 +66,7 @@ export class PointerImages { } this.resizing.image.temporaryResize(width, height); pixiApp.cellImages.dirtyResizing = true; - pixiApp.cellImages.dirtyBorders = true; + // pixiApp.cellImages.dirtyBorders = true; pixiApp.setViewportDirty(); return true; } @@ -125,7 +125,7 @@ export class PointerImages { this.resizing.image.width = this.resizing.image.viewBounds.width; this.resizing.image.height = this.resizing.image.viewBounds.height; pixiApp.cellImages.dirtyResizing = true; - pixiApp.cellImages.dirtyBorders = true; + // pixiApp.cellImages.dirtyBorders = true; this.resizing = undefined; return true; } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index ab61018120..44ea771a03 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -20,7 +20,6 @@ import { CellHighlights } from '@/app/gridGL/UI/cellHighlights/CellHighlights'; import { GridHeadings } from '@/app/gridGL/UI/gridHeadings/GridHeadings'; import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; import { CellsSheets } from '@/app/gridGL/cells/CellsSheets'; -import { CellsImages } from '@/app/gridGL/cells/cellsImages/CellsImages'; import { Pointer } from '@/app/gridGL/interaction/pointer/Pointer'; import { ensureVisible } from '@/app/gridGL/interaction/viewportHelper'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; @@ -335,9 +334,6 @@ export class PixiApp { this.cursor.dirty = true; this.cellHighlights.dirty = true; this.headings.dirty = true; - if (!pixiAppSettings.showCellTypeOutlines) { - this.cellsSheets.updateCellsArray(); - } if (visible) ensureVisible(visible !== true ? visible : undefined); events.emit('cursorPosition'); } @@ -356,14 +352,6 @@ export class PixiApp { } } - // this shows the CellImages of the current sheet, removing any old ones. This - // is needed to ensure the proper z-index for the images (ie, so it shows over - // the grid lines). - changeCellImages(cellsImages: CellsImages) { - this.imagePlaceholders.removeChildren(); - this.imagePlaceholders.addChild(cellsImages); - } - isCursorOnCodeCell(): boolean { return this.cellsSheets.isCursorOnCodeCell(); } diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index fc3db56c1f..994b84efc4 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -89,13 +89,13 @@ class PixiAppSettings { pixiApp.axesLines.dirty = true; pixiApp.headings.dirty = true; - if ( - (this.lastSettings && this.lastSettings.showCellTypeOutlines !== this.settings.showCellTypeOutlines) || - (this.lastSettings && this.lastSettings.presentationMode !== this.settings.presentationMode) - ) { - pixiApp.cellsSheets.updateCellsArray(); - pixiApp.viewport.dirty = true; - } + // todo: not sure what to do with this... + // if ( + // (this.lastSettings && this.lastSettings.showCellTypeOutlines !== this.settings.showCellTypeOutlines) || + // (this.lastSettings && this.lastSettings.presentationMode !== this.settings.presentationMode) + // ) { + // pixiApp.viewport.dirty = true; + // } this.lastSettings = this.settings; }; diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index e267515bee..eae41c0f05 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -170,15 +170,13 @@ pub struct CellFormatSummary { pub strike_through: Option, } -#[derive(Serialize, PartialEq, Debug)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] +#[derive(Serialize, PartialEq, Debug, TS)] pub struct JsReturnInfo { pub line_number: Option, pub output_type: Option, } -#[derive(Serialize, PartialEq, Debug)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] +#[derive(Serialize, PartialEq, Debug, TS)] pub struct JsCodeCell { pub x: i64, pub y: i64, @@ -209,8 +207,7 @@ pub struct JsRenderCodeCell { pub alternating_colors: bool, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)] pub struct JsHtmlOutput { pub sheet_id: String, pub x: i64, @@ -220,8 +217,7 @@ pub struct JsHtmlOutput { pub h: Option, } -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, TS)] pub enum JsRenderCodeCellState { NotYetRun, RunError, @@ -247,8 +243,7 @@ pub struct JsValidationSheet { errors: Vec<(Pos, String)>, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct JsDataTableColumn { pub name: String, @@ -266,9 +261,7 @@ impl From for JsDataTableColumn { } } -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] -#[serde(rename_all = "camelCase")] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, TS)] pub struct JsRowHeight { pub row: i64, pub height: f64, @@ -280,9 +273,7 @@ impl fmt::Display for JsRowHeight { } } -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] -#[serde(rename_all = "camelCase")] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, TS)] pub struct JsOffset { pub column: Option, pub row: Option, @@ -299,9 +290,7 @@ impl fmt::Display for JsOffset { } } -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] -#[serde(rename_all = "camelCase")] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, TS)] pub struct JsPos { pub x: i64, pub y: i64, From 15328e69adaefd1f681caec95fc23908452077c2 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 30 Oct 2024 09:11:35 -0700 Subject: [PATCH 171/373] better table highlighting for html and image --- .../app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 9 +- .../HTMLGrid/htmlCells/htmlCellsHandler.ts | 22 +- quadratic-client/src/app/gridGL/UI/Cursor.ts | 9 +- .../src/app/gridGL/cells/CellsArray.ts | 239 ------------------ .../src/app/gridGL/cells/tables/Table.ts | 7 +- .../src/app/gridGL/cells/tables/Tables.ts | 67 ++++- 6 files changed, 93 insertions(+), 260 deletions(-) delete mode 100644 quadratic-client/src/app/gridGL/cells/CellsArray.ts diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index 6f212f5466..f62fef4128 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -1,7 +1,6 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; import { JsHtmlOutput } from '@/app/quadratic-core-types'; -import { colors } from '@/app/theme/colors'; import { CELL_HEIGHT, CELL_WIDTH } from '@/shared/constants/gridConstants'; import { InteractionEvent } from 'pixi.js'; import { pixiApp } from '../../pixiApp/PixiApp'; @@ -18,7 +17,6 @@ const ALLOW_CHART_INTERACTIVITY = import.meta.env.VITE_ALLOW_CHART_INTERACTIVITY export class HtmlCell { private right: HTMLDivElement; - private iframe: HTMLIFrameElement; private bottom: HTMLDivElement; private htmlCell: JsHtmlOutput; private resizing: HtmlCellResizing | undefined; @@ -27,6 +25,7 @@ export class HtmlCell { sheet: Sheet; div: HTMLDivElement; + iframe: HTMLIFrameElement; constructor(htmlCell: JsHtmlOutput) { if (htmlCell.html === null) throw new Error('Expected html to be defined in HtmlCell constructor'); @@ -39,12 +38,12 @@ export class HtmlCell { this.div = document.createElement('div'); this.div.className = 'html-cell'; - this.div.style.border = `1px solid ${colors.cellColorUserPythonRgba}`; + this.div.style.boxShadow = '0 0 0 1px hsl(var(--primary))'; const offset = this.sheet.getCellOffsets(Number(htmlCell.x), Number(htmlCell.y)); // the 0.5 is adjustment for the border - this.div.style.left = `${offset.x - 0.5}px`; - this.div.style.top = `${offset.y - 0.5}px`; + this.div.style.left = `${offset.x}px`; + this.div.style.top = `${offset.y}px`; this.right = document.createElement('div'); this.right.className = 'html-resize-control-right'; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts index 0baa81a9d7..4f8c4620bc 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts @@ -2,7 +2,7 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { Coordinate } from '@/app/gridGL/types/size'; -import { JsHtmlOutput } from '@/app/quadratic-core-types'; +import { JsHtmlOutput, JsRenderCodeCell } from '@/app/quadratic-core-types'; import { Point, Rectangle } from 'pixi.js'; import { HtmlCell } from './HtmlCell'; @@ -143,6 +143,26 @@ class HTMLCellsHandler { isHtmlCell(x: number, y: number): boolean { return this.getCells().some((cell) => cell.x === x && cell.y === y && cell.sheet.id === sheets.sheet.id); } + + showActive(codeCell: JsRenderCodeCell, isSelected: boolean) { + const cell = this.getCells().find( + (cell) => cell.x === codeCell.x && cell.y === codeCell.y && cell.sheet.id === sheets.sheet.id + ); + if (cell) { + cell.div.style.boxShadow = '0 0 0 2px hsl(var(--primary))'; + cell.iframe.style.pointerEvents = isSelected ? 'auto' : 'none'; + } + } + + hideActive(codeCell: JsRenderCodeCell) { + const cell = this.getCells().find( + (cell) => cell.x === codeCell.x && cell.y === codeCell.y && cell.sheet.id === sheets.sheet.id + ); + if (cell) { + cell.div.style.boxShadow = '0 0 0 1px hsl(var(--primary))'; + cell.iframe.style.pointerEvents = 'none'; + } + } } export const htmlCellsHandler = new HTMLCellsHandler(); diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index bbf9ceb88e..bbe6980636 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -76,6 +76,9 @@ export class Cursor extends Container { const cell = cursor.cursorPosition; const showInput = pixiAppSettings.input.show; + if (cursor.onlySingleSelection() && pixiApp.cellsSheet().tables.isHtmlOrImage(cell)) { + return; + } let { x, y, width, height } = sheet.getCellOffsets(cell.x, cell.y); const color = colors.cursorCell; const codeCell = codeEditorState.codeCell; @@ -240,7 +243,6 @@ export class Cursor extends Container { // visible bounds on the screen, not to the selection size. update(viewportDirty: boolean) { const columnRow = !!sheets.sheet.cursor.columnRow; - const multiCursor = sheets.sheet.cursor.multiCursor; if (this.dirty || (viewportDirty && columnRow)) { this.dirty = false; this.graphics.clear(); @@ -253,6 +255,7 @@ export class Cursor extends Container { this.drawCodeCursor(); if (!pixiAppSettings.input.show) { + const cursorPosition = sheets.sheet.cursor.cursorPosition; this.drawMultiCursor(); const columnRow = sheets.sheet.cursor.columnRow; if (columnRow) { @@ -261,10 +264,10 @@ export class Cursor extends Container { columnRow, color: colors.cursorCell, alpha: FILL_ALPHA, - cursorPosition: sheets.sheet.cursor.cursorPosition, + cursorPosition, }); } - if (!columnRow && (!multiCursor || multiCursor.length === 1)) { + if (sheets.sheet.cursor.onlySingleSelection() && !pixiApp.cellsSheet().tables.isHtmlOrImage(cursorPosition)) { this.drawCursorIndicator(); } } diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts deleted file mode 100644 index b572096f0f..0000000000 --- a/quadratic-client/src/app/gridGL/cells/CellsArray.ts +++ /dev/null @@ -1,239 +0,0 @@ -//! Holds borders for tables and code errors. - -// todo: this should move to TableOutline - -import { events } from '@/app/events/events'; -import { sheets } from '@/app/grid/controller/Sheets'; -import { Sheet } from '@/app/grid/sheet/Sheet'; -import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; -import { Coordinate } from '@/app/gridGL/types/size'; -import { getCSSVariableTint } from '@/app/helpers/convertColor'; -import { JsCodeCell, JsRenderCodeCell, RunError } from '@/app/quadratic-core-types'; -import mixpanel from 'mixpanel-browser'; -import { Container, Graphics, ParticleContainer, Rectangle, Sprite, Texture } from 'pixi.js'; -import { intersects } from '../helpers/intersects'; -import { pixiApp } from '../pixiApp/PixiApp'; -import { CellsSheet } from './CellsSheet'; -import { BorderCull, drawBorder } from './drawBorders'; - -export class CellsArray extends Container { - private cellsSheet: CellsSheet; - private codeCells: Map; - private tables: Map; - - private particles: ParticleContainer; - - // only used for the spill error indicators (lines are drawn using sprites in particles for performance) - private graphics: Graphics; - private lines: BorderCull[]; - - constructor(cellsSheet: CellsSheet) { - super(); - this.particles = this.addChild(new ParticleContainer(undefined, { vertices: true, tint: true }, undefined, true)); - this.graphics = this.addChild(new Graphics()); - this.cellsSheet = cellsSheet; - this.lines = []; - this.codeCells = new Map(); - this.tables = new Map(); - events.on('renderCodeCells', this.renderCodeCells); - events.on('sheetOffsets', this.sheetOffsets); - events.on('updateCodeCell', this.updateCodeCell); - } - - destroy() { - events.off('renderCodeCells', this.renderCodeCells); - events.off('sheetOffsets', this.sheetOffsets); - events.off('updateCodeCell', this.updateCodeCell); - super.destroy(); - } - - private key(x: number, y: number): string { - return `${x},${y}`; - } - - private renderCodeCells = (sheetId: string, codeCells: JsRenderCodeCell[]) => { - if (sheetId === this.sheetId) { - const map = new Map(); - codeCells.forEach((cell) => map.set(this.key(cell.x, cell.y), cell)); - this.codeCells = map; - this.create(); - } - }; - - private sheetOffsets = (sheetId: string) => { - if (sheetId === this.cellsSheet.sheetId) { - this.create(); - } - }; - - private updateCodeCell = (options: { - sheetId: string; - x: number; - y: number; - renderCodeCell?: JsRenderCodeCell; - codeCell?: JsCodeCell; - }) => { - const { sheetId, x, y, renderCodeCell, codeCell } = options; - if (sheetId === this.sheetId) { - if (renderCodeCell) { - this.codeCells.set(this.key(x, y), renderCodeCell); - } else { - this.codeCells.delete(this.key(x, y)); - } - this.create(); - - if (!!codeCell && codeCell.std_err !== null && codeCell.evaluation_result) { - try { - // std_err is not null, so evaluation_result will be RunError - const runError = JSON.parse(codeCell.evaluation_result) as RunError; - // track unimplemented errors - if (typeof runError.msg === 'object' && 'Unimplemented' in runError.msg) { - mixpanel.track('[CellsArray].updateCodeCell', { - type: codeCell.language, - error: runError.msg, - }); - } - } catch (error) { - console.error('[CellsArray] Error parsing codeCell.evaluation_result', error); - } - } - } - }; - - get sheetId(): string { - return this.cellsSheet.sheetId; - } - - private create() { - this.lines = []; - this.particles.removeChildren(); - this.graphics.clear(); - this.cellsSheet.cellsMarkers.clear(); - const codeCells = this.codeCells; - if (codeCells.size === 0) { - pixiApp.setViewportDirty(); - return; - } - - const cursor = sheets.sheet.cursor.getCursor(); - codeCells?.forEach((codeCell) => { - const cell = inlineEditorHandler.getShowing(); - const editingCell = cell && codeCell.x === cell.x && codeCell.y === cell.y && cell.sheetId === this.sheetId; - this.draw(codeCell, cursor, editingCell); - }); - pixiApp.setViewportDirty(); - } - - updateCellsArray = () => { - this.create(); - }; - - cheapCull = (bounds: Rectangle): void => { - this.lines.forEach((line) => (line.sprite.visible = intersects.rectangleRectangle(bounds, line.rectangle))); - }; - - get sheet(): Sheet { - const sheet = sheets.getById(this.sheetId); - if (!sheet) throw new Error('Expected sheet to be defined in CellsArray.sheet'); - return sheet; - } - - private draw(codeCell: JsRenderCodeCell, cursor: Coordinate, editingCell?: boolean): void { - const start = this.sheet.getCellOffsets(Number(codeCell.x), Number(codeCell.y)); - - const overlapTest = new Rectangle(Number(codeCell.x), Number(codeCell.y), codeCell.w - 1, codeCell.h - 1); - if (codeCell.spill_error) { - overlapTest.width = 1; - overlapTest.height = 1; - } - - const tint = getCSSVariableTint('primary'); - - // old code that draws a box around the code cell - // let tint = colors.independence; - // if (codeCell.language === 'Python') { - // tint = colors.cellColorUserPython; - // } else if (codeCell.language === 'Formula') { - // tint = colors.cellColorUserFormula; - // } else if (codeCell.language === 'Javascript') { - // tint = colors.cellColorUserJavascript; - // } - - // if (!pixiAppSettings.showCellTypeOutlines) { - // // only show the entire array if the cursor overlaps any part of the output - // if (!intersects.rectanglePoint(overlapTest, new Point(cursor.x, cursor.y))) { - // this.cellsSheet.cellsMarkers.add(start, codeCell, false); - // return; - // } - // } - - if (!editingCell) { - this.cellsSheet.cellsMarkers.add(start, codeCell, true); - } - - const end = this.sheet.getCellOffsets( - Number(codeCell.x) + (codeCell.spill_error ? 1 : codeCell.w), - Number(codeCell.y) + (codeCell.spill_error ? 1 : codeCell.h) - ); - this.drawBox(start, end, tint); - - // save the entire table for hover checks - if (!codeCell.spill_error) { - const endTable = this.sheet.getCellOffsets(Number(codeCell.x) + codeCell.w, Number(codeCell.y) + codeCell.h); - this.tables.set( - this.key(codeCell.x, codeCell.y), - new Rectangle(start.x, start.y, endTable.x - start.x, endTable.y - start.y) - ); - } - } - - private drawBox(start: Rectangle, end: Rectangle, tint: number) { - this.lines.push( - ...drawBorder({ - alpha: 0.5, - tint, - x: start.x, - y: start.y, - width: end.x - start.x, - height: end.y - start.y, - getSprite: this.getSprite, - top: true, - left: true, - bottom: true, - right: true, - }) - ); - // const right = end.x !== start.x + start.width; - // if (right) { - // this.lines.push( - // drawLine({ - // x: start.x + start.width - borderLineWidth / 2, - // y: start.y + borderLineWidth / 2, - // width: borderLineWidth, - // height: start.height, - // alpha: 0.5, - // tint, - // getSprite: this.getSprite, - // }) - // ); - // } - // const bottom = end.y !== start.y + start.height; - // if (bottom) { - // this.lines.push( - // drawLine({ - // x: start.x + borderLineWidth / 2, - // y: start.y + start.height - borderLineWidth / 2, - // width: start.width - borderLineWidth, - // height: borderLineWidth, - // alpha: 0.5, - // tint, - // getSprite: this.getSprite, - // }) - // ); - // } - } - - private getSprite = (): Sprite => { - return this.particles.addChild(new Sprite(Texture.WHITE)); - }; -} diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index a78d10db7a..24dec86209 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -8,6 +8,7 @@ import { TableName } from '@/app/gridGL/cells/tables/TableName'; import { TableOutline } from '@/app/gridGL/cells/tables/TableOutline'; import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; import { intersects } from '@/app/gridGL/helpers/intersects'; +import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Coordinate } from '@/app/gridGL/types/size'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; @@ -110,7 +111,7 @@ export class Table extends Container { intersects.rectanglePoint(rect, { x, y }) || intersects.rectangleRectangle(rect, this.tableName.tableNameBounds) ) { - this.showActive(); + this.showActive(false); return true; } return false; @@ -141,12 +142,14 @@ export class Table extends Container { hideActive() { this.outline.activate(false); this.tableName.hide(); + htmlCellsHandler.hideActive(this.codeCell); pixiApp.setViewportDirty(); } - showActive() { + showActive(isSelected: boolean) { this.outline.activate(true); this.tableName.show(); + htmlCellsHandler.showActive(this.codeCell, isSelected); pixiApp.setViewportDirty(); } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 99c23deb80..10cb7bbc33 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -10,7 +10,8 @@ import { Table } from '@/app/gridGL/cells/tables/Table'; import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Coordinate } from '@/app/gridGL/types/size'; -import { JsCodeCell, JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { JsCodeCell, JsHtmlOutput, JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { CoreClientImage } from '@/app/web-workers/quadraticCore/coreClientMessages'; import { Container, Point, Rectangle } from 'pixi.js'; export interface TablePointerDownResult { @@ -29,11 +30,16 @@ export class Tables extends Container
{ // either rename or sort private actionDataTable: Table | undefined; + // tracks which tables are html or image cells + private htmlOrImage: Set; + tableCursor: string | undefined; constructor(cellsSheet: CellsSheet) { super(); this.cellsSheet = cellsSheet; + this.htmlOrImage = new Set(); + events.on('renderCodeCells', this.renderCodeCells); events.on('updateCodeCell', this.updateCodeCell); @@ -43,8 +49,42 @@ export class Tables extends Container
{ events.on('contextMenu', this.contextMenu); events.on('contextMenuClose', this.contextMenu); + + events.on('htmlOutput', this.htmlOutput); + events.on('htmlUpdate', this.htmlUpdate); + events.on('updateImage', this.updateImage); } + private htmlOutput = (output: JsHtmlOutput[]) => { + this.htmlOrImage.clear(); + output.forEach((htmlOutput) => { + if (htmlOutput.sheet_id === this.cellsSheet.sheetId) { + this.htmlOrImage.add(`${htmlOutput.x},${htmlOutput.y}`); + } + }); + console.log(this.htmlOrImage); + }; + + private htmlUpdate = (output: JsHtmlOutput) => { + if (output.sheet_id === this.cellsSheet.sheetId) { + if (output.html) { + this.htmlOrImage.add(`${output.x},${output.y}`); + } else { + this.htmlOrImage.delete(`${output.x},${output.y}`); + } + } + }; + + private updateImage = (image: CoreClientImage) => { + if (image.sheetId === this.cellsSheet.sheetId) { + if (image.image) { + this.htmlOrImage.add(`${image.x},${image.y}`); + } else { + this.htmlOrImage.delete(`${image.x},${image.y}`); + } + } + }; + get sheet(): Sheet { const sheet = sheets.getById(this.cellsSheet.sheetId); if (!sheet) { @@ -72,7 +112,7 @@ export class Tables extends Container
{ } else { table.updateCodeCell(renderCodeCell); if (table === this.activeTable || table === this.hoverTable || table === this.contextMenuTable) { - table.showActive(); + table.showActive(true); } } } else if (renderCodeCell) { @@ -108,6 +148,9 @@ export class Tables extends Container
{ } const cursor = sheets.sheet.cursor.cursorPosition; this.activeTable = this.children.find((table) => table.intersectsCursor(cursor.x, cursor.y)); + if (this.activeTable) { + this.activeTable.showActive(true); + } if (this.hoverTable === this.activeTable) { this.hoverTable = undefined; } @@ -149,7 +192,7 @@ export class Tables extends Container
{ } this.hoverTable = hover; if (this.hoverTable) { - this.hoverTable.showActive(); + this.hoverTable.showActive(false); } } } @@ -191,7 +234,7 @@ export class Tables extends Container
{ this.hoverTable?.hideActive(); } this.hoverTable = table; - table.showActive(); + table.showActive(false); } return true; } @@ -206,7 +249,7 @@ export class Tables extends Container
{ this.hoverTable?.hideActive(); } this.hoverTable = table; - table.showActive(); + table.showActive(false); } return true; } @@ -220,7 +263,7 @@ export class Tables extends Container
{ this.hoverTable?.hideActive(); } this.hoverTable = tableImage; - tableImage.showActive(); + tableImage.showActive(false); } return true; } @@ -257,7 +300,7 @@ export class Tables extends Container
{ if (options.type === ContextMenuType.TableSort) { this.actionDataTable = this.children.find((table) => table.codeCell === options.table); if (this.actionDataTable) { - this.actionDataTable.showActive(); + this.actionDataTable.showActive(true); if (this.hoverTable === this.actionDataTable) { this.hoverTable = undefined; } @@ -266,7 +309,7 @@ export class Tables extends Container
{ if (options.rename) { this.actionDataTable = this.children.find((table) => table.codeCell === options.table); if (this.actionDataTable) { - this.actionDataTable.showActive(); + this.actionDataTable.showActive(true); if (options.selectedColumn === undefined) { this.actionDataTable.hideTableName(); } else { @@ -277,7 +320,7 @@ export class Tables extends Container
{ } else { this.contextMenuTable = this.children.find((table) => table.codeCell === options.table); if (this.contextMenuTable) { - this.contextMenuTable.showActive(); + this.contextMenuTable.showActive(true); if (this.hoverTable === this.contextMenuTable) { this.hoverTable = undefined; } @@ -356,7 +399,7 @@ export class Tables extends Container
{ this.actionDataTable = undefined; } this.activeTable = table; - table.showActive(); + table.showActive(true); } } @@ -366,4 +409,8 @@ export class Tables extends Container
{ table.resize(width, height); } } + + isHtmlOrImage(cell: Coordinate): boolean { + return this.htmlOrImage.has(`${cell.x},${cell.y}`); + } } From 90d6d1103ee7769c2286a6d49b18a4b0c45c7015 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 30 Oct 2024 09:13:21 -0700 Subject: [PATCH 172/373] html border isnet --- .../src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 3 +-- .../src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index f62fef4128..ca4116dee0 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -38,10 +38,9 @@ export class HtmlCell { this.div = document.createElement('div'); this.div.className = 'html-cell'; - this.div.style.boxShadow = '0 0 0 1px hsl(var(--primary))'; + this.div.style.boxShadow = 'inset 0 0 0 1px hsl(var(--primary))'; const offset = this.sheet.getCellOffsets(Number(htmlCell.x), Number(htmlCell.y)); - // the 0.5 is adjustment for the border this.div.style.left = `${offset.x}px`; this.div.style.top = `${offset.y}px`; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts index 4f8c4620bc..1959ac2b87 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts @@ -149,7 +149,7 @@ class HTMLCellsHandler { (cell) => cell.x === codeCell.x && cell.y === codeCell.y && cell.sheet.id === sheets.sheet.id ); if (cell) { - cell.div.style.boxShadow = '0 0 0 2px hsl(var(--primary))'; + cell.div.style.boxShadow = 'inset 0 0 0 2px hsl(var(--primary))'; cell.iframe.style.pointerEvents = isSelected ? 'auto' : 'none'; } } @@ -159,7 +159,7 @@ class HTMLCellsHandler { (cell) => cell.x === codeCell.x && cell.y === codeCell.y && cell.sheet.id === sheets.sheet.id ); if (cell) { - cell.div.style.boxShadow = '0 0 0 1px hsl(var(--primary))'; + cell.div.style.boxShadow = 'inset 0 0 0 1px hsl(var(--primary))'; cell.iframe.style.pointerEvents = 'none'; } } From b01cd40ea0a684447845ef2abcacb39013cbca36 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 30 Oct 2024 09:17:12 -0700 Subject: [PATCH 173/373] clip all table cells --- quadratic-core/src/grid/sheet/rendering.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 8f5c49f344..7f8a647a8f 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -4,7 +4,7 @@ use crate::grid::js_types::{ JsHtmlOutput, JsNumber, JsRenderCell, JsRenderCellSpecial, JsRenderCodeCell, JsRenderCodeCellState, JsRenderFill, JsSheetFill, JsValidationWarning, }; -use crate::grid::{CellAlign, CodeCellLanguage, Column, DataTable}; +use crate::grid::{CellAlign, CellWrap, CodeCellLanguage, Column, DataTable}; use crate::renderer_constants::{CELL_SHEET_HEIGHT, CELL_SHEET_WIDTH}; use crate::{CellValue, Pos, Rect, RunError, RunErrorMsg, Value}; @@ -28,6 +28,7 @@ impl Sheet { value: &CellValue, language: Option, special: Option, + from_table: bool, ) -> JsRenderCell { if let CellValue::Html(_) = value { return JsRenderCell { @@ -135,13 +136,19 @@ impl Sheet { } _ => value.to_display(), }; + // force clipping for cells in tables + let wrap = if from_table { + Some(CellWrap::Clip) + } else { + format.wrap + }; JsRenderCell { x, y, value, language, align: format.align.or(align), - wrap: format.wrap, + wrap, bold: format.bold, italic: format.italic, text_color: format.text_color, @@ -177,6 +184,7 @@ impl Sheet { })), Some(code_cell_value.language), None, + false, )); } else if let Some(error) = data_table.get_error() { cells.push(self.get_render_cell( @@ -186,6 +194,7 @@ impl Sheet { &CellValue::Error(Box::new(error)), Some(code_cell_value.language), None, + false, )); } else { // find overlap of code_rect into rect @@ -239,7 +248,7 @@ impl Sheet { } }; cells.push( - self.get_render_cell(x, y, column, &value, language, special), + self.get_render_cell(x, y, column, &value, language, special, true), ); } } @@ -271,6 +280,7 @@ impl Sheet { value, None, None, + false, )); } }); From 14da94d68561bf8d55bffab3e27a6fa23d1ceeed Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 30 Oct 2024 09:18:21 -0700 Subject: [PATCH 174/373] fix adjustColumns for htmlCells --- .../src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index ca4116dee0..b311e18631 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -288,9 +288,7 @@ export class HtmlCell { updateOffsets() { const offset = this.sheet.getCellOffsets(this.x, this.y); - - // the 0.5 is adjustment for the border - this.div.style.left = `${offset.x - 0.5}px`; - this.div.style.top = `${offset.y + offset.height - 0.5}px`; + this.div.style.left = `${offset.x}px`; + this.div.style.top = `${offset.y}px`; } } From 94e70c8dd708a31ecd540b2fe7f0b2132a67b8e0 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 04:55:26 -0700 Subject: [PATCH 175/373] resizing image properly updates grid lines --- .../src/app/grid/sheet/GridOverflowLines.ts | 5 +++++ .../src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 4 +--- .../src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css | 12 ++++++------ .../src/app/gridGL/cells/tables/Tables.ts | 6 +++++- .../app/gridGL/interaction/pointer/PointerImages.ts | 2 +- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts b/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts index f2c1851168..5a5bfe93e0 100644 --- a/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts +++ b/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts @@ -34,6 +34,11 @@ export class GridOverflowLines { }); } + resizeImage(x: number, y: number, width: number, height: number) { + this.updateImageHtml(x, y, width, height); + } + + // updates the hash with a rectangle of an image or html table updateImageHtml(column: number, row: number, width?: number, height?: number) { if (width === undefined || height === undefined) { this.overflowImageHtml.delete(`${column},${row}`); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index b311e18631..75147da73f 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -13,8 +13,6 @@ const tolerance = 5; const DEFAULT_HTML_WIDTH = '600'; const DEFAULT_HTML_HEIGHT = '460'; -const ALLOW_CHART_INTERACTIVITY = import.meta.env.VITE_ALLOW_CHART_INTERACTIVITY === '1'; - export class HtmlCell { private right: HTMLDivElement; private bottom: HTMLDivElement; @@ -51,7 +49,7 @@ export class HtmlCell { this.iframe = document.createElement('iframe'); this.iframe.className = 'html-cell-iframe'; - this.iframe.style.pointerEvents = ALLOW_CHART_INTERACTIVITY ? 'auto' : 'none'; + this.iframe.style.pointerEvents = 'none'; this.iframe.srcdoc = htmlCell.html; this.iframe.title = `HTML from ${htmlCell.x}, ${htmlCell.y}}`; this.iframe.width = this.width; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css index fd511f6df3..759539aaa5 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css @@ -26,9 +26,9 @@ .html-resize-control-right { position: absolute; width: 5px; - height: calc(100% + 2px); - top: -1px; - right: -6px; + height: 100%; + top: 0; + right: -5px; } .html-resize-control-right-corner { @@ -42,7 +42,7 @@ .html-resize-control-bottom { position: absolute; height: 5px; - width: calc(100% + 2px); - bottom: -6px; - left: -1px; + width: 100%; + bottom: -5px; + left: 0; } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 10cb7bbc33..820bb3a2e9 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -62,7 +62,6 @@ export class Tables extends Container
{ this.htmlOrImage.add(`${htmlOutput.x},${htmlOutput.y}`); } }); - console.log(this.htmlOrImage); }; private htmlUpdate = (output: JsHtmlOutput) => { @@ -177,6 +176,9 @@ export class Tables extends Container
{ // Checks if the mouse cursor is hovering over a table or table heading. checkHover(world: Point, event: PointerEvent) { + // don't allow hover when the mouse is over the headings + if (world.y < pixiApp.viewport.y - pixiApp.headings.headingSize.height) return; + // only allow hover when the mouse is over the canvas (and not menus) if (event.target !== pixiApp.canvas) { return; @@ -407,6 +409,8 @@ export class Tables extends Container
{ const table = this.children.find((table) => table.codeCell.x === x && table.codeCell.y === y); if (table) { table.resize(width, height); + sheets.sheet.gridOverflowLines.resizeImage(x, y, width, height); + pixiApp.gridLines.dirty = true; } } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts index 7d406b3bec..2a2dacf946 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerImages.ts @@ -66,7 +66,7 @@ export class PointerImages { } this.resizing.image.temporaryResize(width, height); pixiApp.cellImages.dirtyResizing = true; - // pixiApp.cellImages.dirtyBorders = true; + pixiApp.cellsSheet().tables.resizeTable(this.resizing.image.column, this.resizing.image.row, width, height); pixiApp.setViewportDirty(); return true; } From 4e75b0c05921ccc6e23740dcefdbbcaa23102150 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 05:00:00 -0700 Subject: [PATCH 176/373] fix bug with position of context menu --- .../HTMLGrid/contextMenus/ContextMenuBase.tsx | 50 +------------------ 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx index 607e1f4714..4463a549cd 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx @@ -45,56 +45,9 @@ export const ContextMenuBase = ({ }, [onClose]); const left = contextMenu.world?.x ?? 0; - const [top, setTop] = useState(contextMenu.world?.y ?? 0); - // useEffect(() => { - // const updateAfterRender = () => { - // const content = refContent.current; - // if (open && content) { - // let newTop = contextMenu.world?.y ?? 0; - // if (open && content) { - // // we use the screen bounds in world coordinates to determine if the - // // menu is going to be cut off - // const viewportTop = pixiApp.viewport.toWorld(0, 0).y; - // const viewportBottom = pixiApp.viewport.toWorld(0, window.innerHeight).y; - - // // we use the viewport bounds to determine the direction the menu is - // // opening - // const bounds = pixiApp.viewport.getVisibleBounds(); - - // // menu is opening downwards - // if (newTop < bounds.y + bounds.height / 2) { - // if (newTop + content.offsetHeight > viewportBottom) { - // newTop = viewportBottom - content.offsetHeight; - // } - // } - - // // menu is opening upwards - // else { - // if (newTop - content.offsetHeight < viewportTop) { - // newTop = viewportTop + content.offsetHeight; - // } - // } - // } - // setTop(newTop); - // } - // }; - - // // need to wait for the next render to update the position - // setTimeout(updateAfterRender); - // }, [contextMenu.world, open]); + const top = contextMenu.world?.y ?? 0; return ( - //
- //
); }; From 6227df8b2f35f323b4830685779612c669af7f7f Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 05:03:41 -0700 Subject: [PATCH 177/373] ensure gridLines are defined in constructor --- quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index f73493c450..c2fa04c52d 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -52,7 +52,7 @@ export class PixiApp { canvas!: HTMLCanvasElement; viewport!: Viewport; - gridLines!: GridLines; + gridLines: GridLines; axesLines!: AxesLines; cursor!: Cursor; cellHighlights!: CellHighlights; @@ -93,6 +93,7 @@ export class PixiApp { constructor() { // This is created first so it can listen to messages from QuadraticCore. this.cellsSheets = new CellsSheets(); + this.gridLines = new GridLines(); this.cellImages = new UICellImages(); this.validations = new UIValidations(); this.overHeadingsColumnsHeaders = new Container(); @@ -157,7 +158,7 @@ export class PixiApp { this.debug = this.viewportContents.addChild(new Graphics()); this.cellsSheets = this.viewportContents.addChild(this.cellsSheets); - this.gridLines = this.viewportContents.addChild(new GridLines()); + this.gridLines = this.viewportContents.addChild(this.gridLines); this.viewportContents.addChild(this.overHeadingsColumnsHeaders); // this is a hack to ensure that table column names appears over the column From 47f77e1682128a136087953046d9416e4fd1d434 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 05:24:45 -0700 Subject: [PATCH 178/373] hook up color picker to tables --- quadratic-client/index.html | 2 +- .../app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx | 2 +- .../src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx | 5 +++-- quadratic-client/src/app/gridGL/cells/CellsFills.ts | 2 ++ .../app/gridGL/cells/tables/TableColumnHeadersGridLines.ts | 7 +++++-- quadratic-client/src/app/gridGL/cells/tables/TableName.ts | 6 ++++-- .../src/app/gridGL/cells/tables/TableOutline.ts | 7 +++++-- .../src/app/gridGL/interaction/pointer/PointerTable.ts | 2 +- 8 files changed, 22 insertions(+), 11 deletions(-) diff --git a/quadratic-client/index.html b/quadratic-client/index.html index d1ca8862aa..95b8d24d0b 100644 --- a/quadratic-client/index.html +++ b/quadratic-client/index.html @@ -13,7 +13,7 @@ --> Quadratic - diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx index d862eaeb40..cd53765efd 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableContextMenu.tsx @@ -7,7 +7,7 @@ import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; export const TableContextMenu = () => { return ( - {({ contextMenu }) => } + {({ contextMenu }) => } ); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index 6cb95ffb42..bf197ec020 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -12,10 +12,11 @@ import { useMemo } from 'react'; interface Props { defaultRename?: boolean; codeCell?: JsRenderCodeCell; + selectedColumn?: number; } export const TableMenu = (props: Props) => { - const { defaultRename, codeCell } = props; + const { defaultRename, codeCell, selectedColumn } = props; const cell = getCodeCell(codeCell?.language); const isCodeCell = cell && cell.id !== 'Import'; @@ -33,7 +34,7 @@ export const TableMenu = (props: Props) => { ); }, [codeCell]); - if (!codeCell) { + if (!codeCell || selectedColumn !== undefined) { return null; } diff --git a/quadratic-client/src/app/gridGL/cells/CellsFills.ts b/quadratic-client/src/app/gridGL/cells/CellsFills.ts index 287f69a27d..e9b8c8f1e5 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsFills.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsFills.ts @@ -2,6 +2,7 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { JsRenderCodeCell, JsRenderFill, JsSheetFill } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; +import { sharedEvents } from '@/shared/sharedEvents'; import { Container, Graphics, ParticleContainer, Rectangle, Sprite, Texture } from 'pixi.js'; import { Sheet } from '../../grid/sheet/Sheet'; import { convertColorStringToTint, getCSSVariableTint } from '../../helpers/convertColor'; @@ -64,6 +65,7 @@ export class CellsFills extends Container { events.on('cursorPosition', this.setDirty); events.on('resizeHeadingColumn', this.drawCells); events.on('resizeHeadingRow', this.drawCells); + sharedEvents.on('changeThemeAccentColor', this.drawAlternatingColors); pixiApp.viewport.on('zoomed', this.setDirty); pixiApp.viewport.on('moved', this.setDirty); } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts index 9a55abe76b..47745c407b 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeadersGridLines.ts @@ -4,6 +4,7 @@ import { Table } from '@/app/gridGL/cells/tables/Table'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; +import { sharedEvents } from '@/shared/sharedEvents'; import { Graphics } from 'pixi.js'; export class TableColumnHeadersGridLines extends Graphics { @@ -12,9 +13,11 @@ export class TableColumnHeadersGridLines extends Graphics { constructor(table: Table) { super(); this.table = table; + + sharedEvents.on('changeThemeAccentColor', this.update); } - update() { + update = () => { this.clear(); if (pixiAppSettings.showGridLines && pixiApp.gridLines?.visible) { const { y0, y1, lines } = this.table.getColumnHeaderLines(); @@ -39,5 +42,5 @@ export class TableColumnHeadersGridLines extends Graphics { this.moveTo(lines[0], y0).lineTo(lines[lines.length - 1], y0); this.moveTo(lines[0], y1).lineTo(lines[lines.length - 1], y1); } - } + }; } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts index 0ac2728b8a..9a61955c96 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableName.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableName.ts @@ -8,6 +8,7 @@ import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { OPEN_SANS_FIX } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; import { CELL_HEIGHT } from '@/shared/constants/gridConstants'; +import { sharedEvents } from '@/shared/sharedEvents'; import { BitmapText, Container, Graphics, Point, Rectangle, Sprite, Texture } from 'pixi.js'; export const TABLE_NAME_FONT_SIZE = 12; @@ -43,10 +44,11 @@ export class TableName extends Container { if (sheets.sheet.id === this.table.sheet.id) { pixiApp.overHeadingsTableNames.addChild(this); } + sharedEvents.on('changeThemeAccentColor', this.drawBackground); this.visible = false; } - private drawBackground() { + private drawBackground = () => { const width = this.text.width + OPEN_SANS_FIX.x + @@ -60,7 +62,7 @@ export class TableName extends Container { this.background.endFill(); this.backgroundWidth = width; - } + }; private drawSymbol() { if (this.symbol) { diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts index f1e5ee44b6..59d512c540 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts @@ -5,6 +5,7 @@ import { Table } from '@/app/gridGL/cells/tables/Table'; import { generatedTextures } from '@/app/gridGL/generateTextures'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { colors } from '@/app/theme/colors'; +import { sharedEvents } from '@/shared/sharedEvents'; import { Graphics, Rectangle } from 'pixi.js'; const SPILL_HIGHLIGHT_THICKNESS = 2; @@ -17,6 +18,8 @@ export class TableOutline extends Graphics { constructor(table: Table) { super(); this.table = table; + + sharedEvents.on('changeThemeAccentColor', this.update); } activate(active: boolean) { @@ -25,7 +28,7 @@ export class TableOutline extends Graphics { this.update(); } - update() { + update = () => { this.clear(); // draw the table selected outline @@ -53,7 +56,7 @@ export class TableOutline extends Graphics { this.drawDashedRectangle(rectangle, colors.cellColorError); }); } - } + }; // draw a dashed and filled rectangle to identify the cause of the spill error private drawDashedRectangle(rectangle: Rectangle, color: number) { diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index 87e80c48e2..0996094f19 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -85,7 +85,7 @@ export class PointerTable { } if (event.button === 2 || (isMac && event.button === 0 && event.ctrlKey)) { events.emit('contextMenu', { - type: ContextMenuType.Table, + type: tableDown.type === 'column-name' ? ContextMenuType.TableColumn : ContextMenuType.Table, world, column: tableDown.table.x, row: tableDown.table.y, From e9ece641db832b8af02b26e575d6db88729712fb Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 05:30:25 -0700 Subject: [PATCH 179/373] hook up asc and desc from column menu --- .../src/app/actions/dataTableSpec.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 76d7abd08a..c83a933af7 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -179,14 +179,38 @@ export const dataTableSpec: DataTableSpec = { label: 'Sort column ascending', Icon: UpArrowIcon, run: () => { - console.log('TODO: sort column ascending'); + const table = getTable(); + if (table) { + const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; + if (selectedColumn !== undefined) { + quadraticCore.sortDataTable( + sheets.sheet.id, + table.x, + table.y, + [{ column_index: selectedColumn, direction: 'Ascending' }], + sheets.getCursorPosition() + ); + } + } }, }, [Action.SortTableColumnDescending]: { label: 'Sort column descending', Icon: DownArrowIcon, run: () => { - console.log('TODO: sort column descending'); + const table = getTable(); + if (table) { + const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; + if (selectedColumn !== undefined) { + quadraticCore.sortDataTable( + sheets.sheet.id, + table.x, + table.y, + [{ column_index: selectedColumn, direction: 'Descending' }], + sheets.getCursorPosition() + ); + } + } }, }, [Action.HideTableColumn]: { From a94095b565802fe7237ba07f1ab14fa29335c2d1 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 05:46:17 -0700 Subject: [PATCH 180/373] fix bug with renaming column names --- .../src/app/actions/dataTableSpec.ts | 14 +++++++------ .../contextMenus/TableColumnHeaderRename.tsx | 4 ++-- .../src/app/gridGL/cells/tables/Tables.ts | 20 ++++++++++++++----- .../interaction/pointer/PointerTable.ts | 2 +- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 5 ++++- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index c83a933af7..604bccd0e5 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -165,13 +165,15 @@ export const dataTableSpec: DataTableSpec = { run: () => { const table = getTable(); if (table) { - setTimeout(() => { - const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; - if (selectedColumn !== undefined) { - const contextMenu = { type: ContextMenuType.Table, rename: true, table, selectedColumn }; + const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; + console.log(selectedColumn); + if (selectedColumn !== undefined) { + setTimeout(() => { + const contextMenu = { type: ContextMenuType.TableColumn, rename: true, table, selectedColumn }; events.emit('contextMenu', contextMenu); - } - }); + console.log('emitting...'); + }); + } } }, }, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx index ff5ac903f1..802b465bcb 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -11,7 +11,7 @@ export const TableColumnHeaderRename = () => { const position = useMemo(() => { if ( - contextMenu.type !== ContextMenuType.Table || + contextMenu.type !== ContextMenuType.TableColumn || !contextMenu.rename || !contextMenu.table || contextMenu.selectedColumn === undefined @@ -33,7 +33,7 @@ export const TableColumnHeaderRename = () => { }, [contextMenu.selectedColumn, contextMenu.table]); if ( - contextMenu.type !== ContextMenuType.Table || + contextMenu.type !== ContextMenuType.TableColumn || !contextMenu.rename || !contextMenu.table || contextMenu.selectedColumn === undefined diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 820bb3a2e9..592e3fa4fc 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -312,11 +312,7 @@ export class Tables extends Container
{ this.actionDataTable = this.children.find((table) => table.codeCell === options.table); if (this.actionDataTable) { this.actionDataTable.showActive(true); - if (options.selectedColumn === undefined) { - this.actionDataTable.hideTableName(); - } else { - this.actionDataTable.hideColumnHeaders(options.selectedColumn); - } + this.actionDataTable.hideTableName(); this.hoverTable = undefined; } } else { @@ -328,6 +324,20 @@ export class Tables extends Container
{ } } } + } else if ( + options.type === ContextMenuType.TableColumn && + options.table && + options.rename && + options.selectedColumn !== undefined + ) { + this.actionDataTable = this.children.find((table) => table.codeCell === options.table); + if (this.actionDataTable) { + this.actionDataTable.showActive(true); + if (this.hoverTable === this.actionDataTable) { + this.hoverTable = undefined; + } + this.actionDataTable.hideColumnHeaders(options.selectedColumn); + } } pixiApp.setViewportDirty(); }; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index 0996094f19..b8d1928007 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -50,7 +50,7 @@ export class PointerTable { } if (this.doubleClickTimeout) { events.emit('contextMenu', { - type: ContextMenuType.Table, + type: ContextMenuType.TableColumn, world, column: tableDown.table.x, row: tableDown.table.y, diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index 994b84efc4..511131daa7 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -237,7 +237,10 @@ class PixiAppSettings { }; isRenamingTable(): boolean { - return !!(this.contextMenu.type === ContextMenuType.Table && this.contextMenu.rename); + return !!( + (this.contextMenu.type === ContextMenuType.Table || this.contextMenu.type === ContextMenuType.TableColumn) && + this.contextMenu.rename + ); } } From 66cc859a4334f1374e79afdb6b174c3d09a33961 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 06:33:37 -0700 Subject: [PATCH 181/373] themes work on tables --- quadratic-client/src/app/gridGL/cells/CellsFills.ts | 4 +++- .../src/app/gridGL/cells/tables/TableColumnHeader.ts | 2 +- .../src/app/gridGL/cells/tables/TableColumnHeaders.ts | 9 ++++++++- quadratic-client/src/app/helpers/convertColor.ts | 10 ++++++++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsFills.ts b/quadratic-client/src/app/gridGL/cells/CellsFills.ts index e9b8c8f1e5..6948eca814 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsFills.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsFills.ts @@ -21,6 +21,8 @@ interface ColumnRow { timestamp: number; } +const ALTERNATING_COLOR_LUMINOSITY = 1.85; + export class CellsFills extends Container { private cellsSheet: CellsSheet; private cells: JsRenderFill[] = []; @@ -209,7 +211,7 @@ export class CellsFills extends Container { private drawAlternatingColors = () => { this.alternatingColorsGraphics.clear(); - const color = getCSSVariableTint('table-alternating-background'); + const color = getCSSVariableTint('primary', { luminosity: ALTERNATING_COLOR_LUMINOSITY }); this.alternatingColors.forEach((table) => { const bounds = this.sheet.getScreenRectangle(table.x, table.y + 1, table.w - 1, table.y); let yOffset = bounds.y; diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 13fb987336..e2728d96e1 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -53,7 +53,7 @@ export class TableColumnHeader extends Container { this.h = height; this.position.set(x, 0); - const tint = getCSSVariableTint('table-column-header-foreground'); + const tint = getCSSVariableTint('foreground'); this.columnName = this.addChild( new BitmapText(name, { fontName: 'OpenSans-Bold', diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 792f10e534..96682a5c58 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -9,8 +9,12 @@ import { Coordinate } from '@/app/gridGL/types/size'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { JsDataTableColumn, SortDirection } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { sharedEvents } from '@/shared/sharedEvents'; import { Container, Graphics, Point, Rectangle } from 'pixi.js'; +// used to make the column header background a bit darker than the primary color +const COLUMN_HEADER_BACKGROUND_LUMINOSITY = 1.75; + export class TableColumnHeaders extends Container { private table: Table; private background: Graphics; @@ -24,11 +28,14 @@ export class TableColumnHeaders extends Container { this.table = table; this.background = this.addChild(new Graphics()); this.columns = this.addChild(new Container()); + + sharedEvents.on('changeThemeAccentColor', this.drawBackground); } drawBackground = () => { this.background.clear(); - const color = getCSSVariableTint('table-column-header-background'); + const color = getCSSVariableTint('primary', { luminosity: COLUMN_HEADER_BACKGROUND_LUMINOSITY }); + this.background.beginFill(color); // need to adjust so the outside border is still visible this.background.drawShape(new Rectangle(0.5, 0, this.table.tableBounds.width - 1, this.headerHeight)); diff --git a/quadratic-client/src/app/helpers/convertColor.ts b/quadratic-client/src/app/helpers/convertColor.ts index 8fbbb54cbd..392f5d8fc0 100644 --- a/quadratic-client/src/app/helpers/convertColor.ts +++ b/quadratic-client/src/app/helpers/convertColor.ts @@ -77,8 +77,10 @@ export function convertRgbaToTint(rgba: Rgba): { tint: number; alpha: number } { * Given the name of a CSS variable that maps to an HSL string, return the tint * we can use in pixi. * @param cssVariableName - CSS var without the `--` prefix + * @param options - Optional options object + * @param options.luminosity - If provided, will mulitply the luminosity by this number */ -export function getCSSVariableTint(cssVariableName: string): number { +export function getCSSVariableTint(cssVariableName: string, options?: { luminosity?: number }): number { if (cssVariableName.startsWith('--')) { console.warn( '`getCSSVariableTint` expects a CSS variable name without the `--` prefix. Are you sure you meant: `%s`', @@ -87,7 +89,11 @@ export function getCSSVariableTint(cssVariableName: string): number { } const hslColorString = getComputedStyle(document.documentElement).getPropertyValue(`--${cssVariableName}`).trim(); - const parsed = Color.hsl(hslColorString.split(' ').map(parseFloat)); + const numbers = hslColorString.split(' ').map(parseFloat); + if (options?.luminosity) { + numbers[2] *= options.luminosity; + } + const parsed = Color.hsl(...numbers); const out = parsed.rgbNumber(); return out; } From c6e8e0b5581b156a7157aaf8e1dca82fccb3cf90 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 06:40:22 -0700 Subject: [PATCH 182/373] fix sort table and layout --- quadratic-client/src/app/actions/dataTableSpec.ts | 2 +- .../app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 604bccd0e5..388fb7afaa 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -143,8 +143,8 @@ export const dataTableSpec: DataTableSpec = { label: 'Sort', Icon: SortIcon, run: () => { + const table = getTable(); setTimeout(() => { - const table = getTable(); const contextMenu = { type: ContextMenuType.TableSort, table }; events.emit('contextMenu', contextMenu); }); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx index f773ea310e..ff49c2f3ee 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -28,7 +28,7 @@ export const TableColumnContextMenu = () => { - + {contextMenu.table?.language === 'Import' ? 'Data' : 'Code'} Table From 1f10377e4fed32250b337f81997ef8f39105a814 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 06:58:37 -0700 Subject: [PATCH 183/373] added tab to PixiRename --- .../src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx index 83f35bdbe6..1bb15a65d7 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx @@ -83,7 +83,7 @@ export const PixiRename = (props: Props) => { close(); e.stopPropagation(); e.preventDefault(); - } else if (e.key === 'Enter') { + } else if (e.key === 'Enter' || e.key === 'Tab') { saveAndClose(); e.stopPropagation(); e.preventDefault(); From 559dbd6e458e4ecb0ccb52f11fb3f034eb70ea17 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 07:11:04 -0700 Subject: [PATCH 184/373] table moving by dragging table name --- .../interaction/pointer/PointerCellMoving.ts | 25 +++++++++++++++++++ .../interaction/pointer/PointerTable.ts | 11 ++++++++ 2 files changed, 36 insertions(+) diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index 70233407e2..8e8811b05a 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -25,6 +25,7 @@ interface MoveCells { toColumn: number; toRow: number; offset: { x: number; y: number }; + table?: { offsetX: number; offsetY: number }; } export class PointerCellMoving { @@ -43,6 +44,30 @@ export class PointerCellMoving { } } + // Starts a table move. + tableMove(column: number, row: number, point: Point) { + if (this.state) return false; + this.state = 'move'; + this.startCell = new Point(column, row); + const offset = sheets.sheet.getCellOffsets(column, row); + this.movingCells = { + column, + row, + width: 1, + height: 1, + toColumn: column, + toRow: row, + offset: { x: 0, y: 0 }, + table: { offsetX: point.x - offset.x, offsetY: point.y - offset.y }, + }; + events.emit('cellMoving', true); + pixiApp.viewport.mouseEdges({ + distance: MOUSE_EDGES_DISTANCE, + allowButtons: true, + speed: MOUSE_EDGES_SPEED / pixiApp.viewport.scale.x, + }); + } + findCorner(world: Point): Point { return world; } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index b8d1928007..7cd217f756 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -15,6 +15,7 @@ export class PointerTable { cursor: string | undefined; private doubleClickTimeout: number | undefined; + private tableNameDown: { column: number; row: number; point: Point } | undefined; private pointerDownTableName(world: Point, tableDown: TablePointerDownResult) { pixiApp.cellsSheet().tables.ensureActive(tableDown.table); @@ -31,6 +32,7 @@ export class PointerTable { this.doubleClickTimeout = window.setTimeout(() => { this.doubleClickTimeout = undefined; }, DOUBLE_CLICK_TIME); + this.tableNameDown = { column: tableDown.table.x, row: tableDown.table.y, point: world }; } } @@ -112,6 +114,15 @@ export class PointerTable { clearTimeout(this.doubleClickTimeout); this.doubleClickTimeout = undefined; } + if (this.tableNameDown) { + pixiApp.pointer.pointerCellMoving.tableMove( + this.tableNameDown.column, + this.tableNameDown.row, + this.tableNameDown.point + ); + this.tableNameDown = undefined; + return true; + } const result = pixiApp.cellsSheet().tables.pointerMove(world); this.cursor = pixiApp.cellsSheet().tables.tableCursor; return result; From 7cd0d693c7d8680f77fbb4e0d787153e48eac282 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 31 Oct 2024 10:14:14 -0700 Subject: [PATCH 185/373] initial work on formats for tables --- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../execution/run_code/run_javascript.rs | 25 ++--- .../execution/run_code/run_python.rs | 25 ++--- .../src/controller/execution/spills.rs | 27 +----- quadratic-core/src/grid/data_table/column.rs | 1 + quadratic-core/src/grid/data_table/mod.rs | 5 +- .../src/grid/data_table/table_formats.rs | 19 ++++ .../src/grid/file/serialize/data_table.rs | 95 ++++++++++++++++++- quadratic-core/src/grid/file/v1_7/file.rs | 1 + quadratic-core/src/grid/file/v1_8/schema.rs | 12 ++- quadratic-core/src/grid/js_types.rs | 3 +- quadratic-core/src/grid/sheet/rendering.rs | 36 ++++--- 12 files changed, 176 insertions(+), 75 deletions(-) create mode 100644 quadratic-core/src/grid/data_table/table_formats.rs diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 600438d05c..cacf2105ed 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -36,7 +36,7 @@ export interface JsNumber { decimals: number | null, commas: boolean | null, for export interface JsOffset { column: number | null, row: number | null, size: number, } export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } -export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader" | "TableAlternatingColor"; +export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } diff --git a/quadratic-core/src/controller/execution/run_code/run_javascript.rs b/quadratic-core/src/controller/execution/run_code/run_javascript.rs index 82ff891b14..7a46bdd50b 100644 --- a/quadratic-core/src/controller/execution/run_code/run_javascript.rs +++ b/quadratic-core/src/controller/execution/run_code/run_javascript.rs @@ -35,7 +35,7 @@ mod tests { controller::{ execution::run_code::get_cells::JsGetCellResponse, transaction_types::JsCodeResult, }, - grid::js_types::{JsRenderCell, JsRenderCellSpecial}, + grid::js_types::JsRenderCell, ArraySize, CellValue, Pos, Rect, }; use bigdecimal::BigDecimal; @@ -338,24 +338,15 @@ mod tests { assert_eq!(cells.len(), 3); assert_eq!( cells[0], - JsRenderCell::new_number( - 0, - 0, - 1, - Some(CodeCellLanguage::Javascript), - Some(JsRenderCellSpecial::TableAlternatingColor) - ) - ); - assert_eq!(cells[1], JsRenderCell::new_number(0, 1, 2, None, None)); + JsRenderCell::new_number(0, 0, 1, Some(CodeCellLanguage::Javascript), None, true) + ); + assert_eq!( + cells[1], + JsRenderCell::new_number(0, 1, 2, None, None, true) + ); assert_eq!( cells[2], - JsRenderCell::new_number( - 0, - 2, - 3, - None, - Some(JsRenderCellSpecial::TableAlternatingColor) - ) + JsRenderCell::new_number(0, 2, 3, None, None, true) ); } diff --git a/quadratic-core/src/controller/execution/run_code/run_python.rs b/quadratic-core/src/controller/execution/run_code/run_python.rs index e455cd7ee1..482a7a55cc 100644 --- a/quadratic-core/src/controller/execution/run_code/run_python.rs +++ b/quadratic-core/src/controller/execution/run_code/run_python.rs @@ -34,7 +34,7 @@ mod tests { controller::{ execution::run_code::get_cells::JsGetCellResponse, transaction_types::JsCodeResult, }, - grid::js_types::{JsRenderCell, JsRenderCellSpecial}, + grid::js_types::JsRenderCell, ArraySize, CellValue, Pos, Rect, }; use bigdecimal::BigDecimal; @@ -353,24 +353,15 @@ mod tests { assert_eq!(cells.len(), 3); assert_eq!( cells[0], - JsRenderCell::new_number( - 0, - 0, - 1, - Some(CodeCellLanguage::Python), - Some(JsRenderCellSpecial::TableAlternatingColor) - ) - ); - assert_eq!(cells[1], JsRenderCell::new_number(0, 1, 2, None, None)); + JsRenderCell::new_number(0, 0, 1, Some(CodeCellLanguage::Python), None, true) + ); + assert_eq!( + cells[1], + JsRenderCell::new_number(0, 1, 2, None, None, true) + ); assert_eq!( cells[2], - JsRenderCell::new_number( - 0, - 2, - 3, - None, - Some(JsRenderCellSpecial::TableAlternatingColor) - ) + JsRenderCell::new_number(0, 2, 3, None, None, true) ); // transaction should be completed diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index 96a9dab147..e8551a341d 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -114,7 +114,7 @@ mod tests { use crate::controller::active_transactions::pending_transaction::PendingTransaction; use crate::controller::GridController; use crate::grid::js_types::{JsNumber, JsRenderCell, JsRenderCellSpecial}; - use crate::grid::{CellAlign, CodeCellLanguage, CodeRun, DataTable, DataTableKind}; + use crate::grid::{CellAlign, CellWrap, CodeCellLanguage, CodeRun, DataTable, DataTableKind}; use crate::wasm_bindings::js::{clear_js_calls, expect_js_call_count}; use crate::{Array, CellValue, Pos, Rect, SheetPos, Value}; @@ -143,6 +143,7 @@ mod tests { align: Some(CellAlign::Right), number: Some(JsNumber::default()), special, + wrap: Some(CellWrap::Clip), ..Default::default() }] } @@ -295,13 +296,7 @@ mod tests { // should be B0: "1" since spill was removed assert_eq!( render_cells, - output_number( - 0, - 0, - "1", - Some(CodeCellLanguage::Formula), - Some(JsRenderCellSpecial::TableAlternatingColor) - ), + output_number(0, 0, "1", Some(CodeCellLanguage::Formula), None), ); } @@ -338,13 +333,7 @@ mod tests { let render_cells = sheet.get_render_cells(Rect::single_pos(Pos { x: 0, y: 0 })); assert_eq!( render_cells, - output_number( - 0, - 0, - "1", - Some(CodeCellLanguage::Formula), - Some(JsRenderCellSpecial::TableAlternatingColor) - ) + output_number(0, 0, "1", Some(CodeCellLanguage::Formula), None) ); let render_cells = sheet.get_render_cells(Rect::single_pos(Pos { x: 0, y: 1 })); assert_eq!(render_cells, output_number(0, 1, "2", None, None)); @@ -430,13 +419,7 @@ mod tests { let render_cells = sheet.get_render_cells(Rect::single_pos(Pos { x: 11, y: 9 })); assert_eq!( render_cells, - output_number( - 11, - 9, - "1", - Some(CodeCellLanguage::Formula), - Some(JsRenderCellSpecial::TableAlternatingColor) - ) + output_number(11, 9, "1", Some(CodeCellLanguage::Formula), None) ); } diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index 2c15b829a2..d99b0ac8d8 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -233,6 +233,7 @@ pub mod test { show_header: true, header_is_first_row: true, alternating_colors: true, + formats: Default::default(), }; sheet.set_cell_value( pos, diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index f2dc2dea7b..1a123326a9 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -7,7 +7,7 @@ pub mod column; pub mod display_value; pub mod sort; - +pub mod table_formats; use std::num::NonZeroU32; use crate::cellvalue::Import; @@ -23,6 +23,7 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use sort::DataTableSort; use strum_macros::Display; +use table_formats::TableFormats; use tabled::{ builder::Builder, settings::{Color, Modify, Style}, @@ -82,6 +83,7 @@ pub struct DataTable { pub spill_error: bool, pub last_modified: DateTime, pub alternating_colors: bool, + pub formats: TableFormats, } impl From<(Import, Array, &Grid)> for DataTable { @@ -129,6 +131,7 @@ impl DataTable { spill_error, alternating_colors: true, last_modified: Utc::now(), + formats: Default::default(), }; if header_is_first_row { diff --git a/quadratic-core/src/grid/data_table/table_formats.rs b/quadratic-core/src/grid/data_table/table_formats.rs new file mode 100644 index 0000000000..180d1ef4f8 --- /dev/null +++ b/quadratic-core/src/grid/data_table/table_formats.rs @@ -0,0 +1,19 @@ +//! Tracks formatting for a Table. There are three levels of formatting (in order of precedence): +//! - Cells (tracked to the unsorted index) +//! - Columns +//! - Table + +use crate::grid::{block::SameValue, formats::format::Format, ColumnData}; +use serde::{Deserialize, Serialize}; + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TableFormats { + pub table: Option, + + // Indexed by column index. + pub columns: Vec, + + // Indexed by column index and then via RunLengthEncoding. + // Note: index is unsorted index. + pub cells: Vec>>, +} diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 1e0619a0a5..cfa51e5a3d 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use anyhow::{anyhow, Result}; use chrono::Utc; use indexmap::IndexMap; @@ -5,9 +7,14 @@ use itertools::Itertools; use crate::{ grid::{ - data_table::column::DataTableColumn, - data_table::sort::{DataTableSort, SortDirection}, - CodeRun, DataTable, DataTableKind, + block::SameValue, + data_table::{ + column::DataTableColumn, + sort::{DataTableSort, SortDirection}, + }, + formats::format::Format, + table_formats::TableFormats, + CodeRun, ColumnData, DataTable, DataTableKind, }, ArraySize, Axis, Pos, RunError, RunErrorMsg, Value, }; @@ -15,6 +22,7 @@ use crate::{ use super::{ cell_value::{export_cell_value, import_cell_value}, current, + format::{export_format, import_format}, }; pub(crate) fn import_run_error_msg_builder( @@ -134,6 +142,37 @@ pub(crate) fn import_code_run_builder(code_run: current::CodeRunSchema) -> Resul Ok(code_run) } +pub(crate) fn import_formats( + column: HashMap>, +) -> ColumnData> { + let mut data = ColumnData::new(); + column.into_iter().for_each(|(start, schema)| { + let value = import_format(schema.value); + let len = schema.len as usize; + data.insert_block(start, len, value); + }); + data +} + +fn import_data_table_formats(formats: current::TableFormatsSchema) -> TableFormats { + let table = formats.table.map(|format| import_format(format)); + let columns = formats + .columns + .into_iter() + .map(|format| import_format(format)) + .collect(); + let cells = formats + .cells + .into_iter() + .map(|formats| import_formats(formats)) + .collect(); + TableFormats { + table, + columns, + cells, + } +} + pub(crate) fn import_data_table_builder( data_tables: Vec<(current::PosSchema, current::DataTableSchema)>, ) -> Result> { @@ -202,6 +241,7 @@ pub(crate) fn import_data_table_builder( }), display_buffer: data_table.display_buffer, alternating_colors: data_table.alternating_colors, + formats: import_data_table_formats(data_table.formats), }; new_data_tables.insert(Pos { x: pos.x, y: pos.y }, data_table); @@ -326,6 +366,34 @@ pub(crate) fn export_code_run(code_run: CodeRun) -> current::CodeRunSchema { } } +pub(crate) fn export_formats( + formats: ColumnData>, +) -> HashMap> { + formats + .into_blocks() + .filter_map(|block| { + let len = block.len() as u32; + if let Some(format) = export_format(block.content.value) { + Some((block.y, current::ColumnRepeatSchema { value: format, len })) + } else { + None + } + }) + .collect() +} + +pub(crate) fn export_data_table_formats(formats: TableFormats) -> current::TableFormatsSchema { + current::TableFormatsSchema { + table: formats.table.and_then(export_format), + columns: formats + .columns + .into_iter() + .filter_map(export_format) + .collect(), + cells: formats.cells.into_iter().map(export_formats).collect(), + } +} + pub(crate) fn export_data_tables( data_tables: IndexMap, ) -> Vec<(current::PosSchema, current::DataTableSchema)> { @@ -404,9 +472,30 @@ pub(crate) fn export_data_tables( spill_error: data_table.spill_error, value, alternating_colors: data_table.alternating_colors, + formats: export_data_table_formats(data_table.formats), }; (current::PosSchema::from(pos), data_table) }) .collect() } + +#[cfg(test)] +mod tests { + use serial_test::parallel; + + use super::*; + + #[test] + #[parallel] + fn test_empty_table_formats_serialized() { + let formats = TableFormats { + table: None, + columns: vec![], + cells: vec![], + }; + let serialized = export_data_table_formats(formats.clone()); + let import = import_data_table_formats(serialized); + assert_eq!(import, formats); + } +} diff --git a/quadratic-core/src/grid/file/v1_7/file.rs b/quadratic-core/src/grid/file/v1_7/file.rs index 7e3b506f90..24a5ad7713 100644 --- a/quadratic-core/src/grid/file/v1_7/file.rs +++ b/quadratic-core/src/grid/file/v1_7/file.rs @@ -58,6 +58,7 @@ fn upgrade_code_runs( spill_error: code_run.spill_error, last_modified: code_run.last_modified, alternating_colors: true, + formats: Default::default(), }; Ok((v1_8::PosSchema::from(pos), new_data_table)) }) diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index b479d02f7d..5821a24a41 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::grid::file::v1_7; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -39,9 +41,7 @@ pub type BorderStyleTimestampSchema = v1_7::schema::BorderStyleTimestampSchema; pub type CellBorderLineSchema = v1_7::schema::CellBorderLineSchema; pub type RgbaSchema = v1_7::schema::RgbaSchema; pub type BorderStyleCell = v1_7::schema::BorderStyleCellSchema; - pub type SelectionSchema = v1_7::SelectionSchema; - pub type ValidationSchema = v1_7::ValidationSchema; pub type ValidationStyleSchema = v1_7::ValidationStyleSchema; pub type ValidationMessageSchema = v1_7::ValidationMessageSchema; @@ -119,6 +119,13 @@ pub struct DataTableSortOrderSchema { pub direction: SortDirectionSchema, } +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TableFormatsSchema { + pub table: Option, + pub columns: Vec, + pub cells: Vec>>, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DataTableSchema { pub kind: DataTableKindSchema, @@ -133,6 +140,7 @@ pub struct DataTableSchema { pub spill_error: bool, pub last_modified: Option>, pub alternating_colors: bool, + pub formats: TableFormatsSchema, } impl From for AxisSchema { diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index eae41c0f05..e28ecef3c5 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -20,7 +20,6 @@ pub enum JsRenderCellSpecial { Checkbox, List, TableColumnHeader, - TableAlternatingColor, } #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)] @@ -102,6 +101,7 @@ impl JsRenderCell { value: isize, language: Option, special: Option, + table: bool, ) -> Self { Self { x, @@ -111,6 +111,7 @@ impl JsRenderCell { align: Some(CellAlign::Right), number: Some(JsNumber::default()), special, + wrap: if table { Some(CellWrap::Clip) } else { None }, ..Default::default() } } diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 7f8a647a8f..bf26045d17 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -97,6 +97,17 @@ impl Sheet { None }; let value = Self::value_date_time(value, format.date_time); + // force clipping for cells in tables + let wrap = if from_table { + // we allow wrap or clip in tables, but not overflow + match format.wrap { + Some(CellWrap::Wrap) => Some(CellWrap::Wrap), + _ => Some(CellWrap::Clip), + } + } else { + format.wrap + }; + JsRenderCell { x, y, @@ -104,7 +115,7 @@ impl Sheet { language, align, vertical_align: format.vertical_align, - wrap: format.wrap, + wrap, bold: format.bold, italic: format.italic, text_color: format.text_color, @@ -138,7 +149,11 @@ impl Sheet { }; // force clipping for cells in tables let wrap = if from_table { - Some(CellWrap::Clip) + // we allow wrap or clip in tables, but not overflow + match format.wrap { + Some(CellWrap::Wrap) => Some(CellWrap::Wrap), + _ => Some(CellWrap::Clip), + } } else { format.wrap }; @@ -241,11 +256,7 @@ impl Sheet { let special = if y == code_rect.min.y && data_table.show_header { Some(JsRenderCellSpecial::TableColumnHeader) } else { - if (y - code_rect.min.y) % 2 == 0 { - Some(JsRenderCellSpecial::TableAlternatingColor) - } else { - None - } + None }; cells.push( self.get_render_cell(x, y, column, &value, language, special, true), @@ -1042,7 +1053,7 @@ mod tests { language: Some(CodeCellLanguage::Formula), align: Some(CellAlign::Right), number: Some(JsNumber::default()), - special: Some(JsRenderCellSpecial::TableAlternatingColor), + wrap: Some(CellWrap::Clip), ..Default::default() }] ); @@ -1219,21 +1230,24 @@ mod tests { y: 0, value: "true".to_string(), language: Some(CodeCellLanguage::Formula), - special: Some(JsRenderCellSpecial::TableAlternatingColor), + wrap: Some(CellWrap::Clip), + special: Some(JsRenderCellSpecial::Logical), ..Default::default() }, JsRenderCell { x: 1, y: 0, value: "false".to_string(), - special: Some(JsRenderCellSpecial::TableAlternatingColor), + wrap: Some(CellWrap::Clip), + special: Some(JsRenderCellSpecial::Logical), ..Default::default() }, JsRenderCell { x: 2, y: 0, value: "true".to_string(), - special: Some(JsRenderCellSpecial::TableAlternatingColor), + wrap: Some(CellWrap::Clip), + special: Some(JsRenderCellSpecial::Logical), ..Default::default() }, ]; From 101ab4cd7e4cf1cdaf2d222f53b9699d9e8b8642 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 1 Nov 2024 04:26:53 -0700 Subject: [PATCH 186/373] WIP formats --- .../src/grid/data_table/table_formats.rs | 85 ++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/quadratic-core/src/grid/data_table/table_formats.rs b/quadratic-core/src/grid/data_table/table_formats.rs index 180d1ef4f8..737c4ed488 100644 --- a/quadratic-core/src/grid/data_table/table_formats.rs +++ b/quadratic-core/src/grid/data_table/table_formats.rs @@ -3,7 +3,13 @@ //! - Columns //! - Table -use crate::grid::{block::SameValue, formats::format::Format, ColumnData}; +use std::collections::HashMap; + +use crate::grid::{ + block::SameValue, + formats::{format::Format, format_update::FormatUpdate}, + ColumnData, +}; use serde::{Deserialize, Serialize}; #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -11,9 +17,82 @@ pub struct TableFormats { pub table: Option, // Indexed by column index. - pub columns: Vec, + pub columns: HashMap, // Indexed by column index and then via RunLengthEncoding. // Note: index is unsorted index. - pub cells: Vec>>, + pub cells: HashMap>>, +} + +impl TableFormats { + /// Returns the format for the given column and row in a table. + pub fn format(&self, column_index: usize, unsorted_row_index: i64) -> Option { + let cell = self + .cells + .get(&column_index) + .and_then(|value| value.get(unsorted_row_index)); + let column = self.columns.get(&column_index); + let format = Format::combine(cell.as_ref(), column, None, self.table.as_ref()); + if format.is_default() { + None + } else { + Some(format) + } + } + + /// Sets the format for the given column and row in a table. + pub fn set_format_cell( + &mut self, + column_index: usize, + unsorted_row_index: i64, + format: FormatUpdate, + ) -> Option { + let column = self + .cells + .entry(column_index) + .or_insert_with(ColumnData::default); + + if let Some(cell) = column.get(unsorted_row_index) { + let replace = cell.merge_update_into(&format); + column.set(unsorted_row_index, Some(replace.to_replace())); + Some(replace) + } else { + None + } + // .get(unsorted_row_index) + // .unwrap_or_default() + // .merge_update_into(&format); + // self.cells + // .entry(column_index) + // .or_insert_with(ColumnData::default) + // .set(unsorted_row_index, Some(new_format.to_replace())) + // .map(|f| FormatUpdate::from(f)) + } + + /// Sets the format for the given column. + pub fn set_format_column( + &mut self, + column_index: usize, + format: Option, + ) -> Option { + self.columns + .insert(column_index, format) + .map(|f| FormatUpdate::from(f)) + } } + +#[cfg(test)] +mod tests { + use serial_test::parallel; + + use super::*; + + #[test] + #[parallel] + fn test_format() { + let table_formats = TableFormats::default(); + assert_eq!(table_formats.format(0, 0), None); + } +} + +// need to redo serialization for formats From 5afa0a499028e49b2a26f8fcec2eb767f9745655 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 1 Nov 2024 05:46:40 -0700 Subject: [PATCH 187/373] fix serialization --- .../src/grid/data_table/table_formats.rs | 82 +++++++++---------- .../src/grid/file/serialize/data_table.rs | 22 +++-- quadratic-core/src/grid/file/v1_8/schema.rs | 4 +- 3 files changed, 53 insertions(+), 55 deletions(-) diff --git a/quadratic-core/src/grid/data_table/table_formats.rs b/quadratic-core/src/grid/data_table/table_formats.rs index 737c4ed488..0052693ff7 100644 --- a/quadratic-core/src/grid/data_table/table_formats.rs +++ b/quadratic-core/src/grid/data_table/table_formats.rs @@ -5,11 +5,7 @@ use std::collections::HashMap; -use crate::grid::{ - block::SameValue, - formats::{format::Format, format_update::FormatUpdate}, - ColumnData, -}; +use crate::grid::{block::SameValue, formats::format::Format, ColumnData}; use serde::{Deserialize, Serialize}; #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -40,45 +36,45 @@ impl TableFormats { } } - /// Sets the format for the given column and row in a table. - pub fn set_format_cell( - &mut self, - column_index: usize, - unsorted_row_index: i64, - format: FormatUpdate, - ) -> Option { - let column = self - .cells - .entry(column_index) - .or_insert_with(ColumnData::default); + // /// Sets the format for the given column and row in a table. + // pub fn set_format_cell( + // &mut self, + // column_index: usize, + // unsorted_row_index: i64, + // format: FormatUpdate, + // ) -> Option { + // let column = self + // .cells + // .entry(column_index) + // .or_insert_with(ColumnData::default); - if let Some(cell) = column.get(unsorted_row_index) { - let replace = cell.merge_update_into(&format); - column.set(unsorted_row_index, Some(replace.to_replace())); - Some(replace) - } else { - None - } - // .get(unsorted_row_index) - // .unwrap_or_default() - // .merge_update_into(&format); - // self.cells - // .entry(column_index) - // .or_insert_with(ColumnData::default) - // .set(unsorted_row_index, Some(new_format.to_replace())) - // .map(|f| FormatUpdate::from(f)) - } + // if let Some(cell) = column.get(unsorted_row_index) { + // let replace = cell.merge_update_into(&format); + // column.set(unsorted_row_index, Some(replace.to_replace())); + // Some(replace) + // } else { + // None + // } + // // .get(unsorted_row_index) + // // .unwrap_or_default() + // // .merge_update_into(&format); + // // self.cells + // // .entry(column_index) + // // .or_insert_with(ColumnData::default) + // // .set(unsorted_row_index, Some(new_format.to_replace())) + // // .map(|f| FormatUpdate::from(f)) + // } - /// Sets the format for the given column. - pub fn set_format_column( - &mut self, - column_index: usize, - format: Option, - ) -> Option { - self.columns - .insert(column_index, format) - .map(|f| FormatUpdate::from(f)) - } + // /// Sets the format for the given column. + // pub fn set_format_column( + // &mut self, + // column_index: usize, + // format: Option, + // ) -> Option { + // self.columns + // .insert(column_index, format) + // .map(|f| FormatUpdate::from(f)) + // } } #[cfg(test)] @@ -94,5 +90,3 @@ mod tests { assert_eq!(table_formats.format(0, 0), None); } } - -// need to redo serialization for formats diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index cfa51e5a3d..6248101f91 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -155,16 +155,16 @@ pub(crate) fn import_formats( } fn import_data_table_formats(formats: current::TableFormatsSchema) -> TableFormats { - let table = formats.table.map(|format| import_format(format)); - let columns = formats + let table = formats.table.map(import_format); + let columns: HashMap = formats .columns .into_iter() - .map(|format| import_format(format)) + .map(|(key, format)| (key as usize, import_format(format))) .collect(); - let cells = formats + let cells: HashMap>> = formats .cells .into_iter() - .map(|formats| import_formats(formats)) + .map(|(key, formats)| (key as usize, import_formats(formats))) .collect(); TableFormats { table, @@ -388,9 +388,13 @@ pub(crate) fn export_data_table_formats(formats: TableFormats) -> current::Table columns: formats .columns .into_iter() - .filter_map(export_format) + .filter_map(|(key, format)| export_format(format).map(|f| (key as i64, f))) + .collect(), + cells: formats + .cells + .into_iter() + .map(|(key, formats)| (key as i64, export_formats(formats))) .collect(), - cells: formats.cells.into_iter().map(export_formats).collect(), } } @@ -491,8 +495,8 @@ mod tests { fn test_empty_table_formats_serialized() { let formats = TableFormats { table: None, - columns: vec![], - cells: vec![], + columns: HashMap::new(), + cells: HashMap::new(), }; let serialized = export_data_table_formats(formats.clone()); let import = import_data_table_formats(serialized); diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index 5821a24a41..75c300b657 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -122,8 +122,8 @@ pub struct DataTableSortOrderSchema { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TableFormatsSchema { pub table: Option, - pub columns: Vec, - pub cells: Vec>>, + pub columns: HashMap, + pub cells: HashMap>>, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] From fd3b9a4fee66d45b000c62e85943b23e54748602 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 1 Nov 2024 10:23:23 -0700 Subject: [PATCH 188/373] improve format combine --- .../src/grid/data_table/table_formats.rs | 108 +++++++++++------- quadratic-core/src/grid/formats/format.rs | 41 +++---- quadratic-core/src/grid/sheet.rs | 10 +- .../src/grid/sheet/formats/format_cell.rs | 6 +- quadratic-core/src/grid/sheet/rendering.rs | 19 ++- 5 files changed, 101 insertions(+), 83 deletions(-) diff --git a/quadratic-core/src/grid/data_table/table_formats.rs b/quadratic-core/src/grid/data_table/table_formats.rs index 0052693ff7..c3ad38b5c0 100644 --- a/quadratic-core/src/grid/data_table/table_formats.rs +++ b/quadratic-core/src/grid/data_table/table_formats.rs @@ -5,7 +5,11 @@ use std::collections::HashMap; -use crate::grid::{block::SameValue, formats::format::Format, ColumnData}; +use crate::grid::{ + block::SameValue, + formats::{format::Format, format_update::FormatUpdate}, + ColumnData, +}; use serde::{Deserialize, Serialize}; #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -28,7 +32,7 @@ impl TableFormats { .get(&column_index) .and_then(|value| value.get(unsorted_row_index)); let column = self.columns.get(&column_index); - let format = Format::combine(cell.as_ref(), column, None, self.table.as_ref()); + let format = Format::combine(vec![self.table.as_ref(), column, cell.as_ref()]); if format.is_default() { None } else { @@ -36,45 +40,61 @@ impl TableFormats { } } - // /// Sets the format for the given column and row in a table. - // pub fn set_format_cell( - // &mut self, - // column_index: usize, - // unsorted_row_index: i64, - // format: FormatUpdate, - // ) -> Option { - // let column = self - // .cells - // .entry(column_index) - // .or_insert_with(ColumnData::default); + /// Sets the format for the given column and row in a table. Returns the + /// undo for the change. + pub fn set_format_cell( + &mut self, + column_index: usize, + unsorted_row_index: i64, + format: FormatUpdate, + ) -> Option { + let column = self + .cells + .entry(column_index) + .or_insert_with(ColumnData::default); - // if let Some(cell) = column.get(unsorted_row_index) { - // let replace = cell.merge_update_into(&format); - // column.set(unsorted_row_index, Some(replace.to_replace())); - // Some(replace) - // } else { - // None - // } - // // .get(unsorted_row_index) - // // .unwrap_or_default() - // // .merge_update_into(&format); - // // self.cells - // // .entry(column_index) - // // .or_insert_with(ColumnData::default) - // // .set(unsorted_row_index, Some(new_format.to_replace())) - // // .map(|f| FormatUpdate::from(f)) - // } + if let Some(mut cell) = column.get(unsorted_row_index) { + let replace = cell.merge_update_into(&format); + column.set(unsorted_row_index, Some(cell)); + Some(replace) + } else { + None + } + } - // /// Sets the format for the given column. - // pub fn set_format_column( - // &mut self, - // column_index: usize, - // format: Option, - // ) -> Option { - // self.columns - // .insert(column_index, format) - // .map(|f| FormatUpdate::from(f)) - // } + /// Sets the format for the given column. Returns the undo for the change. + pub fn set_format_column( + &mut self, + column_index: usize, + format: FormatUpdate, + ) -> Option { + let column = self + .columns + .entry(column_index) + .or_insert_with(Format::default); + let undo = column.merge_update_into(&format); + if undo.is_default() { + None + } else { + Some(undo) + } + } + + /// Sets the table format. Returns the undo for the change. + pub fn set_format_table(&mut self, format: FormatUpdate) -> Option { + let mut table = self.table.clone().unwrap_or_default(); + let replace = table.merge_update_into(&format); + if table.is_default() { + self.table = None; + } else { + self.table = Some(table); + } + if !replace.is_default() { + Some(replace) + } else { + None + } + } } #[cfg(test)] @@ -89,4 +109,14 @@ mod tests { let table_formats = TableFormats::default(); assert_eq!(table_formats.format(0, 0), None); } + + #[test] + #[parallel] + fn test_set_format_cell() { + let mut table_formats = TableFormats::default(); + assert_eq!( + table_formats.set_format_cell(0, 0, FormatUpdate::default()), + None + ); + } } diff --git a/quadratic-core/src/grid/formats/format.rs b/quadratic-core/src/grid/formats/format.rs index 1f79248bc4..2d8771d618 100644 --- a/quadratic-core/src/grid/formats/format.rs +++ b/quadratic-core/src/grid/formats/format.rs @@ -184,27 +184,16 @@ impl Format { } } - /// Combines formatting from a cell, column, or, and sheet (in that order) - pub fn combine( - cell: Option<&Format>, - column: Option<&Format>, - row: Option<&Format>, - sheet: Option<&Format>, - ) -> Format { - let mut format = Format::default(); - if let Some(sheet) = sheet { - format.merge_update_into(&sheet.into()); - } - if let Some(row) = row { - format.merge_update_into(&row.into()); - } - if let Some(column) = column { - format.merge_update_into(&column.into()); - } - if let Some(cell) = cell { - format.merge_update_into(&cell.into()); + /// Combines formatting from least significant to most significant (ie, + /// sheet, row, column, cell, table, table-column, table-cell) + pub fn combine(formats: Vec>) -> Format { + let mut combined_format = Format::default(); + for format in formats { + if let Some(format) = format { + combined_format.merge_update_into(&format.into()); + } } - format + combined_format } /// Turns a Format into a FormatUpdate, with None set to Some(None) to @@ -525,13 +514,13 @@ mod test { #[test] #[parallel] - fn combine() { + fn test_combine() { let cell = Format { bold: Some(false), ..Default::default() }; assert_eq!( - Format::combine(Some(&cell), None, None, None), + Format::combine(vec![Some(&cell)]), Format { bold: Some(false), ..Default::default() @@ -552,28 +541,28 @@ mod test { }; let cell = Format::default(); assert_eq!( - Format::combine(Some(&cell), Some(&column), Some(&row), Some(&sheet)), + Format::combine(vec![Some(&sheet), Some(&row), Some(&column), Some(&cell)]), Format { align: Some(CellAlign::Right), ..Default::default() } ); assert_eq!( - Format::combine(None, Some(&column), Some(&row), Some(&sheet)), + Format::combine(vec![Some(&sheet), Some(&row), Some(&column)]), Format { align: Some(CellAlign::Right), ..Default::default() } ); assert_eq!( - Format::combine(None, None, None, Some(&sheet)), + Format::combine(vec![Some(&sheet)]), Format { align: Some(CellAlign::Center), ..Default::default() } ); - assert_eq!(Format::combine(None, None, None, None), Format::default()); + assert_eq!(Format::combine(vec![]), Format::default()); } #[test] diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 45a165289f..c768924d4e 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -322,12 +322,12 @@ impl Sheet { _ => None, }); let format = if include_sheet_info { - Format::combine( - cell.as_ref(), - self.try_format_column(pos.x).as_ref(), - self.try_format_row(pos.y).as_ref(), + Format::combine(vec![ self.format_all.as_ref(), - ) + self.try_format_row(pos.y).as_ref(), + self.try_format_column(pos.x).as_ref(), + cell.as_ref(), + ]) } else { cell.unwrap_or_default() }; diff --git a/quadratic-core/src/grid/sheet/formats/format_cell.rs b/quadratic-core/src/grid/sheet/formats/format_cell.rs index 38c804a00b..41ea0e8777 100644 --- a/quadratic-core/src/grid/sheet/formats/format_cell.rs +++ b/quadratic-core/src/grid/sheet/formats/format_cell.rs @@ -17,7 +17,7 @@ impl Sheet { let column = self.try_format_column(x); let row = self.try_format_row(y); let sheet = self.format_all.as_ref(); - let format = Format::combine(None, column.as_ref(), row.as_ref(), sheet); + let format = Format::combine(vec![sheet, row.as_ref(), column.as_ref()]); if format.numeric_decimals.is_some() { format.numeric_decimals } else { @@ -48,7 +48,7 @@ impl Sheet { let column = self.try_format_column(x); let row = self.try_format_row(y); let sheet = self.format_all.as_ref(); - Format::combine(format.as_ref(), column.as_ref(), row.as_ref(), sheet) + Format::combine(vec![sheet, row.as_ref(), column.as_ref(), format.as_ref()]) } else { format.unwrap_or_default() } @@ -286,7 +286,7 @@ mod tests { #[test] #[parallel] - fn decimal_places() { + fn test_decimal_places() { let mut sheet = Sheet::test(); assert_eq!(sheet.decimal_places(0, 0), None); diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index bf26045d17..43aa297bce 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -84,12 +84,11 @@ impl Sheet { match column { None => { - let format = Format::combine( - None, - self.try_format_column(x).as_ref(), - self.try_format_row(y).as_ref(), + let format = Format::combine(vec![ self.format_all.as_ref(), - ); + self.try_format_row(y).as_ref(), + self.try_format_column(x).as_ref(), + ]); let align = format.align.or(align); let number: Option = if matches!(value, CellValue::Number(_)) { Some((&format).into()) @@ -127,12 +126,12 @@ impl Sheet { } Some(column) => { let format_cell = column.format(y); - let mut format = Format::combine( - format_cell.as_ref(), - self.try_format_column(x).as_ref(), - self.try_format_row(y).as_ref(), + let mut format = Format::combine(vec![ self.format_all.as_ref(), - ); + self.try_format_row(y).as_ref(), + self.try_format_column(x).as_ref(), + format_cell.as_ref(), + ]); let mut number: Option = None; let value = match &value { CellValue::Number(_) => { From 3a16f02c4973b8122ed2a64fb9120caa488a1858 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 3 Nov 2024 06:48:05 -0800 Subject: [PATCH 189/373] improve context menu positioning --- .../gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx index 4463a549cd..f85faa6e45 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase.tsx @@ -44,6 +44,7 @@ export const ContextMenuBase = ({ }; }, [onClose]); + const bounds = pixiApp.viewport.getVisibleBounds(); const left = contextMenu.world?.x ?? 0; const top = contextMenu.world?.y ?? 0; @@ -56,7 +57,6 @@ export const ContextMenuBase = ({ if (!dropdownOpen) onClose(); }} > - {/* Radix wants the trigger for positioning the content, so we hide it visibly */} e.preventDefault()} + collisionBoundary={document.querySelector('.grid-container')} + collisionPadding={8} + hideWhenDetached={false} + avoidCollisions={true} > {children({ contextMenu })} From 0f522ae53b31510bc13770ef782d4f387fd00a0e Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 4 Nov 2024 08:53:42 -0700 Subject: [PATCH 190/373] Finish DataTableMeta operations in core, remove UpdateDataTableName --- .../active_transactions/transaction_name.rs | 2 +- .../execute_operation/execute_data_table.rs | 64 ++++--------------- .../execution/execute_operation/mod.rs | 3 - .../src/controller/operations/data_table.rs | 18 ++++-- .../src/controller/operations/operation.rs | 11 ---- .../src/controller/user_actions/data_table.rs | 18 ++++-- quadratic-core/src/grid/js_types.rs | 10 +++ .../wasm_bindings/controller/data_table.rs | 31 +++++---- 8 files changed, 69 insertions(+), 88 deletions(-) diff --git a/quadratic-core/src/controller/active_transactions/transaction_name.rs b/quadratic-core/src/controller/active_transactions/transaction_name.rs index 846ae9d875..7afb70bcad 100644 --- a/quadratic-core/src/controller/active_transactions/transaction_name.rs +++ b/quadratic-core/src/controller/active_transactions/transaction_name.rs @@ -19,7 +19,7 @@ pub enum TransactionName { FlattenDataTable, SwitchDataTableKind, GridToDataTable, - UpdateDataTableName, + DataTableMeta, DataTableFirstRowAsHeader, Import, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 0911283466..d30fee43af 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -321,50 +321,6 @@ impl GridController { bail!("Expected Operation::GridToDataTable in execute_grid_to_data_table"); } - pub(super) fn execute_update_data_table_name( - &mut self, - transaction: &mut PendingTransaction, - op: Operation, - ) -> Result<()> { - if let Operation::UpdateDataTableName { - sheet_pos, - ref name, - } = op - { - let sheet_id = sheet_pos.sheet_id; - let name = self.grid.unique_data_table_name(name, false); - let sheet = self.try_sheet_mut_result(sheet_id)?; - let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; - let data_table = sheet.data_table_mut(data_table_pos)?; - - let old_name = data_table.name.to_owned(); - data_table.name = name; - let data_table_rect = data_table - .output_rect(sheet_pos.into(), true) - .to_sheet_rect(sheet_id); - - self.send_to_wasm(transaction, &data_table_rect)?; - transaction.add_code_cell(sheet_id, data_table_pos.into()); - - let forward_operations = vec![op]; - let reverse_operations = vec![Operation::UpdateDataTableName { - sheet_pos, - name: old_name, - }]; - - self.data_table_operations( - transaction, - &data_table_rect, - forward_operations, - reverse_operations, - ); - - return Ok(()); - }; - - bail!("Expected Operation::UpdateDataTableName in execute_update_data_table_name"); - } - pub(super) fn execute_data_table_meta( &mut self, transaction: &mut PendingTransaction, @@ -794,12 +750,14 @@ mod tests { println!("Initial data table name: {}", &data_table.name); let sheet_pos = SheetPos::from((pos, sheet_id)); - let op = Operation::UpdateDataTableName { + let op = Operation::DataTableMeta { sheet_pos, - name: updated_name.into(), + name: Some(updated_name.into()), + alternating_colors: None, + columns: None, }; let mut transaction = PendingTransaction::default(); - gc.execute_update_data_table_name(&mut transaction, op.clone()) + gc.execute_data_table_meta(&mut transaction, op.clone()) .unwrap(); let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); @@ -822,19 +780,19 @@ mod tests { // ensure names are unique let mut transaction = PendingTransaction::default(); - gc.execute_update_data_table_name(&mut transaction, op) - .unwrap(); + gc.execute_data_table_meta(&mut transaction, op).unwrap(); let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); assert_eq!(&data_table.name, "My Table1"); // ensure numbers aren't added for unique names - let op = Operation::UpdateDataTableName { + let op = Operation::DataTableMeta { sheet_pos, - name: "ABC".into(), + name: Some("ABC".into()), + alternating_colors: None, + columns: None, }; let mut transaction = PendingTransaction::default(); - gc.execute_update_data_table_name(&mut transaction, op) - .unwrap(); + gc.execute_data_table_meta(&mut transaction, op).unwrap(); let data_table = gc.sheet_mut(sheet_id).data_table_mut(pos).unwrap(); assert_eq!(&data_table.name, "ABC"); } diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 69e1c6e2b1..1a12c9e926 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -44,9 +44,6 @@ impl GridController { Operation::GridToDataTable { .. } => Self::handle_execution_operation_result( self.execute_grid_to_data_table(transaction, op), ), - Operation::UpdateDataTableName { .. } => Self::handle_execution_operation_result( - self.execute_update_data_table_name(transaction, op), - ), Operation::DataTableMeta { .. } => Self::handle_execution_operation_result( self.execute_data_table_meta(transaction, op), ), diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index 08b25ffb19..2a6a9293ad 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -2,7 +2,10 @@ use super::operation::Operation; use crate::{ cellvalue::Import, controller::GridController, - grid::{data_table::sort::DataTableSort, DataTableKind}, + grid::{ + data_table::{column::DataTableColumn, sort::DataTableSort}, + DataTableKind, + }, CellValue, SheetPos, SheetRect, }; @@ -43,13 +46,20 @@ impl GridController { vec![Operation::GridToDataTable { sheet_rect }] } - pub fn update_data_table_name_operations( + pub fn data_table_meta_operations( &self, sheet_pos: SheetPos, - name: String, + name: Option, + alternating_colors: Option, + columns: Option>, _cursor: Option, ) -> Vec { - vec![Operation::UpdateDataTableName { sheet_pos, name }] + vec![Operation::DataTableMeta { + sheet_pos, + name, + alternating_colors, + columns, + }] } pub fn sort_data_table_operations( diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 487b98e9ff..5287c84603 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -56,10 +56,6 @@ pub enum Operation { GridToDataTable { sheet_rect: SheetRect, }, - UpdateDataTableName { - sheet_pos: SheetPos, - name: String, - }, DataTableMeta { sheet_pos: SheetPos, name: Option, @@ -243,13 +239,6 @@ impl fmt::Display for Operation { Operation::GridToDataTable { sheet_rect } => { write!(fmt, "GridToDataTable {{ sheet_rect: {} }}", sheet_rect) } - Operation::UpdateDataTableName { sheet_pos, name } => { - write!( - fmt, - "UpdateDataTableName {{ sheet_pos: {} name: {} }}", - sheet_pos, name - ) - } Operation::DataTableMeta { sheet_pos, name, diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index fa0ce4c457..07d195eb28 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -1,6 +1,6 @@ use crate::{ controller::{active_transactions::transaction_name::TransactionName, GridController}, - grid::sort::DataTableSort, + grid::{data_table::column::DataTableColumn, sort::DataTableSort}, Pos, SheetPos, SheetRect, }; @@ -47,14 +47,22 @@ impl GridController { self.start_user_transaction(ops, cursor, TransactionName::GridToDataTable); } - pub fn update_data_table_name( + pub fn data_table_meta( &mut self, sheet_pos: SheetPos, - name: String, + name: Option, + alternating_colors: Option, + columns: Option>, cursor: Option, ) { - let ops = self.update_data_table_name_operations(sheet_pos, name, cursor.to_owned()); - self.start_user_transaction(ops, cursor, TransactionName::UpdateDataTableName); + let ops = self.data_table_meta_operations( + sheet_pos, + name, + alternating_colors, + columns, + cursor.to_owned(), + ); + self.start_user_transaction(ops, cursor, TransactionName::DataTableMeta); } pub fn sort_data_table( diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index e28ecef3c5..80a53124c7 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -262,6 +262,16 @@ impl From for JsDataTableColumn { } } +impl From for DataTableColumn { + fn from(column: JsDataTableColumn) -> Self { + DataTableColumn { + name: column.name.into(), + display: column.display, + value_index: column.value_index, + } + } +} + #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, TS)] pub struct JsRowHeight { pub row: i64, diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index 09416346ec..fd41cd82b4 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -1,3 +1,4 @@ +use grid::data_table::column::DataTableColumn; use selection::Selection; use sort::DataTableSort; @@ -62,13 +63,9 @@ impl GridController { let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; - let mut sort = None; - - if let Some(sort_js) = sort_js { - sort = Some( - serde_json::from_str::>(&sort_js).map_err(|e| e.to_string())?, - ); - } + let sort = sort_js + .map(|s| serde_json::from_str::>(&s).map_err(|e| e.to_string())) + .transpose()?; self.sort_data_table(pos.to_sheet_pos(sheet_id), sort, cursor); @@ -95,18 +92,30 @@ impl GridController { Ok(()) } /// Update a Data Table's name - #[wasm_bindgen(js_name = "updateDataTableName")] - pub fn js_update_data_table_name( + #[wasm_bindgen(js_name = "dataTableMeta")] + pub fn js_data_table_meta( &mut self, sheet_id: String, pos: String, - name: String, + name: Option, + alternating_colors: Option, + columns_js: Option, cursor: Option, ) -> Result<(), JsValue> { let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; - self.update_data_table_name(pos.to_sheet_pos(sheet_id), name, cursor); + let columns = columns_js + .map(|c| serde_json::from_str::>(&c).map_err(|e| e.to_string())) + .transpose()?; + + self.data_table_meta( + pos.to_sheet_pos(sheet_id), + name, + alternating_colors, + columns, + cursor, + ); Ok(()) } From 2c6b8a00571638288372a3225af6d7998ed0f062 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 4 Nov 2024 09:05:53 -0700 Subject: [PATCH 191/373] Replace UpdateDataTableName references in client with DataTableMeta --- .../src/app/actions/dataTableSpec.ts | 6 +++--- .../HTMLGrid/contextMenus/TableRename.tsx | 4 +++- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../quadraticCore/coreClientMessages.ts | 14 ++++++++++---- .../quadraticCore/quadraticCore.ts | 14 +++++++++++--- .../web-workers/quadraticCore/worker/core.ts | 19 +++++++++++++++++-- .../quadraticCore/worker/coreClient.ts | 12 ++++++++++-- 7 files changed, 55 insertions(+), 16 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 388fb7afaa..b4466a8ca4 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -65,7 +65,7 @@ export const dataTableSpec: DataTableSpec = { [Action.FlattenTable]: { label: 'Flatten to sheet', Icon: FlattenTableIcon, - run: async () => { + run: () => { const table = getTable(); if (table) { quadraticCore.flattenDataTable(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); @@ -75,7 +75,7 @@ export const dataTableSpec: DataTableSpec = { [Action.GridToDataTable]: { label: 'Convert to table', Icon: TableConvertIcon, - run: async () => { + run: () => { quadraticCore.gridToDataTable(sheets.getRustSelection(), sheets.getCursorPosition()); }, }, @@ -113,7 +113,7 @@ export const dataTableSpec: DataTableSpec = { [Action.ToggleHeaderTable]: { label: 'Show column headings', checkbox: isHeadingShowing, - run: async () => { + run: () => { // const table = getTable(); // quadraticCore.dataTableShowHeadings(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); }, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx index 9db863eb3e..0a41533e58 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx @@ -34,11 +34,13 @@ export const TableRename = () => { styles={{ fontSize: TABLE_NAME_FONT_SIZE, paddingLeft: TABLE_NAME_PADDING[0] }} onSave={(value: string) => { if (contextMenu.table && pixiApp.cellsSheets.current) { - quadraticCore.updateDataTableName( + quadraticCore.dataTableMeta( pixiApp.cellsSheets.current?.sheetId, contextMenu.table.x, contextMenu.table.y, value, + undefined, + undefined, '' ); } diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index cacf2105ed..a5b07a863c 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -69,7 +69,7 @@ export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; export type TextMatch = { "Exactly": TextCase } | { "Contains": TextCase } | { "NotContains": TextCase } | { "TextLength": { min: number | null, max: number | null, } }; -export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "SwitchDataTableKind" | "GridToDataTable" | "UpdateDataTableName" | "DataTableFirstRowAsHeader" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; +export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "SwitchDataTableKind" | "GridToDataTable" | "DataTableMeta" | "DataTableFirstRowAsHeader" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; export interface TransientResize { row: bigint | null, column: bigint | null, old_size: number, new_size: number, } export interface Validation { id: string, selection: Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } export interface ValidationDateTime { ignore_blank: boolean, require_date: boolean, require_time: boolean, prohibit_date: boolean, prohibit_time: boolean, ranges: Array, } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index 608073e046..41a534814b 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -1038,12 +1038,18 @@ export interface ClientCoreGridToDataTable { cursor: string; } -export interface ClientCoreUpdateDataTableName { - type: 'clientCoreUpdateDataTableName'; +export interface ClientCoreDataTableMeta { + type: 'clientCoreDataTableMeta'; sheetId: string; x: number; y: number; - name: string; + name?: string; + alternatingColors?: boolean; + columns?: { + name: string; + display: boolean; + valueIndex: number; + }[]; cursor: string; } @@ -1148,7 +1154,7 @@ export type ClientCoreMessage = | ClientCoreFlattenDataTable | ClientCoreCodeDataTableToDataTable | ClientCoreGridToDataTable - | ClientCoreUpdateDataTableName + | ClientCoreDataTableMeta | ClientCoreSortDataTable | ClientCoreDataTableFirstRowAsHeader; diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index 2858b5713f..27ce7d0a90 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -1209,14 +1209,22 @@ class QuadraticCore { }); } - updateDataTableName(sheetId: string, x: number, y: number, name: string, cursor: string) { + dataTableMeta( + sheetId: string, + x: number, + y: number, + name?: string, + alternatingColors?: boolean, + columns?: { name: string; display: boolean; valueIndex: number }[], + cursor?: string + ) { this.send({ - type: 'clientCoreUpdateDataTableName', + type: 'clientCoreDataTableMeta', sheetId, x, y, name, - cursor, + cursor: cursor || '', }); } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 1f29a56f02..782e1c680b 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1130,9 +1130,24 @@ class Core { this.gridController.gridToDataTable(JSON.stringify(selection, bigIntReplacer), cursor); } - updateDataTableName(sheetId: string, x: number, y: number, name: string, cursor: string) { + dataTableMeta( + sheetId: string, + x: number, + y: number, + name?: string, + alternatingColors?: boolean, + columns?: { name: string; display: boolean; valueIndex: number }[], + cursor?: string + ) { if (!this.gridController) throw new Error('Expected gridController to be defined'); - this.gridController.updateDataTableName(sheetId, posToPos(x, y), name, cursor); + this.gridController.dataTableMeta( + sheetId, + posToPos(x, y), + name, + alternatingColors, + JSON.stringify(columns), + cursor + ); } sortDataTable( diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index d554769734..599973d115 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -593,8 +593,16 @@ class CoreClient { core.gridToDataTable(e.data.selection, e.data.cursor); return; - case 'clientCoreUpdateDataTableName': - core.updateDataTableName(e.data.sheetId, e.data.x, e.data.y, e.data.name, e.data.cursor); + case 'clientCoreDataTableMeta': + core.dataTableMeta( + e.data.sheetId, + e.data.x, + e.data.y, + e.data.name, + e.data.alternatingColors, + e.data.columns, + e.data.cursor + ); return; case 'clientCoreSortDataTable': From fe8b91658c5cce641c020213a9e65af7e35d9255 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 4 Nov 2024 08:27:18 -0800 Subject: [PATCH 192/373] rework how the cursor moves into charts --- .../app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 63 +++++++--------- .../HTMLGrid/htmlCells/htmlCellsHandler.ts | 9 +++ .../gridGL/cells/cellsImages/CellsImage.ts | 39 ++++++++-- .../gridGL/cells/cellsImages/CellsImages.ts | 17 ++++- .../src/app/gridGL/cells/tables/Table.ts | 22 ++++-- .../src/app/gridGL/cells/tables/Tables.ts | 18 ++++- .../interaction/keyboard/keyboardPosition.ts | 73 +++++++++++++++++++ 7 files changed, 183 insertions(+), 58 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index 75147da73f..9bd0cae43d 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -1,8 +1,9 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; +import { intersects } from '@/app/gridGL/helpers/intersects'; import { JsHtmlOutput } from '@/app/quadratic-core-types'; import { CELL_HEIGHT, CELL_WIDTH } from '@/shared/constants/gridConstants'; -import { InteractionEvent } from 'pixi.js'; +import { InteractionEvent, Point, Rectangle } from 'pixi.js'; import { pixiApp } from '../../pixiApp/PixiApp'; import { Wheel } from '../../pixiOverride/Wheel'; import { HtmlCellResizing } from './HtmlCellResizing'; @@ -16,9 +17,12 @@ const DEFAULT_HTML_HEIGHT = '460'; export class HtmlCell { private right: HTMLDivElement; private bottom: HTMLDivElement; - private htmlCell: JsHtmlOutput; private resizing: HtmlCellResizing | undefined; private hoverSide: 'right' | 'bottom' | 'corner' | undefined; + private offset: Point; + + htmlCell: JsHtmlOutput; + gridBounds: Rectangle; sheet: Sheet; @@ -38,6 +42,8 @@ export class HtmlCell { this.div.className = 'html-cell'; this.div.style.boxShadow = 'inset 0 0 0 1px hsl(var(--primary))'; const offset = this.sheet.getCellOffsets(Number(htmlCell.x), Number(htmlCell.y)); + this.offset = new Point(offset.x, offset.y); + this.gridBounds = new Rectangle(Number(htmlCell.x), Number(htmlCell.y), 0, 0); this.div.style.left = `${offset.x}px`; this.div.style.top = `${offset.y}px`; @@ -117,43 +123,8 @@ export class HtmlCell { }, { passive: false } ); - - // move margin to the div holding the iframe to avoid pinch-to-zoom issues at the iframe margins - // const style = window.getComputedStyle(this.iframe.contentWindow.document.body); - // if (style.marginLeft) { - // this.div.style.paddingLeft = style.marginLeft; - // } - // if (style.marginTop) { - // this.div.style.paddingTop = style.marginTop; - // } - // if (style.marginRight) { - // this.div.style.paddingRight = style.marginRight; - // } - // if (style.marginBottom) { - // this.div.style.paddingBottom = style.marginBottom; - // } - this.iframe.contentWindow.document.body.style.margin = ''; - - // this is the automatic size calculation -- replaced for *now* with default width/height - // if (!this.htmlCell.w) { - // this.iframe.width = ( - // this.iframe.contentWindow.document.body.scrollWidth + - // parseInt(style.marginLeft) + - // parseInt(style.marginRight) - // ).toString(); - // } else { - // this.iframe.width = this.htmlCell.w; - // } - // if (!this.htmlCell.h) { - // this.iframe.height = ( - // this.iframe.contentWindow.document.body.scrollHeight + - // parseInt(style.marginTop) + - // parseInt(style.marginBottom) - // ).toString(); - // } else { - // this.iframe.height = this.htmlCell.h; - // } + this.calculateGridBounds(); } else { throw new Error('Expected content window to be defined on iframe'); } @@ -169,6 +140,14 @@ export class HtmlCell { this.iframe.srcdoc = htmlCell.html; } this.htmlCell = htmlCell; + this.calculateGridBounds(); + } + + private calculateGridBounds() { + const right = this.sheet.offsets.getXPlacement(this.offset.x + this.div.offsetWidth); + this.gridBounds.width = right.index - this.gridBounds.x; + const bottom = this.sheet.offsets.getYPlacement(this.offset.y + this.div.offsetHeight); + this.gridBounds.height = bottom.index - this.gridBounds.y; } changeSheet(sheetId: string) { @@ -286,7 +265,15 @@ export class HtmlCell { updateOffsets() { const offset = this.sheet.getCellOffsets(this.x, this.y); + this.offset.set(offset.x, offset.y); this.div.style.left = `${offset.x}px`; this.div.style.top = `${offset.y}px`; + this.gridBounds.x = offset.x; + this.gridBounds.y = offset.y; + } + + // checks if the cell contains a grid point + contains(x: number, y: number): boolean { + return intersects.rectanglePoint(this.gridBounds, new Point(x, y)); } } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts index 1959ac2b87..d83d1f83d8 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts @@ -144,6 +144,15 @@ class HTMLCellsHandler { return this.getCells().some((cell) => cell.x === x && cell.y === y && cell.sheet.id === sheets.sheet.id); } + // returns true if the Pos overlaps with the output of an html cell + contains(x: number, y: number): boolean { + return this.getCells().some((cell) => cell.contains(x, y)); + } + + findCodeCell(x: number, y: number): HtmlCell | undefined { + return this.getCells().find((cell) => cell.sheet.id === sheets.sheet.id && cell.contains(x, y)); + } + showActive(codeCell: JsRenderCodeCell, isSelected: boolean) { const cell = this.getCells().find( (cell) => cell.x === codeCell.x && cell.y === codeCell.y && cell.sheet.id === sheets.sheet.id diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts index 9a11a49d25..5a60b0afdc 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts @@ -14,8 +14,7 @@ export class CellsImage extends Container { private background: Graphics; private sprite: Sprite; - column: number; - row: number; + gridBounds: Rectangle; // these are user set values for the image size imageWidth?: number; @@ -28,8 +27,7 @@ export class CellsImage extends Container { constructor(cellsSheet: CellsSheet, message: CoreClientImage) { super(); this.cellsSheet = cellsSheet; - this.column = message.x; - this.row = message.y; + this.gridBounds = new Rectangle(message.x, message.y, 0, 0); this.background = this.addChild(new Graphics()); this.sprite = this.addChild(new Sprite(Texture.EMPTY)); @@ -48,6 +46,13 @@ export class CellsImage extends Container { return this.cellsSheet.sheetId; } + get column(): number { + return this.gridBounds.x; + } + get row(): number { + return this.gridBounds.y; + } + updateMessage(message: CoreClientImage) { if (!message.image) { throw new Error('Expected message.image to be defined in SpriteImage.updateMessage'); @@ -116,23 +121,41 @@ export class CellsImage extends Container { if (!sheet) { throw new Error(`Expected sheet to be defined in CellsImage.resizeImage`); } - sheet.gridOverflowLines.updateImageHtml(this.column, this.row, this.sprite.width, this.sprite.height); - this.cellsSheet.tables.resizeTable(this.column, this.row, this.sprite.width, this.sprite.height); + sheet.gridOverflowLines.updateImageHtml( + this.gridBounds.x, + this.gridBounds.y, + this.sprite.width, + this.sprite.height + ); + this.cellsSheet.tables.resizeTable(this.gridBounds.x, this.gridBounds.y, this.sprite.width, this.sprite.height); if (this.cellsSheet.sheetId === sheets.current) { pixiApp.setViewportDirty(); } + const right = this.sheet.offsets.getXPlacement(this.viewRight.x + IMAGE_BORDER_WIDTH).index; + const bottom = this.sheet.offsets.getYPlacement(this.viewBottom.y + IMAGE_BORDER_WIDTH).index; + this.gridBounds.width = right - this.gridBounds.x + 1; + this.gridBounds.height = bottom - this.gridBounds.y + 1; }; reposition() { - const screen = this.sheet.getCellOffsets(this.column, this.row); + const screen = this.sheet.getCellOffsets(this.gridBounds.x, this.gridBounds.y); this.position.set(screen.x, screen.y); this.resizeImage(); } contains(world: Point): Coordinate | undefined { if (intersects.rectanglePoint(this.viewBounds, world)) { - return { x: this.column, y: this.row }; + return { x: this.gridBounds.x, y: this.gridBounds.y }; } return undefined; } + + isImageCell(x: number, y: number): boolean { + return ( + x >= this.gridBounds.x && + x < this.gridBounds.right && + y >= this.gridBounds.y && + y < this.gridBounds.bottom + ); + } } diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts index 38274a4ab9..e4012c5482 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts @@ -42,7 +42,8 @@ export class CellsImages extends Container { private updateImage = (message: CoreClientImage) => { if (message.sheetId === this.cellsSheet.sheetId) { let sprite = this.children.find( - (sprite) => (sprite as CellsImage).column === message.x && (sprite as CellsImage).row === message.y + (sprite) => + (sprite as CellsImage).gridBounds.x === message.x && (sprite as CellsImage).gridBounds.y === message.y ); if (sprite) { if (message.image) { @@ -74,7 +75,17 @@ export class CellsImages extends Container { } } - isImageCell(x: number, y: number): boolean { - return this.children.some((child) => child.column === x && child.row === y); + // determines whether a column,row is inside the output of an image cell + isImageCell(column: number, row: number): boolean { + return this.children.some((child) => child.isImageCell(column, row)); + } + + // finds the image cell that contains a column,row + findCodeCell(column: number, row: number): CellsImage | undefined { + for (const image of this.children) { + if (image.isImageCell(column, row)) { + return image; + } + } } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 24dec86209..88beb7838b 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -32,6 +32,8 @@ export class Table extends Container { codeCell: JsRenderCodeCell; tableCursor: string | undefined; + imageHtmlGridBounds?: [number, number]; + constructor(sheet: Sheet, codeCell: JsRenderCodeCell) { super(); this.codeCell = codeCell; @@ -107,14 +109,7 @@ export class Table extends Container { intersectsCursor(x: number, y: number) { const rect = new Rectangle(this.codeCell.x, this.codeCell.y, this.codeCell.w - 1, this.codeCell.h - 1); - if ( - intersects.rectanglePoint(rect, { x, y }) || - intersects.rectangleRectangle(rect, this.tableName.tableNameBounds) - ) { - this.showActive(false); - return true; - } - return false; + return intersects.rectanglePoint(rect, { x, y }); } // Checks whether the mouse cursor is hovering over the table or the table name @@ -254,4 +249,15 @@ export class Table extends Container { this.tableBounds.height = height; this.outline.update(); } + + // Checks whether the cell is within the table + contains(cell: Coordinate): boolean { + // first check if we're even in the right x/y range + return ( + cell.x >= this.codeCell.x && + cell.y >= this.codeCell.y && + cell.x < this.codeCell.x + this.codeCell.w && + cell.y < this.codeCell.y + this.codeCell.h + ); + } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 592e3fa4fc..34963e2d62 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -147,6 +147,17 @@ export class Tables extends Container
{ } const cursor = sheets.sheet.cursor.cursorPosition; this.activeTable = this.children.find((table) => table.intersectsCursor(cursor.x, cursor.y)); + if (!this.activeTable) { + const image = pixiApp.cellsSheet().cellsImages.findCodeCell(cursor.x, cursor.y); + const codeCell = + htmlCellsHandler.findCodeCell(cursor.x, cursor.y)?.htmlCell || + (image ? { x: image.gridBounds.x, y: image.gridBounds.y } : undefined); + if (codeCell) { + this.activeTable = this.children.find( + (table) => table.codeCell.x === codeCell.x && table.codeCell.y === codeCell.y + ); + } + } if (this.activeTable) { this.activeTable.showActive(true); } @@ -425,6 +436,11 @@ export class Tables extends Container
{ } isHtmlOrImage(cell: Coordinate): boolean { - return this.htmlOrImage.has(`${cell.x},${cell.y}`); + if (this.htmlOrImage.has(`${cell.x},${cell.y}`)) { + return true; + } + return ( + !!htmlCellsHandler.findCodeCell(cell.x, cell.y) || pixiApp.cellsSheet().cellsImages.isImageCell(cell.x, cell.y) + ); } } diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts index 932740d7ec..762883df41 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts @@ -3,8 +3,10 @@ import { Action } from '@/app/actions/actions'; import { sheets } from '@/app/grid/controller/Sheets'; +import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; import { moveViewport } from '@/app/gridGL/interaction/viewportHelper'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { matchShortcut } from '@/app/helpers/keyboardShortcuts.js'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; @@ -46,6 +48,18 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { if (deltaX === 1) { let x = keyboardX; const y = cursor.keyboardMovePosition.y; + + // adjust the jump position if it is inside an image or html cell + const image = pixiApp.cellsSheet().cellsImages.findCodeCell(x, y); + if (image) { + x = image.gridBounds.x + image.gridBounds.width - 1; + } else { + const html = htmlCellsHandler.findCodeCell(x, y); + if (html) { + x = html.gridBounds.x + html.gridBounds.width; + } + } + // always use the original cursor position to search const yCheck = cursor.cursorPosition.y; // handle case of cell with content @@ -86,6 +100,7 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { if (nextCol === undefined) { nextCol = x < 0 ? 0 : x + 1; } + x = nextCol; if (keyboardX < -1) { x = Math.min(x, -1); @@ -105,6 +120,18 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { } else if (deltaX === -1) { let x = keyboardX; const y = cursor.keyboardMovePosition.y; + + // adjust the jump position if it is inside an image or html cell + const image = pixiApp.cellsSheet().cellsImages.findCodeCell(x, y); + if (image) { + x = image.gridBounds.x - 1; + } else { + const html = htmlCellsHandler.findCodeCell(x, y); + if (html) { + x = html.gridBounds.x - 1; + } + } + // always use the original cursor position to search const yCheck = cursor.cursorPosition.y; // handle case of cell with content @@ -314,6 +341,52 @@ function expandSelection(deltaX: number, deltaY: number) { function moveCursor(deltaX: number, deltaY: number) { const cursor = sheets.sheet.cursor; const newPos = { x: cursor.cursorPosition.x + deltaX, y: cursor.cursorPosition.y + deltaY }; + + // need to adjust the cursor position if it is inside an image cell + const image = pixiApp.cellsSheet().cellsImages.findCodeCell(newPos.x, newPos.y); + if (image) { + if (deltaX === 1) { + if (newPos.x !== image.gridBounds.x) { + newPos.x = image.gridBounds.x + image.gridBounds.width; + } + } else if (deltaX === -1) { + if (newPos.x !== image.gridBounds.x + image.gridBounds.width - 1) { + newPos.x = image.gridBounds.x - 1; + } + } + if (deltaY === 1) { + if (newPos.y !== image.gridBounds.y) { + newPos.y = image.gridBounds.y + image.gridBounds.height; + } + } else if (deltaY === -1) { + if (newPos.y !== image.gridBounds.y + image.gridBounds.height - 1) { + newPos.y = image.gridBounds.y - 1; + } + } + } + + // if the cursor is inside an html cell, move the cursor to the top left of the cell + const html = htmlCellsHandler.findCodeCell(newPos.x, newPos.y); + if (html) { + if (deltaX === 1) { + if (newPos.x !== html.gridBounds.x) { + newPos.x = html.gridBounds.x + html.gridBounds.width + 1; + } + } else if (deltaX === -1) { + if (newPos.x !== html.gridBounds.x + html.gridBounds.width) { + newPos.x = html.gridBounds.x - 1; + } + } + if (deltaY === 1) { + if (newPos.y !== html.gridBounds.y) { + newPos.y = html.gridBounds.y + html.gridBounds.height + 1; + } + } else if (deltaY === -1) { + if (newPos.y !== html.gridBounds.y + html.gridBounds.height) { + newPos.y = html.gridBounds.y - 1; + } + } + } cursor.changePosition({ columnRow: null, multiCursor: null, From 829cf1364daa72c888fdd2fa015d288465dae24b Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 4 Nov 2024 09:49:36 -0700 Subject: [PATCH 193/373] Fix all lints --- .../execute_operation/execute_data_table.rs | 26 +++++++++--------- .../execution/execute_operation/mod.rs | 14 ++++++---- .../src/controller/operations/import.rs | 4 +-- .../src/controller/user_actions/cells.rs | 14 ++++------ .../src/controller/user_actions/import.rs | 6 ++--- .../src/formulas/functions/mathematics.rs | 4 +-- quadratic-core/src/grid/data_table/column.rs | 26 +++++++++--------- .../src/grid/data_table/display_value.rs | 23 +++++++++------- quadratic-core/src/grid/data_table/mod.rs | 8 +++--- quadratic-core/src/grid/data_table/sort.rs | 4 +-- .../src/grid/data_table/table_formats.rs | 4 +-- quadratic-core/src/grid/file/v1_8/schema.rs | 1 + quadratic-core/src/grid/formats/format.rs | 8 +++--- quadratic-core/src/grid/sheet/code.rs | 6 ++--- quadratic-core/src/grid/sheet/data_table.rs | 2 +- quadratic-core/src/grid/sheet/rendering.rs | 27 +++++++++---------- quadratic-core/src/grid/sheet/search.rs | 4 +-- quadratic-core/src/grid/sheet/sheet_test.rs | 6 ++--- quadratic-core/src/test_util.rs | 3 +-- quadratic-core/src/util.rs | 8 +++--- 20 files changed, 102 insertions(+), 96 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index d30fee43af..b00e11c449 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -17,7 +17,7 @@ impl GridController { sheet_rect: &SheetRect, ) -> Result<()> { if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() { - self.send_updated_bounds_rect(&sheet_rect, false); + self.send_updated_bounds_rect(sheet_rect, false); transaction.add_dirty_hashes_from_sheet_rect(*sheet_rect); if transaction.is_user() { @@ -55,7 +55,7 @@ impl GridController { transaction.reverse_operations.extend(reverse_operations); } - transaction.generate_thumbnail |= self.thumbnail_dirty_sheet_rect(&sheet_rect); + transaction.generate_thumbnail |= self.thumbnail_dirty_sheet_rect(sheet_rect); } // delete any code runs within the sheet_rect. @@ -369,7 +369,7 @@ impl GridController { .to_sheet_rect(sheet_id); self.send_to_wasm(transaction, &data_table_rect)?; - transaction.add_code_cell(sheet_id, data_table_pos.into()); + transaction.add_code_cell(sheet_id, data_table_pos); let forward_operations = vec![op]; let reverse_operations = vec![Operation::DataTableMeta { @@ -411,7 +411,7 @@ impl GridController { data_table.sort_all()?; self.send_to_wasm(transaction, &data_table_sheet_rect)?; - transaction.add_code_cell(sheet_id, data_table_pos.into()); + transaction.add_code_cell(sheet_id, data_table_pos); let forward_operations = vec![op]; let reverse_operations = vec![Operation::SortDataTable { @@ -509,7 +509,7 @@ mod tests { let op = Operation::FlattenDataTable { sheet_pos }; let mut transaction = PendingTransaction::default(); - assert_simple_csv(&gc, sheet_id, pos, file_name); + assert_simple_csv(gc, sheet_id, pos, file_name); gc.execute_flatten_data_table(&mut transaction, op).unwrap(); @@ -518,9 +518,9 @@ mod tests { assert!(gc.sheet(sheet_id).first_data_table_within(pos).is_err()); - assert_flattened_simple_csv(&gc, sheet_id, pos, file_name); + assert_flattened_simple_csv(gc, sheet_id, pos, file_name); - print_table(&gc, sheet_id, Rect::new(0, 0, 2, 2)); + print_table(gc, sheet_id, Rect::new(0, 0, 2, 2)); transaction } @@ -536,10 +536,10 @@ mod tests { assert!(gc.sheet(sheet_id).first_data_table_within(pos).is_err()); let first_row = vec!["city", "region", "country", "population"]; - assert_cell_value_row(&gc, sheet_id, 0, 3, 0, first_row); + assert_cell_value_row(gc, sheet_id, 0, 3, 0, first_row); let last_row = vec!["Concord", "NH", "United States", "42605"]; - assert_cell_value_row(&gc, sheet_id, 0, 3, 10, last_row); + assert_cell_value_row(gc, sheet_id, 0, 3, 10, last_row); (gc, sheet_id, pos, file_name) } @@ -552,16 +552,16 @@ mod tests { file_name: &'a str, ) -> (&'a GridController, SheetId, Pos, &'a str) { let first_row = vec!["Concord", "NH", "United States", "42605"]; - assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 1, first_row); + assert_data_table_cell_value_row(gc, sheet_id, 0, 3, 1, first_row); let second_row = vec!["Marlborough", "MA", "United States", "38334"]; - assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 2, second_row); + assert_data_table_cell_value_row(gc, sheet_id, 0, 3, 2, second_row); let third_row = vec!["Northbridge", "MA", "United States", "14061"]; - assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 3, third_row); + assert_data_table_cell_value_row(gc, sheet_id, 0, 3, 3, third_row); let last_row = vec!["Westborough", "MA", "United States", "29313"]; - assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 10, last_row); + assert_data_table_cell_value_row(gc, sheet_id, 0, 3, 10, last_row); (gc, sheet_id, pos, file_name) } diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 1a12c9e926..9213bf58fb 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -53,7 +53,7 @@ impl GridController { Operation::DataTableFirstRowAsHeader { .. } => { Self::handle_execution_operation_result( self.execute_data_table_first_row_as_header(transaction, op), - ) + ); } Operation::ComputeCode { .. } => self.execute_compute_code(transaction, op), Operation::SetCellFormats { .. } => self.execute_set_cell_formats(transaction, op), @@ -111,14 +111,18 @@ impl GridController { #[cfg(test)] pub fn execute_reverse_operations(gc: &mut GridController, transaction: &PendingTransaction) { - let mut undo_transaction = PendingTransaction::default(); - undo_transaction.operations = transaction.reverse_operations.clone().into(); + let mut undo_transaction = PendingTransaction { + operations: transaction.reverse_operations.clone().into(), + ..Default::default() + }; gc.execute_operation(&mut undo_transaction); } #[cfg(test)] pub fn execute_forward_operations(gc: &mut GridController, transaction: &mut PendingTransaction) { - let mut undo_transaction = PendingTransaction::default(); - undo_transaction.operations = transaction.forward_operations.clone().into(); + let mut undo_transaction = PendingTransaction { + operations: transaction.forward_operations.clone().into(), + ..Default::default() + }; gc.execute_operation(&mut undo_transaction); } diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 1f9afe2fa9..815cf29b2e 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -338,7 +338,7 @@ impl GridController { for (x, header) in headers.into_iter().enumerate() { cell_values .set(x as u32, 0, header) - .map_err(|e| error(e.to_string()))? + .map_err(|e| error(e.to_string()))?; } let reader = builder.build()?; @@ -517,7 +517,7 @@ mod test { sheet_pos, code_run, .. - } => (sheet_pos.clone(), code_run.clone().unwrap()), + } => (*sheet_pos, code_run.clone().unwrap()), _ => panic!("Expected SetCodeRun operation"), }; assert_eq!(sheet_pos.x, 1); diff --git a/quadratic-core/src/controller/user_actions/cells.rs b/quadratic-core/src/controller/user_actions/cells.rs index 5ad013b0d3..82f489466e 100644 --- a/quadratic-core/src/controller/user_actions/cells.rs +++ b/quadratic-core/src/controller/user_actions/cells.rs @@ -22,15 +22,11 @@ impl GridController { let is_data_table = if let Some(cell_value) = cell_value { matches!(cell_value, CellValue::Code(_) | CellValue::Import(_)) } else { - sheet - .data_tables - .iter() - .find(|(code_cell_pos, data_table)| { - data_table - .output_rect(**code_cell_pos, false) - .contains(Pos::from(sheet_pos)) - }) - .is_some() + sheet.data_tables.iter().any(|(code_cell_pos, data_table)| { + data_table + .output_rect(*code_cell_pos, false) + .contains(Pos::from(sheet_pos)) + }) }; match is_data_table { diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index ad892d4bd6..0451939339 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -140,10 +140,10 @@ pub(crate) mod tests { ); let first_row = vec!["city", "region", "country", "population"]; - assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 0, first_row); + assert_data_table_cell_value_row(gc, sheet_id, 0, 3, 0, first_row); let last_row = vec!["Concord", "NH", "United States", "42605"]; - assert_data_table_cell_value_row(&gc, sheet_id, 0, 3, 10, last_row); + assert_data_table_cell_value_row(gc, sheet_id, 0, 3, 10, last_row); (gc, sheet_id, pos, file_name) } @@ -504,7 +504,7 @@ pub(crate) mod tests { #[parallel] fn should_import_utf16_with_invalid_characters() { let file_name = "encoding_issue.csv"; - let csv_file = read_test_csv_file(&file_name); + let csv_file = read_test_csv_file(file_name); let mut gc = GridController::test(); let sheet_id = gc.grid.sheets()[0].id; diff --git a/quadratic-core/src/formulas/functions/mathematics.rs b/quadratic-core/src/formulas/functions/mathematics.rs index ca177ed259..20c464e262 100644 --- a/quadratic-core/src/formulas/functions/mathematics.rs +++ b/quadratic-core/src/formulas/functions/mathematics.rs @@ -441,8 +441,8 @@ mod tests { // Test mismatched range assert_eq!( RunErrorMsg::ExactArraySizeMismatch { - expected: ArraySize::try_from((1 as i64, 12 as i64)).unwrap(), - got: ArraySize::try_from((1 as i64, 11 as i64)).unwrap(), + expected: ArraySize::try_from((1_i64, 12_i64)).unwrap(), + got: ArraySize::try_from((1_i64, 11_i64)).unwrap(), }, eval_to_err(&g, "SUMIFS(0..10, 0..11, \"<=5\")").msg, ); diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index d99b0ac8d8..20c62e6c3c 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -1,4 +1,4 @@ -//! +//! DataTable columns use crate::grid::js_types::JsDataTableColumn; use crate::{CellValue, Value}; @@ -101,13 +101,14 @@ impl DataTable { // .collect_vec(); // let name = unique_name(&name, all_names); - self.columns + if let Some(column) = self + .columns .as_mut() .and_then(|columns| columns.get_mut(index)) - .map(|column| { - column.name = CellValue::Text(name); - column.display = display; - }); + { + column.name = CellValue::Text(name); + column.display = display; + } Ok(()) } @@ -116,12 +117,13 @@ impl DataTable { pub fn set_header_display_at(&mut self, index: usize, display: bool) -> anyhow::Result<()> { self.check_index(index, true)?; - self.columns + if let Some(column) = self + .columns .as_mut() .and_then(|columns| columns.get_mut(index)) - .map(|column| { - column.display = display; - }); + { + column.display = display; + } Ok(()) } @@ -183,7 +185,7 @@ pub mod test { assert_eq!(data_table.columns, Some(expected_columns)); // test column headings taken from first row - let value = Value::Array(values.clone().into()); + let value = Value::Array(values.clone()); let mut data_table = DataTable::new(kind.clone(), "Table 1", value, false, true, true) .with_last_modified(data_table.last_modified); @@ -211,7 +213,7 @@ pub mod test { // test setting header display at index data_table.set_header_display_at(0, false).unwrap(); - assert_eq!(data_table.columns.as_ref().unwrap()[0].display, false); + assert!(!data_table.columns.as_ref().unwrap()[0].display); } #[test] diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs index 2c02aafd50..72d50e1cb6 100644 --- a/quadratic-core/src/grid/data_table/display_value.rs +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -1,4 +1,14 @@ +//! Display value for DataTable //! +//! The display value is the value that is displayed in the DataTable and may +//! be different from the actual value. +//! +//! The `display_buffer` is an option mapping row indices to display values. +//! If the `display_buffer` is `None`, then the display value is the same as the +//! actual value. If the `display_buffer` is `Some(display_buffer)`, then the +//! display value is the value at the index specified by the `display_buffer` +//! at the given row and column. This pattern enables a single copy of the +//! DataTable to exist in memory and be used for both display and execution. use crate::{Array, CellValue, Pos, Value}; use anyhow::{anyhow, Ok, Result}; @@ -6,17 +16,12 @@ use anyhow::{anyhow, Ok, Result}; use super::DataTable; impl DataTable { - pub fn display_value_from_buffer(&self, display_buffer: &Vec) -> Result { + pub fn display_value_from_buffer(&self, display_buffer: &[u64]) -> Result { let value = self.value.to_owned().into_array()?; let values = display_buffer .iter() - .filter_map(|index| { - value - .get_row(*index as usize) - .map(|row| row.into_iter().cloned().collect::>()) - .ok() - }) + .filter_map(|index| value.get_row(*index as usize).map(|row| row.to_vec()).ok()) .collect::>>(); let array = Array::from(values); @@ -26,7 +31,7 @@ impl DataTable { pub fn display_value_from_buffer_at( &self, - display_buffer: &Vec, + display_buffer: &[u64], pos: Pos, ) -> Result<&CellValue> { let y = display_buffer @@ -59,7 +64,7 @@ impl DataTable { } if !self.header_is_first_row && self.show_header { - pos.y = pos.y - 1; + pos.y -= 1; } match self.display_buffer { diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 1a123326a9..2020686881 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -39,7 +39,7 @@ impl Grid { .flat_map(|sheet| sheet.data_tables.values().map(|table| table.name.as_str())) .collect_vec(); - return unique_name(name, all_names, require_number); + unique_name(name, all_names, require_number) } pub fn update_data_table_name( @@ -63,6 +63,7 @@ impl Grid { } } +#[allow(clippy::large_enum_variant)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Display)] pub enum DataTableKind { CodeRun(CodeRun), @@ -315,7 +316,8 @@ impl DataTable { // bold the headers if they exist if data_table.header_is_first_row { - [0..table.count_columns()] + (0..table.count_columns()) + .collect::>() .iter() .enumerate() .for_each(|(index, _)| { @@ -392,7 +394,7 @@ pub mod test { let kind = data_table.kind.clone(); let values = data_table.value.clone().into_array().unwrap(); - let expected_values = Value::Array(values.clone().into()); + let expected_values = Value::Array(values.clone()); let expected_data_table = DataTable::new( kind.clone(), "test.csv", diff --git a/quadratic-core/src/grid/data_table/sort.rs b/quadratic-core/src/grid/data_table/sort.rs index b6c7b62076..2ef378e750 100644 --- a/quadratic-core/src/grid/data_table/sort.rs +++ b/quadratic-core/src/grid/data_table/sort.rs @@ -1,4 +1,4 @@ -//! +//! DataTable sorting use anyhow::{Ok, Result}; use itertools::Itertools; @@ -82,7 +82,7 @@ impl DataTable { .iter() .position(|sort| sort.column_index == column_index); - index.and_then(|index| Some(sort.remove(index))) + index.map(|index| sort.remove(index)) }); match self.sort { diff --git a/quadratic-core/src/grid/data_table/table_formats.rs b/quadratic-core/src/grid/data_table/table_formats.rs index c3ad38b5c0..86cc5b8bd8 100644 --- a/quadratic-core/src/grid/data_table/table_formats.rs +++ b/quadratic-core/src/grid/data_table/table_formats.rs @@ -51,7 +51,7 @@ impl TableFormats { let column = self .cells .entry(column_index) - .or_insert_with(ColumnData::default); + .or_default(); if let Some(mut cell) = column.get(unsorted_row_index) { let replace = cell.merge_update_into(&format); @@ -71,7 +71,7 @@ impl TableFormats { let column = self .columns .entry(column_index) - .or_insert_with(Format::default); + .or_default(); let undo = column.merge_update_into(&format); if undo.is_default() { None diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index 75c300b657..f47d854eec 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -100,6 +100,7 @@ pub struct DataTableColumnSchema { pub value_index: u32, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum DataTableKindSchema { CodeRun(CodeRunSchema), diff --git a/quadratic-core/src/grid/formats/format.rs b/quadratic-core/src/grid/formats/format.rs index 2d8771d618..5f809b6014 100644 --- a/quadratic-core/src/grid/formats/format.rs +++ b/quadratic-core/src/grid/formats/format.rs @@ -188,11 +188,11 @@ impl Format { /// sheet, row, column, cell, table, table-column, table-cell) pub fn combine(formats: Vec>) -> Format { let mut combined_format = Format::default(); - for format in formats { - if let Some(format) = format { - combined_format.merge_update_into(&format.into()); - } + + for format in formats.into_iter().flatten() { + combined_format.merge_update_into(&format.into()); } + combined_format } diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 7812b9ba33..01f8b8f324 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -84,12 +84,10 @@ impl Sheet { .find(|(code_cell_pos, data_table)| { data_table.output_rect(**code_cell_pos, false).contains(pos) }) - .and_then(|(code_cell_pos, data_table)| { + .map(|(code_cell_pos, data_table)| { let x = (pos.x - code_cell_pos.x) as u32; let y = (pos.y - code_cell_pos.y) as u32; - data_table.set_cell_value_at(x, y, value); - - Some(()) + data_table.set_cell_value_at(x, y, value).then_some(|| true) }) .is_some() } diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index 0291afdc2e..0bf1c20161 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -54,7 +54,7 @@ impl Sheet { data_table .output_rect(*data_table_pos, false) .contains(pos) - .then(|| *data_table_pos) + .then_some(*data_table_pos) }) .collect(); diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 43aa297bce..69e3ce2ba4 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -20,6 +20,7 @@ impl Sheet { } /// creates a render for a single cell + #[allow(clippy::too_many_arguments)] fn get_render_cell( &self, x: i64, @@ -71,15 +72,13 @@ impl Sheet { None }; let special = special.or_else(|| { - self.validations - .render_special_pos(Pos { x, y }) - .or_else(|| { - if matches!(value, CellValue::Logical(_)) { - Some(JsRenderCellSpecial::Logical) - } else { - None - } - }) + self.validations.render_special_pos(Pos { x, y }).or({ + if matches!(value, CellValue::Logical(_)) { + Some(JsRenderCellSpecial::Logical) + } else { + None + } + }) }); match column { @@ -727,7 +726,7 @@ mod tests { Pos { x: 2, y: 3 }, Some(DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1".into(), + "Table 1", Value::Single(CellValue::Text("hello".to_string())), false, false, @@ -946,7 +945,7 @@ mod tests { // data_table is always 3x2 let data_table = DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1".into(), + "Table 1", Value::Array(vec![vec!["1", "2", "3"], vec!["4", "5", "6"]].into()), false, false, @@ -1002,7 +1001,7 @@ mod tests { let code_run = DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1".into(), + "Table 1", Value::Single(CellValue::Number(1.into())), false, false, @@ -1129,7 +1128,7 @@ mod tests { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1".into(), + "Table 1", Value::Single(CellValue::Number(2.into())), false, false, @@ -1190,7 +1189,7 @@ mod tests { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1".into(), + "Table 1", Value::Single(CellValue::Image(image.clone())), false, false, diff --git a/quadratic-core/src/grid/sheet/search.rs b/quadratic-core/src/grid/sheet/search.rs index cb134029e2..63b8b03e1f 100644 --- a/quadratic-core/src/grid/sheet/search.rs +++ b/quadratic-core/src/grid/sheet/search.rs @@ -507,7 +507,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1".into(), + "Table 1", Value::Single("world".into()), false, false, @@ -552,7 +552,7 @@ mod test { }; let data_table = DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1".into(), + "Table 1", Value::Array(Array::from(vec![ vec!["abc", "def", "ghi"], vec!["jkl", "mno", "pqr"], diff --git a/quadratic-core/src/grid/sheet/sheet_test.rs b/quadratic-core/src/grid/sheet/sheet_test.rs index b8c896e929..365770014c 100644 --- a/quadratic-core/src/grid/sheet/sheet_test.rs +++ b/quadratic-core/src/grid/sheet/sheet_test.rs @@ -71,7 +71,7 @@ impl Sheet { crate::Pos { x, y }, Some(DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1".into(), + "Table 1", Value::Single(value), false, false, @@ -130,7 +130,7 @@ impl Sheet { Pos { x, y }, Some(DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1".into(), + "Table 1", Value::Array(array), false, false, @@ -176,7 +176,7 @@ impl Sheet { Pos { x, y }, Some(DataTable::new( DataTableKind::CodeRun(code_run), - "Table 1".into(), + "Table 1", Value::Array(array), false, false, diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index 6fb16989d1..4992391915 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -263,7 +263,6 @@ pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rec ); } else { println!("Sheet not found"); - return; } } @@ -312,7 +311,7 @@ pub fn print_table_sheet(sheet: &Sheet, rect: Rect, disply_cell_values: bool) { true => sheet.cell_value(pos), false => sheet .data_table(rect.min) - .expect(&format!("Data table not found at {:?}", rect.min)) + .unwrap_or_else(|| panic!("Data table not found at {:?}", rect.min)) .cell_value_at(x as u32, y as u32), }; diff --git a/quadratic-core/src/util.rs b/quadratic-core/src/util.rs index 4c7e29e589..23a4675728 100644 --- a/quadratic-core/src/util.rs +++ b/quadratic-core/src/util.rs @@ -214,12 +214,12 @@ pub fn unused_name(prefix: &str, already_used: &[&str]) -> String { /// Starts at 1, and checks if the name is unique, then 2, etc. /// If `require_number` is true, the name will always have an appended number. pub fn unique_name(name: &str, all_names: &[&str], require_number: bool) -> String { - let base = MATCH_NUMBERS.replace(&name, ""); + let base = MATCH_NUMBERS.replace(name, ""); let contains_number = base != name; - let should_short_circuit = !(require_number && !contains_number); + let should_short_circuit = !require_number || contains_number; // short circuit if the name is unique - if should_short_circuit && !all_names.contains(&&name) { + if should_short_circuit && !all_names.contains(&name) { return name.to_string(); } @@ -227,7 +227,7 @@ pub fn unique_name(name: &str, all_names: &[&str], require_number: bool) -> Stri let mut num = 1; let mut name = String::from(""); - while name == "" { + while name.is_empty() { let new_name = format!("{}{}", base, num); let new_name_alt = format!("{} {}", base, num); let new_names = [new_name.as_str(), new_name_alt.as_str()]; From 8beca45649ea468e25229cba77976efa31a81325 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 4 Nov 2024 08:55:06 -0800 Subject: [PATCH 194/373] add chart_outpu --- .../execution/auto_resize_row_heights.rs | 17 +- .../execution/control_transaction.rs | 17 +- .../execution/receive_multiplayer.rs | 68 ++---- .../src/controller/execution/run_code/mod.rs | 7 +- .../execution/run_code/run_formula.rs | 36 ++- .../execution/run_code/run_javascript.rs | 205 ++++++---------- .../execution/run_code/run_python.rs | 218 +++++++----------- quadratic-core/src/controller/send_render.rs | 14 +- .../src/controller/transaction_types.rs | 5 +- quadratic-core/src/grid/data_table/column.rs | 1 + quadratic-core/src/grid/data_table/mod.rs | 22 +- .../src/grid/file/serialize/data_table.rs | 2 + quadratic-core/src/grid/file/v1_7/file.rs | 1 + quadratic-core/src/grid/file/v1_8/schema.rs | 1 + quadratic-core/src/grid/sheet/rendering.rs | 17 +- 15 files changed, 242 insertions(+), 389 deletions(-) diff --git a/quadratic-core/src/controller/execution/auto_resize_row_heights.rs b/quadratic-core/src/controller/execution/auto_resize_row_heights.rs index 27d6c6b0d5..fbbd33746e 100644 --- a/quadratic-core/src/controller/execution/auto_resize_row_heights.rs +++ b/quadratic-core/src/controller/execution/auto_resize_row_heights.rs @@ -507,17 +507,12 @@ mod tests { assert_eq!(transaction.has_async, 1); assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["10".into(), "number".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["10".into(), "number".into()]), + ..Default::default() + }) .is_ok()); let sheet = gc.try_sheet(sheet_id).unwrap(); assert_eq!( diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index 052e091f2c..a0ba1dac6d 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -505,17 +505,12 @@ mod tests { let transaction_id = gc.last_transaction().unwrap().id; - let result = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["1".into(), "number".into()]), - None, - None, - None, - None, - )); + let result = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["1".into(), "number".into()]), + ..Default::default() + }); assert!(result.is_ok()); } diff --git a/quadratic-core/src/controller/execution/receive_multiplayer.rs b/quadratic-core/src/controller/execution/receive_multiplayer.rs index 63239153a3..a3077c394c 100644 --- a/quadratic-core/src/controller/execution/receive_multiplayer.rs +++ b/quadratic-core/src/controller/execution/receive_multiplayer.rs @@ -787,17 +787,12 @@ mod tests { assert!(matches!(code_cell, Some(CellValue::Code(_)))); // mock the python calculation returning the result - let result = client.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["async output".into(), "text".into()]), - None, - None, - None, - None, - )); + let result = client.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["async output".into(), "text".into()]), + ..Default::default() + }); assert!(result.is_ok()); assert_eq!( @@ -885,17 +880,12 @@ mod tests { assert!(matches!(code_cell, Some(CellValue::Code(_)))); // mock the python calculation returning the result - let result = client.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["async output".into(), "text".into()]), - None, - None, - None, - None, - )); + let result = client.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["async output".into(), "text".into()]), + ..Default::default() + }); assert!(result.is_ok()); assert_eq!( @@ -945,17 +935,12 @@ mod tests { .ok() .unwrap(); - let result = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["2".into(), "number".into()]), - None, - None, - None, - None, - )); + let result = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["2".into(), "number".into()]), + ..Default::default() + }); assert!(result.is_ok()); let transaction = gc.last_transaction().unwrap(); @@ -981,17 +966,12 @@ mod tests { .ok() .unwrap(); - let result = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["3".into(), "number".into()]), - None, - None, - None, - None, - )); + let result = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["3".into(), "number".into()]), + ..Default::default() + }); assert!(result.is_ok()); let transaction = gc.last_transaction().unwrap(); diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index aeaad7327d..94642624a8 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -499,13 +499,8 @@ mod test { let result = JsCodeResult { transaction_id: transaction.id.to_string(), success: true, - std_out: None, - std_err: None, - line_number: None, output_value: Some(vec!["test".into(), "image".into()]), - output_array: None, - output_display_type: None, - cancel_compute: None, + ..Default::default() }; gc.calculation_complete(result).unwrap(); expect_js_call_count("jsSendImage", 1, true); diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index 325f89b742..28b599a67b 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -231,17 +231,13 @@ mod test { fn test_js_code_result_to_code_cell_value_single() { let mut gc = GridController::test(); let sheet_id = gc.sheet_ids()[0]; - let result = JsCodeResult::new( - Uuid::new_v4().into(), - true, - None, - None, - Some(vec!["$12".into(), "number".into()]), - None, - None, - None, - None, - ); + let result = JsCodeResult { + transaction_id: Uuid::new_v4().into(), + success: true, + output_value: Some(vec!["$12".into(), "number".into()]), + output_display_type: Some("number".into()), + ..Default::default() + }; let mut transaction = PendingTransaction::default(); let sheet_pos = SheetPos { x: 0, @@ -296,17 +292,13 @@ mod test { ], ]; let mut transaction = PendingTransaction::default(); - let result = JsCodeResult::new( - transaction.id.to_string(), - true, - None, - None, - None, - Some(array_output), - None, - None, - None, - ); + let result = JsCodeResult { + transaction_id: transaction.id.to_string(), + success: true, + output_array: Some(array_output), + output_display_type: Some("array".into()), + ..Default::default() + }; let sheet_pos = SheetPos { x: 0, diff --git a/quadratic-core/src/controller/execution/run_code/run_javascript.rs b/quadratic-core/src/controller/execution/run_code/run_javascript.rs index 7a46bdd50b..db94feffed 100644 --- a/quadratic-core/src/controller/execution/run_code/run_javascript.rs +++ b/quadratic-core/src/controller/execution/run_code/run_javascript.rs @@ -56,17 +56,12 @@ mod tests { gc.set_code_cell(sheet_pos, CodeCellLanguage::Python, code.clone(), None); let transaction = gc.async_transactions().first().unwrap(); - gc.calculation_complete(JsCodeResult::new( - transaction.id.to_string(), - true, - None, - None, - Some(vec!["test".into(), "text".into()]), - None, - None, - None, - None, - )) + gc.calculation_complete(JsCodeResult { + transaction_id: transaction.id.to_string(), + success: true, + output_value: Some(vec!["test".into(), "text".into()]), + ..Default::default() + }) .ok(); let sheet = gc.grid.try_sheet(sheet_id).unwrap(); @@ -107,17 +102,12 @@ mod tests { // transaction for its id let transaction_id = gc.async_transactions()[0].id; - let summary = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["hello world".into(), "text".into()]), - None, - None, - None, - None, - )); + let summary = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["hello world".into(), "text".into()]), + ..Default::default() + }); assert!(summary.is_ok()); let sheet = gc.try_sheet(sheet_id).unwrap(); assert_eq!( @@ -174,17 +164,12 @@ mod tests { // mock the python calculation returning the result assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["10".into(), "number".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["10".into(), "number".into()]), + ..Default::default() + }) .is_ok()); // check that the value at (0, 1) contains the expected output @@ -230,17 +215,12 @@ mod tests { // mock the get_cells to populate dependencies let _ = gc.calculation_get_cells(transaction_id.to_string(), 0, 0, 1, Some(1), None, None); // mock the calculation_complete - let _ = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["10".into(), "number".into()]), - None, - None, - None, - None, - )); + let _ = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["10".into(), "number".into()]), + ..Default::default() + }); // replace the value in (0, 0) to trigger the python calculation gc.set_cell_value( @@ -268,17 +248,12 @@ mod tests { }]) ); assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["11".into(), "number".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["11".into(), "number".into()]), + ..Default::default() + }) .is_ok()); // check that the value at (0, 1) contains the expected output @@ -319,17 +294,12 @@ mod tests { // mock the python calculation returning the result assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - None, - Some(python_array(vec![1, 2, 3])), - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_array: Some(python_array(vec![1, 2, 3])), + ..Default::default() + }) .is_ok()); let sheet = gc.try_sheet(sheet_id).unwrap(); @@ -368,17 +338,13 @@ mod tests { ); let transaction_id = gc.async_transactions()[0].id; // mock the python result - let result = JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["".into(), "blank".into()]), - None, - None, - None, - Some(true), - ); + let result = JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["".into(), "blank".into()]), + cancel_compute: Some(true), + ..Default::default() + }; gc.calculation_complete(result).unwrap(); assert!(gc.async_transactions().is_empty()); let sheet = gc.try_sheet(sheet_id).unwrap(); @@ -411,17 +377,12 @@ mod tests { // mock the python calculation returning the result assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["original output".into(), "text".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["original output".into(), "text".into()]), + ..Default::default() + }) .is_ok()); // check that the value at (0, 0) contains the expected output @@ -452,17 +413,12 @@ mod tests { // mock the python calculation returning the result assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["new output".into(), "text".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["new output".into(), "text".into()]), + ..Default::default() + }) .is_ok()); // repeat the same action to find a bug that occurs on second change @@ -493,17 +449,12 @@ mod tests { // mock the python calculation returning the result assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["new output second time".into(), "text".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["new output second time".into(), "text".into()]), + ..Default::default() + }) .is_ok()); // check that the value at (0, 0) contains the original output @@ -555,17 +506,12 @@ mod tests { type_name: "number".into(), } ); - let result = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["2".into(), "number".into()]), - None, - None, - None, - None, - )); + let result = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["2".into(), "number".into()]), + ..Default::default() + }); assert!(result.is_ok()); // todo... @@ -596,17 +542,12 @@ mod tests { type_name: "number".into(), } ); - let result = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["3".into(), "number".into()]), - None, - None, - None, - None, - )); + let result = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["3".into(), "number".into()]), + ..Default::default() + }); assert!(result.is_ok()); // todo... diff --git a/quadratic-core/src/controller/execution/run_code/run_python.rs b/quadratic-core/src/controller/execution/run_code/run_python.rs index 482a7a55cc..45eeb81cbc 100644 --- a/quadratic-core/src/controller/execution/run_code/run_python.rs +++ b/quadratic-core/src/controller/execution/run_code/run_python.rs @@ -56,17 +56,12 @@ mod tests { let transaction = gc.async_transactions().first().unwrap(); let transaction_id = transaction.id; - gc.calculation_complete(JsCodeResult::new( - transaction.id.to_string(), - true, - None, - None, - Some(vec!["test".into(), "text".into()]), - None, - None, - None, - None, - )) + gc.calculation_complete(JsCodeResult { + transaction_id: transaction.id.to_string(), + success: true, + output_value: Some(vec!["test".into(), "text".into()]), + ..Default::default() + }) .ok(); let sheet = gc.grid.try_sheet(sheet_id).unwrap(); @@ -111,17 +106,12 @@ mod tests { // transaction for its id let transaction_id = gc.async_transactions()[0].id; - let summary = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["hello world".into(), "text".into()]), - None, - None, - None, - None, - )); + let summary = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["hello world".into(), "text".into()]), + ..Default::default() + }); assert!(summary.is_ok()); let sheet = gc.try_sheet(sheet_id).unwrap(); assert_eq!( @@ -182,17 +172,12 @@ mod tests { // mock the python calculation returning the result assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["10".into(), "number".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["10".into(), "number".into()]), + ..Default::default() + }) .is_ok()); // check that the value at (0, 1) contains the expected output @@ -242,17 +227,12 @@ mod tests { // mock the get_cells to populate dependencies let _ = gc.calculation_get_cells(transaction_id.to_string(), 0, 0, 1, Some(1), None, None); // mock the calculation_complete - let _ = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["10".into(), "number".into()]), - None, - None, - None, - None, - )); + let _ = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["10".into(), "number".into()]), + ..Default::default() + }); // replace the value in (0, 0) to trigger the python calculation gc.set_cell_value( @@ -280,17 +260,12 @@ mod tests { }]) ); assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["11".into(), "number".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["11".into(), "number".into()]), + ..Default::default() + }) .is_ok()); // check that the value at (0, 1) contains the expected output @@ -335,17 +310,13 @@ mod tests { // mock the python calculation returning the result assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - None, - Some(python_array(vec![1, 2, 3])), - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: None, + output_array: Some(python_array(vec![1, 2, 3])), + ..Default::default() + }) .is_ok()); let sheet = gc.try_sheet(sheet_id).unwrap(); @@ -387,17 +358,13 @@ mod tests { ); let transaction_id = gc.async_transactions()[0].id; // mock the python result - let result = JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["".into(), "blank".into()]), - None, - None, - None, - Some(true), - ); + let result = JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["".into(), "blank".into()]), + cancel_compute: Some(true), + ..Default::default() + }; gc.calculation_complete(result).unwrap(); assert!(gc.async_transactions().is_empty()); let sheet = gc.try_sheet(sheet_id).unwrap(); @@ -434,17 +401,12 @@ mod tests { // mock the python calculation returning the result assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["original output".into(), "text".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["original output".into(), "text".into()]), + ..Default::default() + }) .is_ok()); // check that the value at (0, 0) contains the expected output @@ -475,17 +437,12 @@ mod tests { // mock the python calculation returning the result assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["new output".into(), "text".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["new output".into(), "text".into()]), + ..Default::default() + }) .is_ok()); // repeat the same action to find a bug that occurs on second change @@ -516,17 +473,12 @@ mod tests { // mock the python calculation returning the result assert!(gc - .calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["new output second time".into(), "text".into()]), - None, - None, - None, - None, - )) + .calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["new output second time".into(), "text".into()]), + ..Default::default() + }) .is_ok()); // check that the value at (0, 0) contains the original output @@ -582,17 +534,18 @@ mod tests { type_name: "number".into(), } ); - let result = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["2".into(), "number".into()]), - None, - None, - None, - None, - )); + let result = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["2".into(), "number".into()]), + std_out: None, + std_err: None, + output_array: None, + line_number: None, + output_display_type: None, + cancel_compute: None, + chart_output: None, + }); assert!(result.is_ok()); // todo... @@ -623,17 +576,18 @@ mod tests { type_name: "number".into(), } ); - let result = gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["3".into(), "number".into()]), - None, - None, - None, - None, - )); + let result = gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["3".into(), "number".into()]), + std_out: None, + std_err: None, + output_array: None, + line_number: None, + output_display_type: None, + cancel_compute: None, + chart_output: None, + }); assert!(result.is_ok()); // todo... diff --git a/quadratic-core/src/controller/send_render.rs b/quadratic-core/src/controller/send_render.rs index fedaa8aab5..d6f7c34a8c 100644 --- a/quadratic-core/src/controller/send_render.rs +++ b/quadratic-core/src/controller/send_render.rs @@ -574,13 +574,8 @@ mod test { let _ = gc.calculation_complete(JsCodeResult { transaction_id, success: true, - std_err: None, - std_out: None, output_value: Some(vec!["".to_string(), "text".to_string()]), - output_array: None, - line_number: None, - output_display_type: None, - cancel_compute: None, + ..Default::default() }); expect_js_call( @@ -614,13 +609,8 @@ mod test { let _ = gc.calculation_complete(JsCodeResult { transaction_id, success: true, - std_err: None, - std_out: None, output_value: Some(vec!["".to_string(), "text".to_string()]), - output_array: None, - line_number: None, - output_display_type: None, - cancel_compute: None, + ..Default::default() }); gc.set_cell_render_size( diff --git a/quadratic-core/src/controller/transaction_types.rs b/quadratic-core/src/controller/transaction_types.rs index 32fcccb0ce..6477576b78 100644 --- a/quadratic-core/src/controller/transaction_types.rs +++ b/quadratic-core/src/controller/transaction_types.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; // this version is deprecated. See JsRunResult below for new implementation -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Default, Debug, Serialize, Deserialize, TS)] pub struct JsCodeResult { pub transaction_id: String, pub success: bool, @@ -13,6 +13,7 @@ pub struct JsCodeResult { pub output_array: Option>>>, pub output_display_type: Option, pub cancel_compute: Option, + pub chart_output: Option<(u32, u32)>, } impl JsCodeResult { @@ -28,6 +29,7 @@ impl JsCodeResult { line_number: Option, output_display_type: Option, cancel_compute: Option, + chart_output: Option<(u32, u32)>, ) -> Self { JsCodeResult { transaction_id, @@ -39,6 +41,7 @@ impl JsCodeResult { line_number, output_display_type, cancel_compute, + chart_output, } } } diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index d99b0ac8d8..18b6b0736c 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -234,6 +234,7 @@ pub mod test { header_is_first_row: true, alternating_colors: true, formats: Default::default(), + chart_output: None, }; sheet.set_cell_value( pos, diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 1a123326a9..05a2a683b9 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -84,6 +84,9 @@ pub struct DataTable { pub last_modified: DateTime, pub alternating_colors: bool, pub formats: TableFormats, + + // width and height of the chart (html or image) output + pub chart_output: Option<(u32, u32)>, } impl From<(Import, Array, &Grid)> for DataTable { @@ -132,6 +135,7 @@ impl DataTable { alternating_colors: true, last_modified: Utc::now(), formats: Default::default(), + chart_output: None, }; if header_is_first_row { @@ -244,15 +248,19 @@ impl DataTable { /// Returns the size of the output array, or defaults to `_1X1` (since output always includes the code_cell). /// Note: this does not take spill_error into account. pub fn output_size(&self) -> ArraySize { - match &self.value { - Value::Array(a) => { - let mut size = a.size(); - if self.show_header && !self.header_is_first_row { - size.h = NonZeroU32::new(size.h.get() + 1).unwrap(); + if let Some((w, h)) = self.chart_output { + ArraySize::new(w, h).unwrap() + } else { + match &self.value { + Value::Array(a) => { + let mut size = a.size(); + if self.show_header && !self.header_is_first_row { + size.h = NonZeroU32::new(size.h.get() + 1).unwrap(); + } + size } - size + Value::Single(_) | Value::Tuple(_) => ArraySize::_1X1, } - Value::Single(_) | Value::Tuple(_) => ArraySize::_1X1, } } diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 6248101f91..3b1e5617cb 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -242,6 +242,7 @@ pub(crate) fn import_data_table_builder( display_buffer: data_table.display_buffer, alternating_colors: data_table.alternating_colors, formats: import_data_table_formats(data_table.formats), + chart_output: data_table.chart_output, }; new_data_tables.insert(Pos { x: pos.x, y: pos.y }, data_table); @@ -477,6 +478,7 @@ pub(crate) fn export_data_tables( value, alternating_colors: data_table.alternating_colors, formats: export_data_table_formats(data_table.formats), + chart_output: data_table.chart_output, }; (current::PosSchema::from(pos), data_table) diff --git a/quadratic-core/src/grid/file/v1_7/file.rs b/quadratic-core/src/grid/file/v1_7/file.rs index 24a5ad7713..e82c6a100c 100644 --- a/quadratic-core/src/grid/file/v1_7/file.rs +++ b/quadratic-core/src/grid/file/v1_7/file.rs @@ -59,6 +59,7 @@ fn upgrade_code_runs( last_modified: code_run.last_modified, alternating_colors: true, formats: Default::default(), + chart_output: None, }; Ok((v1_8::PosSchema::from(pos), new_data_table)) }) diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index 75c300b657..82f2e926b5 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -141,6 +141,7 @@ pub struct DataTableSchema { pub last_modified: Option>, pub alternating_colors: bool, pub formats: TableFormatsSchema, + pub chart_output: Option<(u32, u32)>, } impl From for AxisSchema { diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 43aa297bce..24ee140040 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -869,17 +869,12 @@ mod tests { None, ); let transaction_id = gc.async_transactions()[0].id; - gc.calculation_complete(JsCodeResult::new( - transaction_id.to_string(), - true, - None, - None, - Some(vec!["".into(), "text".into()]), - None, - None, - None, - None, - )) + gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["".into(), "text".into()]), + ..Default::default() + }) .ok(); let sheet = gc.sheet(sheet_id); let render_cells = sheet.get_html_output(); From 21f201be1257f8e5bc3772e4865797e16a4aed95 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 4 Nov 2024 09:01:55 -0800 Subject: [PATCH 195/373] fix tests --- .../execution/run_code/run_formula.rs | 19 +++-------- .../src/controller/transaction_types.rs | 33 +------------------ quadratic-core/src/grid/code_run.rs | 2 +- 3 files changed, 7 insertions(+), 47 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index 28b599a67b..30d1c0a951 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -51,7 +51,7 @@ impl GridController { #[cfg(test)] mod test { - use std::{collections::HashSet, str::FromStr}; + use std::str::FromStr; use bigdecimal::BigDecimal; use serial_test::parallel; @@ -253,14 +253,9 @@ mod test { CodeCellLanguage::Javascript, ); let code_run = CodeRun { - std_out: None, - std_err: None, - formatted_code_string: None, - cells_accessed: HashSet::new(), return_type: Some("number".into()), - line_number: None, - output_type: None, - error: None, + output_type: Some("number".into()), + ..Default::default() }; assert_eq!( result, @@ -326,14 +321,10 @@ mod test { CodeCellLanguage::Javascript, ); let code_run = CodeRun { - std_out: None, - std_err: None, formatted_code_string: None, - error: None, return_type: Some("array".into()), - line_number: None, - output_type: None, - cells_accessed: HashSet::new(), + output_type: Some("array".into()), + ..Default::default() }; assert_eq!( result, diff --git a/quadratic-core/src/controller/transaction_types.rs b/quadratic-core/src/controller/transaction_types.rs index 6477576b78..942dac8030 100644 --- a/quadratic-core/src/controller/transaction_types.rs +++ b/quadratic-core/src/controller/transaction_types.rs @@ -16,38 +16,7 @@ pub struct JsCodeResult { pub chart_output: Option<(u32, u32)>, } -impl JsCodeResult { - #[cfg(test)] - #[allow(clippy::too_many_arguments)] - pub fn new( - transaction_id: String, - success: bool, - std_err: Option, - std_out: Option, - output_value: Option>, - output_array: Option>>>, - line_number: Option, - output_display_type: Option, - cancel_compute: Option, - chart_output: Option<(u32, u32)>, - ) -> Self { - JsCodeResult { - transaction_id, - success, - std_err, - std_out, - output_value, - output_array, - line_number, - output_display_type, - cancel_compute, - chart_output, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] +#[derive(Debug, Serialize, Deserialize, TS)] pub struct JsConnectionResult { pub transaction_id: String, pub data: Vec, diff --git a/quadratic-core/src/grid/code_run.rs b/quadratic-core/src/grid/code_run.rs index e609606817..0ee840d7a3 100644 --- a/quadratic-core/src/grid/code_run.rs +++ b/quadratic-core/src/grid/code_run.rs @@ -10,7 +10,7 @@ use std::collections::HashSet; use strum_macros::Display; use wasm_bindgen::{convert::IntoWasmAbi, JsValue}; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct CodeRun { #[serde(skip_serializing_if = "Option::is_none")] pub formatted_code_string: Option, From 560b3299ff516539794b7c1fe8bc3bfc053b3cf2 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 08:22:42 -0700 Subject: [PATCH 196/373] Implement Toggle alternating colors in the UI --- quadratic-client/src/app/actions/dataTableSpec.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index b4466a8ca4..7355ff86c6 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -154,8 +154,18 @@ export const dataTableSpec: DataTableSpec = { label: 'Toggle alternating colors', checkbox: isAlternatingColorsShowing, run: () => { - console.log('TODO: toggle alternating colors'); - // quadraticCore.dataTableToggleAlternatingColors(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); + const table = getTable(); + if (table) { + quadraticCore.dataTableMeta( + sheets.sheet.id, + table.x, + table.y, + undefined, + !isAlternatingColorsShowing(), + undefined, + sheets.getCursorPosition() + ); + } }, }, [Action.RenameTableColumn]: { From c2e2e71143c206c8a4e18cbe85114fd76ddef7a9 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 10:55:20 -0700 Subject: [PATCH 197/373] Implement show_headers in rust --- .../execution/execute_operation/execute_data_table.rs | 11 +++++++++++ .../src/controller/operations/data_table.rs | 2 ++ quadratic-core/src/controller/operations/operation.rs | 6 ++++-- .../src/controller/user_actions/data_table.rs | 2 ++ .../src/wasm_bindings/controller/data_table.rs | 2 ++ 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index b00e11c449..b9f2f74743 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -331,6 +331,7 @@ impl GridController { ref name, ref alternating_colors, ref columns, + ref show_header, } = op { // get unique name first since it requires an immutable reference to the grid @@ -364,6 +365,13 @@ impl GridController { old_columns }); + let old_show_header = show_header.map(|show_header| { + let old_show_header = data_table.show_header.to_owned(); + data_table.show_header = show_header; + + old_show_header + }); + let data_table_rect = data_table .output_rect(sheet_pos.into(), true) .to_sheet_rect(sheet_id); @@ -377,6 +385,7 @@ impl GridController { name: old_name, alternating_colors: old_alternating_colors, columns: old_columns, + show_header: old_show_header, }]; self.data_table_operations( @@ -755,6 +764,7 @@ mod tests { name: Some(updated_name.into()), alternating_colors: None, columns: None, + show_header: None, }; let mut transaction = PendingTransaction::default(); gc.execute_data_table_meta(&mut transaction, op.clone()) @@ -790,6 +800,7 @@ mod tests { name: Some("ABC".into()), alternating_colors: None, columns: None, + show_header: None, }; let mut transaction = PendingTransaction::default(); gc.execute_data_table_meta(&mut transaction, op).unwrap(); diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index 2a6a9293ad..8153df4558 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -52,6 +52,7 @@ impl GridController { name: Option, alternating_colors: Option, columns: Option>, + show_header: Option, _cursor: Option, ) -> Vec { vec![Operation::DataTableMeta { @@ -59,6 +60,7 @@ impl GridController { name, alternating_colors, columns, + show_header, }] } diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 5287c84603..8e4d48bd06 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -61,6 +61,7 @@ pub enum Operation { name: Option, alternating_colors: Option, columns: Option>, + show_header: Option, }, SortDataTable { sheet_pos: SheetPos, @@ -244,11 +245,12 @@ impl fmt::Display for Operation { name, alternating_colors, columns, + show_header, } => { write!( fmt, - "DataTableMeta {{ sheet_pos: {} name: {:?} alternating_colors: {:?} columns: {:?} }}", - sheet_pos, name, alternating_colors, columns + "DataTableMeta {{ sheet_pos: {} name: {:?} alternating_colors: {:?} columns: {:?} show_header: {:?} }}", + sheet_pos, name, alternating_colors, columns, show_header ) } Operation::SortDataTable { sheet_pos, sort } => { diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index 07d195eb28..a57643b003 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -53,6 +53,7 @@ impl GridController { name: Option, alternating_colors: Option, columns: Option>, + show_header: Option, cursor: Option, ) { let ops = self.data_table_meta_operations( @@ -60,6 +61,7 @@ impl GridController { name, alternating_colors, columns, + show_header, cursor.to_owned(), ); self.start_user_transaction(ops, cursor, TransactionName::DataTableMeta); diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index fd41cd82b4..c18475f064 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -100,6 +100,7 @@ impl GridController { name: Option, alternating_colors: Option, columns_js: Option, + show_header: Option, cursor: Option, ) -> Result<(), JsValue> { let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; @@ -114,6 +115,7 @@ impl GridController { name, alternating_colors, columns, + show_header, cursor, ); From 732e3ec5c1c2ebe453968733b78b0bc427996a38 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 11:00:52 -0700 Subject: [PATCH 198/373] Implement show_headers in the client --- .../src/app/actions/dataTableSpec.ts | 16 ++++++++++++++-- .../gridGL/HTMLGrid/contextMenus/TableRename.tsx | 1 + .../src/app/quadratic-core-types/index.d.ts | 2 +- .../quadraticCore/coreClientMessages.ts | 1 + .../web-workers/quadraticCore/quadraticCore.ts | 4 ++++ .../app/web-workers/quadraticCore/worker/core.ts | 3 +++ .../quadraticCore/worker/coreClient.ts | 1 + 7 files changed, 25 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 7355ff86c6..ed15434511 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -114,8 +114,19 @@ export const dataTableSpec: DataTableSpec = { label: 'Show column headings', checkbox: isHeadingShowing, run: () => { - // const table = getTable(); - // quadraticCore.dataTableShowHeadings(sheets.sheet.id, table.x, table.y, sheets.getCursorPosition()); + const table = getTable(); + if (table) { + quadraticCore.dataTableMeta( + sheets.sheet.id, + table.x, + table.y, + undefined, + undefined, + undefined, + !isHeadingShowing(), + sheets.getCursorPosition() + ); + } }, }, [Action.DeleteDataTable]: { @@ -163,6 +174,7 @@ export const dataTableSpec: DataTableSpec = { undefined, !isAlternatingColorsShowing(), undefined, + undefined, sheets.getCursorPosition() ); } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx index 0a41533e58..daec07ee23 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx @@ -41,6 +41,7 @@ export const TableRename = () => { value, undefined, undefined, + undefined, '' ); } diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index a5b07a863c..3412ce0539 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -28,7 +28,7 @@ export interface JsBordersSheet { all: BorderStyleCell | null, columns: Record | null, return_info: JsReturnInfo | null, cells_accessed: Array | null, } -export interface JsCodeResult { transaction_id: string, success: boolean, std_out: string | null, std_err: string | null, line_number: number | null, output_value: Array | null, output_array: Array>> | null, output_display_type: string | null, cancel_compute: boolean | null, } +export interface JsCodeResult { transaction_id: string, success: boolean, std_out: string | null, std_err: string | null, line_number: number | null, output_value: Array | null, output_array: Array>> | null, output_display_type: string | null, cancel_compute: boolean | null, chart_output: [number, number] | null, } export interface JsDataTableColumn { name: string, display: boolean, valueIndex: number, } export interface JsGetCellResponse { x: bigint, y: bigint, value: string, type_name: string, } export interface JsHtmlOutput { sheet_id: string, x: bigint, y: bigint, html: string | null, w: string | null, h: string | null, } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index 41a534814b..eebe9a7874 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -1050,6 +1050,7 @@ export interface ClientCoreDataTableMeta { display: boolean; valueIndex: number; }[]; + showHeader?: boolean; cursor: string; } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index 27ce7d0a90..d89bf742ef 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -1216,6 +1216,7 @@ class QuadraticCore { name?: string, alternatingColors?: boolean, columns?: { name: string; display: boolean; valueIndex: number }[], + showHeader?: boolean, cursor?: string ) { this.send({ @@ -1224,6 +1225,9 @@ class QuadraticCore { x, y, name, + alternatingColors, + columns, + showHeader, cursor: cursor || '', }); } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 782e1c680b..0b60393194 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -954,6 +954,7 @@ class Core { line_number: null, output_display_type: null, cancel_compute: true, + chart_output: null, }; this.gridController.calculationComplete(JSON.stringify(codeResult)); } @@ -1137,6 +1138,7 @@ class Core { name?: string, alternatingColors?: boolean, columns?: { name: string; display: boolean; valueIndex: number }[], + showHeader?: boolean, cursor?: string ) { if (!this.gridController) throw new Error('Expected gridController to be defined'); @@ -1146,6 +1148,7 @@ class Core { name, alternatingColors, JSON.stringify(columns), + showHeader, cursor ); } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index 599973d115..72c64c08a0 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -601,6 +601,7 @@ class CoreClient { e.data.name, e.data.alternatingColors, e.data.columns, + e.data.showHeader, e.data.cursor ); return; From c1b912568278996ef890a0f16e2929ca53029981 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 11:07:56 -0700 Subject: [PATCH 199/373] Rename column_names to columns in JsRenderCodeCell --- quadratic-client/src/app/actions/dataTableSpec.ts | 5 +++++ .../HTMLGrid/contextMenus/TableColumnHeaderRename.tsx | 2 +- .../gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx | 8 ++++---- .../src/app/gridGL/cells/tables/TableColumnHeaders.ts | 4 ++-- quadratic-client/src/app/quadratic-core-types/index.d.ts | 2 +- quadratic-core/src/grid/js_types.rs | 2 +- quadratic-core/src/grid/sheet/rendering.rs | 6 +++--- 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index ed15434511..d5942cddca 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -46,6 +46,11 @@ const getTable = (): JsRenderCodeCell | undefined => { return pixiAppSettings.contextMenu?.table ?? pixiApp.cellsSheet().cursorOnDataTable(); }; +const getColumns = (): { name: string; display: boolean; valueIndex: number }[] => { + const table = getTable(); + return table?.columns ?? []; +}; + const isHeadingShowing = (): boolean => { const table = getTable(); return !!table?.show_header; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx index 802b465bcb..60d16c8999 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -29,7 +29,7 @@ export const TableColumnHeaderRename = () => { if (!contextMenu.table || contextMenu.selectedColumn === undefined) { return; } - return contextMenu.table.column_names[contextMenu.selectedColumn].name; + return contextMenu.table.columns[contextMenu.selectedColumn].name; }, [contextMenu.selectedColumn, contextMenu.table]); if ( diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index d631b1a172..791f56758a 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -35,7 +35,7 @@ export const TableSort = () => { const sort = contextMenu.table.sort ? [...contextMenu.table.sort.filter((item) => item.direction !== 'None')] : []; - if (sort.length !== contextMenu.table.column_names.length) { + if (sort.length !== contextMenu.table.columns.length) { sort.push({ column_index: -1, direction: 'Ascending' }); } setSort(sort); @@ -91,7 +91,7 @@ export const TableSort = () => { }; }, [contextMenu.table]); - const columnNames = useMemo(() => contextMenu.table?.column_names ?? [], [contextMenu.table]); + const columnNames = useMemo(() => contextMenu.table?.columns ?? [], [contextMenu.table]); const availableColumns = useMemo(() => { const availableColumns = columnNames.filter((_, index) => !sort.some((item) => item.column_index === index)); @@ -126,7 +126,7 @@ export const TableSort = () => { setSort((prev) => { const sort = prev.filter((_, i) => i !== index); if ( - sort.length !== contextMenu.table?.column_names.length && + sort.length !== contextMenu.table?.columns.length && sort.length && sort[sort.length - 1].column_index !== -1 ) { @@ -169,7 +169,7 @@ export const TableSort = () => {
Table Sort
{sort.map((entry, index) => { - const name = entry.column_index === -1 ? '' : contextMenu.table?.column_names[entry.column_index]?.name ?? ''; + const name = entry.column_index === -1 ? '' : contextMenu.table?.columns[entry.column_index]?.name ?? ''; const columns = name ? [name, ...availableColumns] : availableColumns; return ( this.table.codeCell.column_names.length) { + while (this.columns.children.length > this.table.codeCell.columns.length) { this.columns.children.pop(); } let x = 0; const codeCell = this.table.codeCell; - codeCell.column_names.forEach((column, index) => { + codeCell.columns.forEach((column, index) => { const width = this.table.sheet.offsets.getColumnWidth(codeCell.x + index); if (index >= this.columns.children.length) { // if this is a new column, then add it diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 3412ce0539..84d3b716d9 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -37,7 +37,7 @@ export interface JsOffset { column: number | null, row: number | null, size: num export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, column_names: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index 80a53124c7..b23702e291 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -201,7 +201,7 @@ pub struct JsRenderCodeCell { pub state: JsRenderCodeCellState, pub spill_error: Option>, pub name: String, - pub column_names: Vec, + pub columns: Vec, pub first_row_header: bool, pub show_header: bool, pub sort: Option>, diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 823df90ef8..66b16c3f97 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -468,7 +468,7 @@ impl Sheet { state, spill_error, name: data_table.name.clone(), - column_names: data_table.send_columns(), + columns: data_table.send_columns(), first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, sort: data_table.sort.clone(), @@ -516,7 +516,7 @@ impl Sheet { state, spill_error, name: data_table.name.clone(), - column_names: data_table.send_columns(), + columns: data_table.send_columns(), first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, sort: data_table.sort.clone(), @@ -1143,7 +1143,7 @@ mod tests { state: crate::grid::js_types::JsRenderCodeCellState::Success, spill_error: None, name: "Table 1".to_string(), - column_names: vec![JsDataTableColumn { + columns: vec![JsDataTableColumn { name: "Column 1".into(), display: true, value_index: 0, From 1b66a612db30eca4f56eda0cdc83f1eacadb4d06 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 11:17:31 -0700 Subject: [PATCH 200/373] Rename column name in the client --- .../src/app/actions/dataTableSpec.ts | 6 +++-- .../contextMenus/TableColumnHeaderRename.tsx | 24 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index d5942cddca..ac0bfe3a6f 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -46,9 +46,9 @@ const getTable = (): JsRenderCodeCell | undefined => { return pixiAppSettings.contextMenu?.table ?? pixiApp.cellsSheet().cursorOnDataTable(); }; -const getColumns = (): { name: string; display: boolean; valueIndex: number }[] => { +export const getColumns = (): { name: string; display: boolean; valueIndex: number }[] | undefined => { const table = getTable(); - return table?.columns ?? []; + return table?.columns; }; const isHeadingShowing = (): boolean => { @@ -193,7 +193,9 @@ export const dataTableSpec: DataTableSpec = { const table = getTable(); if (table) { const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; + console.log(selectedColumn); + if (selectedColumn !== undefined) { setTimeout(() => { const contextMenu = { type: ContextMenuType.TableColumn, rename: true, table, selectedColumn }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx index 60d16c8999..a96840d3fe 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -1,7 +1,9 @@ +import { getColumns } from '@/app/actions/dataTableSpec'; import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { PixiRename } from '@/app/gridGL/HTMLGrid/contextMenus/PixiRename'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { FONT_SIZE } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; @@ -51,10 +53,24 @@ export const TableColumnHeaderRename = () => { color: 'var(--table-column-header-foreground)', backgroundColor: 'var(--table-column-header-background)', }} - onSave={() => { - if (contextMenu.table) { - console.log('TODO: rename column heading'); - // quadraticCore.renameDataTable(contextMenu.table.id, contextMenu.table.name); + onSave={(value: string) => { + if (contextMenu.table && contextMenu.selectedColumn && pixiApp.cellsSheets.current) { + const columns = getColumns(); + + if (columns) { + columns[contextMenu.selectedColumn].name = value; + + quadraticCore.dataTableMeta( + pixiApp.cellsSheets.current?.sheetId, + contextMenu.table.x, + contextMenu.table.y, + undefined, + undefined, + columns, + undefined, + '' + ); + } } }} onClose={() => events.emit('contextMenu', {})} From 247dab8850110c8eb38654d99ebfdb14850c9e52 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 11:29:46 -0700 Subject: [PATCH 201/373] Implement hide column in the UI --- .../src/app/actions/dataTableSpec.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index ac0bfe3a6f..d138e8987c 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -194,8 +194,6 @@ export const dataTableSpec: DataTableSpec = { if (table) { const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; - console.log(selectedColumn); - if (selectedColumn !== undefined) { setTimeout(() => { const contextMenu = { type: ContextMenuType.TableColumn, rename: true, table, selectedColumn }; @@ -248,7 +246,24 @@ export const dataTableSpec: DataTableSpec = { label: 'Hide column', Icon: HideIcon, run: () => { - console.log('TODO: hide column'); + const table = getTable(); + const columns = getColumns(); + const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; + + if (table && columns && selectedColumn) { + columns[selectedColumn].display = false; + + quadraticCore.dataTableMeta( + sheets.sheet.id, + table.x, + table.y, + undefined, + undefined, + columns, + undefined, + sheets.getCursorPosition() + ); + } }, }, [Action.ShowAllColumns]: { From 638945ba4938ace5f709895d340dc9ad517e6bff Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 11:31:38 -0700 Subject: [PATCH 202/373] Implement show all columns in the UI --- .../src/app/actions/dataTableSpec.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index d138e8987c..62399513fc 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -270,7 +270,25 @@ export const dataTableSpec: DataTableSpec = { label: 'Show all columns', Icon: ShowIcon, run: () => { - console.log('TODO: show all columns'); + const table = getTable(); + const columns = getColumns(); + + if (table && columns) { + columns.forEach((column) => { + column.display = true; + } + + quadraticCore.dataTableMeta( + sheets.sheet.id, + table.x, + table.y, + undefined, + undefined, + columns, + undefined, + sheets.getCursorPosition() + ); + } }, }, [Action.EditTableCode]: { From 14f05020c301c4168b06fe50fb6120d3e69a94fc Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 11:34:10 -0700 Subject: [PATCH 203/373] Fix clippy lints --- quadratic-core/src/wasm_bindings/controller/data_table.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index c18475f064..860e5c9dcb 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -92,6 +92,7 @@ impl GridController { Ok(()) } /// Update a Data Table's name + #[allow(clippy::too_many_arguments)] #[wasm_bindgen(js_name = "dataTableMeta")] pub fn js_data_table_meta( &mut self, From 399e870b09859913dcca2f2c308af663e146f0dd Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 11:36:44 -0700 Subject: [PATCH 204/373] Fix prettier lints --- quadratic-client/src/app/actions/dataTableSpec.ts | 4 ++-- .../src/app/gridGL/cells/cellsImages/CellsImage.ts | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 62399513fc..30c0b1ce60 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -276,8 +276,8 @@ export const dataTableSpec: DataTableSpec = { if (table && columns) { columns.forEach((column) => { column.display = true; - } - + }); + quadraticCore.dataTableMeta( sheets.sheet.id, table.x, diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts index 5a60b0afdc..27ed8cda36 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImage.ts @@ -151,11 +151,6 @@ export class CellsImage extends Container { } isImageCell(x: number, y: number): boolean { - return ( - x >= this.gridBounds.x && - x < this.gridBounds.right && - y >= this.gridBounds.y && - y < this.gridBounds.bottom - ); + return x >= this.gridBounds.x && x < this.gridBounds.right && y >= this.gridBounds.y && y < this.gridBounds.bottom; } } From f243cafcc3c2669c08b6aa50671a35d7e7438a7a Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 11:59:55 -0700 Subject: [PATCH 205/373] Add display_buffer to JsRenderCode cell and implement calculateRowIndex() in the client and apply to edit cells --- .../src/app/actions/dataTableSpec.ts | 16 +++++++++++++++- .../src/app/quadratic-core-types/index.d.ts | 2 +- quadratic-core/src/grid/js_types.rs | 1 + quadratic-core/src/grid/sheet/rendering.rs | 3 +++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 30c0b1ce60..96d5619e99 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -51,6 +51,19 @@ export const getColumns = (): { name: string; display: boolean; valueIndex: numb return table?.columns; }; +const calculateRowIndex = (rowIndex: number): number => { + const displayBuffer = getTable()?.display_buffer; + + if (!displayBuffer) { + return rowIndex; + } + + const displayRowIndex = displayBuffer.indexOf(BigInt(rowIndex)); + + // TODO(ddimaria): handle case where row is not in display buffer + return displayRowIndex === -1 ? rowIndex : displayRowIndex; +}; + const isHeadingShowing = (): boolean => { const table = getTable(); return !!table?.show_header; @@ -298,7 +311,8 @@ export const dataTableSpec: DataTableSpec = { const table = getTable(); if (table) { const column = table.x; - const row = table.y; + const row = calculateRowIndex(table.y); + quadraticCore.getCodeCell(sheets.sheet.id, column, row).then((code) => { if (code) { doubleClickCell({ column: Number(code.x), row: Number(code.y), language: code.language, cell: '' }); diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 84d3b716d9..6436bfddc9 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -37,7 +37,7 @@ export interface JsOffset { column: number | null, row: number | null, size: num export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, display_buffer: Array | null, alternating_colors: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index b23702e291..a7043283e5 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -205,6 +205,7 @@ pub struct JsRenderCodeCell { pub first_row_header: bool, pub show_header: bool, pub sort: Option>, + pub display_buffer: Option>, pub alternating_colors: bool, } diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 66b16c3f97..0669ae3fd6 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -472,6 +472,7 @@ impl Sheet { first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, sort: data_table.sort.clone(), + display_buffer: data_table.display_buffer.clone(), alternating_colors: data_table.alternating_colors, }) } @@ -520,6 +521,7 @@ impl Sheet { first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, sort: data_table.sort.clone(), + display_buffer: data_table.display_buffer.clone(), alternating_colors: data_table.alternating_colors, }) } @@ -1151,6 +1153,7 @@ mod tests { first_row_header: false, show_header: true, sort: None, + display_buffer: None, alternating_colors: true, }) ); From d02f543d82173bbb1b6db43ee9e891342c8b65a1 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 13:07:58 -0700 Subject: [PATCH 206/373] Revert adding display_index, peform transmute in rust instead --- .../src/app/actions/dataTableSpec.ts | 16 +--------------- .../execute_operation/execute_data_table.rs | 12 +++++++++++- quadratic-core/src/grid/js_types.rs | 1 - quadratic-core/src/grid/sheet/rendering.rs | 3 --- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 96d5619e99..30c0b1ce60 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -51,19 +51,6 @@ export const getColumns = (): { name: string; display: boolean; valueIndex: numb return table?.columns; }; -const calculateRowIndex = (rowIndex: number): number => { - const displayBuffer = getTable()?.display_buffer; - - if (!displayBuffer) { - return rowIndex; - } - - const displayRowIndex = displayBuffer.indexOf(BigInt(rowIndex)); - - // TODO(ddimaria): handle case where row is not in display buffer - return displayRowIndex === -1 ? rowIndex : displayRowIndex; -}; - const isHeadingShowing = (): boolean => { const table = getTable(); return !!table?.show_header; @@ -311,8 +298,7 @@ export const dataTableSpec: DataTableSpec = { const table = getTable(); if (table) { const column = table.x; - const row = calculateRowIndex(table.y); - + const row = table.y; quadraticCore.getCodeCell(sheets.sheet.id, column, row).then((code) => { if (code) { doubleClickCell({ column: Number(code.x), row: Number(code.y), language: code.language, cell: '' }); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index b9f2f74743..8ffdd373f6 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -120,6 +120,16 @@ impl GridController { let data_table = sheet.data_table_result(data_table_pos)?; + if let Some(display_buffer) = &data_table.display_buffer { + // if there is a display buffer, use it to find the source row index + let row_index = display_buffer + .iter() + .position(|&i| i == pos.y as u64) + .unwrap_or(pos.y as usize); + + pos.y = row_index as i64; + } + if data_table.show_header && !data_table.header_is_first_row { pos.y -= 1; } @@ -127,7 +137,7 @@ impl GridController { let value = values.safe_get(0, 0).cloned()?; let old_value = sheet.get_code_cell_value(pos).unwrap_or(CellValue::Blank); - // sen the new value + // send the new value sheet.set_code_cell_value(pos, value.to_owned()); let data_table_rect = SheetRect::from_numbers( diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index a7043283e5..b23702e291 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -205,7 +205,6 @@ pub struct JsRenderCodeCell { pub first_row_header: bool, pub show_header: bool, pub sort: Option>, - pub display_buffer: Option>, pub alternating_colors: bool, } diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 0669ae3fd6..66b16c3f97 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -472,7 +472,6 @@ impl Sheet { first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, sort: data_table.sort.clone(), - display_buffer: data_table.display_buffer.clone(), alternating_colors: data_table.alternating_colors, }) } @@ -521,7 +520,6 @@ impl Sheet { first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, sort: data_table.sort.clone(), - display_buffer: data_table.display_buffer.clone(), alternating_colors: data_table.alternating_colors, }) } @@ -1153,7 +1151,6 @@ mod tests { first_row_header: false, show_header: true, sort: None, - display_buffer: None, alternating_colors: true, }) ); From 5004a80fd2f0dc59f508ef3c19efd1772472f42f Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 14:37:54 -0700 Subject: [PATCH 207/373] Respect display_buffer when setting data table cell values --- .../execute_operation/execute_data_table.rs | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 8ffdd373f6..45a074320f 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -122,10 +122,13 @@ impl GridController { if let Some(display_buffer) = &data_table.display_buffer { // if there is a display buffer, use it to find the source row index - let row_index = display_buffer - .iter() - .position(|&i| i == pos.y as u64) - .unwrap_or(pos.y as usize); + let row_index = *display_buffer + .get(pos.y as usize) + .unwrap_or(&(pos.y as u64)); + + println!("row_index: {:?}", row_index); + println!("pos.y: {:?}", pos.y); + println!("display_buffer: {:?}", display_buffer); pos.y = row_index as i64; } @@ -136,6 +139,8 @@ impl GridController { let value = values.safe_get(0, 0).cloned()?; let old_value = sheet.get_code_cell_value(pos).unwrap_or(CellValue::Blank); + println!("old_value: {:?}", old_value); + println!("value: {:?}", value); // send the new value sheet.set_code_cell_value(pos, value.to_owned()); @@ -599,7 +604,12 @@ mod tests { // the initial value from the csv assert_data_table_cell_value(&gc, sheet_id, x, y, "MA"); - gc.execute_set_data_table_at(&mut transaction, op).unwrap(); + print_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + + gc.execute_set_data_table_at(&mut transaction, op.clone()) + .unwrap(); + + print_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); // expect the value to be "1" assert_data_table_cell_value(&gc, sheet_id, x, y, "1"); @@ -611,6 +621,23 @@ mod tests { // redo, the value should be "1" again execute_forward_operations(&mut gc, &mut transaction); assert_data_table_cell_value(&gc, sheet_id, x, y, "1"); + + // sort the data table and see if the value is still correct + let sort = vec![DataTableSort { + column_index: 0, + direction: SortDirection::Descending, + }]; + let sort_op = Operation::SortDataTable { + sheet_pos, + sort: Some(sort), + }; + gc.execute_sort_data_table(&mut transaction, sort_op) + .unwrap(); + + print_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + gc.execute_set_data_table_at(&mut transaction, op).unwrap(); + print_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + assert_data_table_cell_value(&gc, sheet_id, x, y, "1"); } #[test] From 38ec43857b33d2974e37cd7e5579e755d0462025 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 5 Nov 2024 14:40:05 -0700 Subject: [PATCH 208/373] Cleanup --- .../execute_operation/execute_data_table.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 45a074320f..b767290e4d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -126,10 +126,6 @@ impl GridController { .get(pos.y as usize) .unwrap_or(&(pos.y as u64)); - println!("row_index: {:?}", row_index); - println!("pos.y: {:?}", pos.y); - println!("display_buffer: {:?}", display_buffer); - pos.y = row_index as i64; } @@ -139,8 +135,6 @@ impl GridController { let value = values.safe_get(0, 0).cloned()?; let old_value = sheet.get_code_cell_value(pos).unwrap_or(CellValue::Blank); - println!("old_value: {:?}", old_value); - println!("value: {:?}", value); // send the new value sheet.set_code_cell_value(pos, value.to_owned()); @@ -604,12 +598,12 @@ mod tests { // the initial value from the csv assert_data_table_cell_value(&gc, sheet_id, x, y, "MA"); - print_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); gc.execute_set_data_table_at(&mut transaction, op.clone()) .unwrap(); - print_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); // expect the value to be "1" assert_data_table_cell_value(&gc, sheet_id, x, y, "1"); @@ -634,9 +628,9 @@ mod tests { gc.execute_sort_data_table(&mut transaction, sort_op) .unwrap(); - print_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); gc.execute_set_data_table_at(&mut transaction, op).unwrap(); - print_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); + print_data_table(&gc, sheet_id, Rect::new(0, 0, 3, 10)); assert_data_table_cell_value(&gc, sheet_id, x, y, "1"); } From 90bb41f62f9ccefde9216e7a9d6875506cae7019 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 6 Nov 2024 05:47:09 -0800 Subject: [PATCH 209/373] move render_size to table --- .vscode/settings.json | 1 + .../HTMLGrid/contextMenus/TableMenu.tsx | 24 ++-- .../src/app/gridGL/cells/tables/Table.ts | 3 + .../interaction/pointer/PointerTable.ts | 14 +- .../src/app/quadratic-core-types/index.d.ts | 2 +- .../worker/javascript/javascript.ts | 3 +- .../worker/javascript/javascriptCompile.ts | 7 +- .../worker/javascript/javascriptOutput.ts | 7 +- .../worker/javascript/javascriptResults.ts | 5 +- .../javascript/javascriptRunnerMessages.ts | 1 + .../web-workers/quadraticCore/worker/core.ts | 13 +- .../quadraticCore/worker/coreClient.ts | 2 +- .../quadraticCore/worker/corePython.ts | 1 + .../quadraticCore/worker/rustConversions.ts | 11 +- .../worker/cellsLabel/CellLabel.ts | 3 +- .../pending_transaction.rs | 2 + quadratic-core/src/controller/dependencies.rs | 1 + .../execution/control_transaction.rs | 1 + .../execute_operation/execute_code.rs | 34 +++++ .../execute_operation/execute_data_table.rs | 1 + .../execute_operation/execute_formats.rs | 23 ++-- .../execution/execute_operation/mod.rs | 1 + .../src/controller/execution/run_code/mod.rs | 26 +++- .../execution/run_code/run_formula.rs | 7 +- .../execution/run_code/run_python.rs | 4 +- .../src/controller/execution/spills.rs | 94 +++++++++++--- .../src/controller/operations/code_cell.rs | 13 ++ .../src/controller/operations/operation.rs | 14 ++ quadratic-core/src/controller/send_render.rs | 51 +++----- .../src/controller/transaction_types.rs | 2 +- .../src/controller/user_actions/code.rs | 11 ++ .../src/controller/user_actions/data_table.rs | 1 + .../src/controller/user_actions/formatting.rs | 71 +---------- quadratic-core/src/grid/data_table/column.rs | 6 +- quadratic-core/src/grid/data_table/mod.rs | 20 ++- .../src/grid/file/serialize/data_table.rs | 2 + quadratic-core/src/grid/file/v1_7/file.rs | 30 ++++- quadratic-core/src/grid/file/v1_8/schema.rs | 1 + quadratic-core/src/grid/formats/format.rs | 4 +- quadratic-core/src/grid/formatting.rs | 7 +- quadratic-core/src/grid/js_types.rs | 6 +- quadratic-core/src/grid/sheet/code.rs | 46 +------ quadratic-core/src/grid/sheet/data_table.rs | 8 +- quadratic-core/src/grid/sheet/rendering.rs | 120 +++++++++++++----- quadratic-core/src/grid/sheet/search.rs | 2 + quadratic-core/src/grid/sheet/sheet_test.rs | 3 + quadratic-core/src/sheet_offsets/mod.rs | 19 +++ .../wasm_bindings/controller/formatting.rs | 28 ++-- quadratic-core/src/wasm_bindings/js.rs | 10 +- 49 files changed, 480 insertions(+), 286 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e32ce15b75..e868bfa695 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "bigdecimal", "bincode", "bindgen", + "cellvalue", "dashmap", "dbgjs", "dcell", diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index bf197ec020..0ad0225ba8 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -34,6 +34,8 @@ export const TableMenu = (props: Props) => { ); }, [codeCell]); + const spillError = codeCell?.spill_error; + if (!codeCell || selectedColumn !== undefined) { return null; } @@ -52,17 +54,19 @@ export const TableMenu = (props: Props) => { )} - {!isImageOrHtmlCell && } + {!isImageOrHtmlCell && !spillError && } - {!isImageOrHtmlCell && hasHiddenColumns && } - {!isImageOrHtmlCell && } - {!isImageOrHtmlCell && } - {!isImageOrHtmlCell && } - {!isImageOrHtmlCell && } - {!isImageOrHtmlCell && } - {!isImageOrHtmlCell && isCodeCell && } - {!isImageOrHtmlCell && } - + {!isImageOrHtmlCell && hasHiddenColumns && !spillError && ( + + )} + {!isImageOrHtmlCell && !spillError && } + {!isImageOrHtmlCell && !spillError && } + {!isImageOrHtmlCell && !spillError && } + {!isImageOrHtmlCell && !spillError && } + {!isImageOrHtmlCell && !spillError && } + {!isImageOrHtmlCell && !spillError && isCodeCell && } + {!isImageOrHtmlCell && !spillError && } + {} ); }; diff --git a/quadratic-client/src/app/gridGL/cells/tables/Table.ts b/quadratic-client/src/app/gridGL/cells/tables/Table.ts index 88beb7838b..a7f9cc134b 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Table.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Table.ts @@ -108,6 +108,9 @@ export class Table extends Container { }; intersectsCursor(x: number, y: number) { + if (this.codeCell.spill_error && (this.codeCell.x !== x || this.codeCell.y !== y)) { + return false; + } const rect = new Rectangle(this.codeCell.x, this.codeCell.y, this.codeCell.w - 1, this.codeCell.h - 1); return intersects.rectanglePoint(rect, { x, y }); } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index 7cd217f756..cc4033d8b9 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -4,6 +4,7 @@ import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; +import { doubleClickCell } from '@/app/gridGL/interaction/pointer/doubleClickCell'; import { DOUBLE_CLICK_TIME } from '@/app/gridGL/interaction/pointer/pointerUtils'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { isMac } from '@/shared/utils/isMac'; @@ -79,8 +80,17 @@ export class PointerTable { if (!tableDown?.table) { const image = pixiApp.cellsSheet().cellsImages.contains(world); if (image) { - pixiApp.cellsSheet().tables.ensureActiveCoordinate(image); - sheets.sheet.cursor.changePosition({ cursorPosition: image }); + if (this.doubleClickTimeout) { + clearTimeout(this.doubleClickTimeout); + this.doubleClickTimeout = undefined; + doubleClickCell({ column: image.x, row: image.y, language: 'Javascript' }); + } else { + pixiApp.cellsSheet().tables.ensureActiveCoordinate(image); + sheets.sheet.cursor.changePosition({ cursorPosition: image }); + this.doubleClickTimeout = window.setTimeout(() => { + this.doubleClickTimeout = undefined; + }, DOUBLE_CLICK_TIME); + } return true; } return false; diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index a5b07a863c..77c87592ec 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -28,7 +28,7 @@ export interface JsBordersSheet { all: BorderStyleCell | null, columns: Record | null, return_info: JsReturnInfo | null, cells_accessed: Array | null, } -export interface JsCodeResult { transaction_id: string, success: boolean, std_out: string | null, std_err: string | null, line_number: number | null, output_value: Array | null, output_array: Array>> | null, output_display_type: string | null, cancel_compute: boolean | null, } +export interface JsCodeResult { transaction_id: string, success: boolean, std_out: string | null, std_err: string | null, line_number: number | null, output_value: Array | null, output_array: Array>> | null, output_display_type: string | null, cancel_compute: boolean | null, chart_pixel_output: [number, number] | null, } export interface JsDataTableColumn { name: string, display: boolean, valueIndex: number, } export interface JsGetCellResponse { x: bigint, y: bigint, value: string, type_name: string, } export interface JsHtmlOutput { sheet_id: string, x: bigint, y: bigint, html: string | null, w: string | null, h: string | null, } 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..697bb85fd0 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 @@ -122,7 +122,8 @@ export class Javascript { message.y, e.data.results, e.data.console, - e.data.lineNumber + e.data.lineNumber, + e.data.chartPixelOutput ); this.state = 'ready'; setTimeout(this.next, 0); 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..c662f9f125 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,6 +5,8 @@ //! numbers to all return statements via a caught thrown error (the only way to //! get line numbers in JS). +// todo: can remove the line number vars as we are no longer using them. + import * as esbuild from 'esbuild-wasm'; import { LINE_NUMBER_VAR } from './javascript'; import { javascriptLibrary } from './runner/generateJavascriptForRunner'; @@ -77,10 +79,11 @@ export function prepareJavascriptCode( 'let results = await (async () => {' + code + '\n })();' + - 'if (results instanceof OffscreenCanvas) results = await results.convertToBlob();' + + 'let chartPixelOutput = undefined;' + + 'if (results instanceof OffscreenCanvas) { chartPixelOutput = [results.width, results.height]; results = await results.convertToBlob(); }' + `self.postMessage({ type: "results", results, console: javascriptConsole.output()${ withLineNumbers ? `, lineNumber: Math.max(${LINE_NUMBER_VAR} - 1, 0)` : '' - } });` + + }, chartPixelOutput });` + `} catch (e) { const error = e.message; const stack = e.stack; self.postMessage({ type: "error", error, stack, console: javascriptConsole.output() }); }` + '})();'; return compiledCode; diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptOutput.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptOutput.ts index 42c95e8ee4..04f7136942 100644 --- a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptOutput.ts +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptOutput.ts @@ -9,7 +9,7 @@ export function javascriptConvertOutputType( row: number, x?: number, y?: number -): { output: [string, string]; displayType: string } | null { +): { output: [string, string]; displayType: string; chartPixelOutput?: [number, number] } | null { if (Array.isArray(value) && value.flat().length !== 0) { return null; } @@ -84,8 +84,9 @@ export function javascriptConvertOutputArray( message: string[], value: any, column: number, - row: number -): { output: [string, string][][]; displayType: string } | null { + row: number, + chartPixelOutput?: [number, number] +): { output: [string, string][][]; displayType: string; chartPixelOutput?: [number, number] } | null { if (!Array.isArray(value) || value.length === 0 || value.flat().length === 0) { return null; } diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptResults.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptResults.ts index 0d8a2a9d27..dd2d1f8171 100644 --- a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptResults.ts +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptResults.ts @@ -16,6 +16,7 @@ export function javascriptErrorResult(transactionId: string, message: string, li line_number: lineNumber ?? null, output_display_type: null, cancel_compute: false, + chart_pixel_output: null, }; javascriptCore.sendJavascriptResults(transactionId, codeResult); javascriptClient.sendState('ready'); @@ -27,7 +28,8 @@ export function javascriptResults( y: number, result: any, consoleOutput: string, - lineNumber?: number + lineNumber?: number, + chartPixelOutput?: [number, number] ) { const message: string[] = []; const outputType = javascriptConvertOutputType(message, result, x, y); @@ -46,6 +48,7 @@ export function javascriptResults( output_display_type: outputType?.displayType || outputArray?.displayType || null, cancel_compute: false, + chart_pixel_output: chartPixelOutput || null, }; javascriptCore.sendJavascriptResults(transactionId, codeResult); javascriptClient.sendState('ready', { current: undefined }); diff --git a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptRunnerMessages.ts b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptRunnerMessages.ts index 18deaed9f8..5fe414cffd 100644 --- a/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptRunnerMessages.ts +++ b/quadratic-client/src/app/web-workers/javascriptWebWorker/worker/javascript/javascriptRunnerMessages.ts @@ -26,6 +26,7 @@ export interface RunnerJavascriptResults { results: any; console: string; lineNumber?: number; + chartPixelOutput?: [number, number]; } export interface RunnerJavascriptError { diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 782e1c680b..8e22823493 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -44,7 +44,7 @@ import { import { coreClient } from './coreClient'; import { coreRender } from './coreRender'; import { offline } from './offline'; -import { numbersToRectStringified, pointsToRect, posToPos, posToRect } from './rustConversions'; +import { numbersToRectStringified, pointsToRect, posToPos, posToRect, toSheetPos } from './rustConversions'; // Used to coerce bigints to numbers for JSON.stringify; see // https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-2064279949. @@ -777,17 +777,11 @@ class Core { }); } - setCellRenderSize(sheetId: string, x: number, y: number, width: number, height: number, cursor: string) { + setChartSize(sheetId: string, x: number, y: number, width: number, height: number, cursor: string) { return new Promise((resolve) => { this.clientQueue.push(() => { if (!this.gridController) throw new Error('Expected gridController to be defined'); - this.gridController.setCellRenderSize( - sheetId, - numbersToRectStringified(x, y, 1, 1), - width.toString(), - height.toString(), - cursor - ); + this.gridController.setChartSize(toSheetPos(x, y, sheetId), width, height, cursor); resolve(undefined); }); }); @@ -954,6 +948,7 @@ class Core { line_number: null, output_display_type: null, cancel_compute: true, + chart_pixel_output: null, }; this.gridController.calculationComplete(JSON.stringify(codeResult)); } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index 599973d115..3cded57699 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -385,7 +385,7 @@ class CoreClient { return; case 'clientCoreSetCellRenderResize': - await core.setCellRenderSize(e.data.sheetId, e.data.x, e.data.y, e.data.width, e.data.height, e.data.cursor); + await core.setChartSize(e.data.sheetId, e.data.x, e.data.y, e.data.width, e.data.height, e.data.cursor); return; case 'clientCoreAutocomplete': diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/corePython.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/corePython.ts index 972e17e876..6ef85df67c 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/corePython.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/corePython.ts @@ -54,6 +54,7 @@ class CorePython { line_number: results.lineno ?? null, output_display_type: results.output_type ?? null, cancel_compute: false, + chart_pixel_output: null, //results.chart_pixel_output ?? null, }; core.calculationComplete(codeResult); diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/rustConversions.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/rustConversions.ts index 65cba017bf..39454f3df4 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/rustConversions.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/rustConversions.ts @@ -3,7 +3,7 @@ */ import { RectangleLike } from '@/app/grid/sheet/SheetCursor'; -import { Pos, Rect, SheetRect } from '@/app/quadratic-core-types'; +import { Pos, Rect, SheetPos, SheetRect } from '@/app/quadratic-core-types'; import { Point, Rectangle } from 'pixi.js'; // Used to coerce bigints to numbers for JSON.stringify; see @@ -80,3 +80,12 @@ export function rectToSheetRect(rectangle: Rectangle, sheetId: string): SheetRec sheet_id: { id: sheetId }, }; } + +export function toSheetPos(x: number, y: number, sheetId: string): string { + const sheetPos: SheetPos = { + x: BigInt(x), + y: BigInt(y), + sheet_id: { id: sheetId }, + }; + return JSON.stringify(sheetPos, bigIntReplacer); +} diff --git a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts index 0ccb5e55e1..7093b28d4d 100644 --- a/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts +++ b/quadratic-client/src/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel.ts @@ -42,7 +42,6 @@ export const OPEN_SANS_FIX = { x: 1.8, y: -1.8 }; const SPILL_ERROR_TEXT = ' #SPILL'; const RUN_ERROR_TEXT = ' #ERROR'; -const CHART_TEXT = ' CHART'; // values based on line position and thickness in monaco editor const HORIZONTAL_LINE_THICKNESS = 1; @@ -131,7 +130,7 @@ export class CellLabel { case 'RunError': return RUN_ERROR_TEXT; case 'Chart': - return CHART_TEXT; + return ''; default: if (cell.value !== undefined && cell.number) { this.number = cell.number; diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index 872839fb9b..6f6c061c41 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -506,6 +506,7 @@ mod tests { false, false, true, + None, ); transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); @@ -530,6 +531,7 @@ mod tests { false, false, true, + None, ); transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); assert_eq!(transaction.code_cells.len(), 1); diff --git a/quadratic-core/src/controller/dependencies.rs b/quadratic-core/src/controller/dependencies.rs index 6ad2b91f38..39e9c4c23b 100644 --- a/quadratic-core/src/controller/dependencies.rs +++ b/quadratic-core/src/controller/dependencies.rs @@ -84,6 +84,7 @@ mod test { false, false, true, + None, )), ); let sheet_pos_02 = SheetPos { diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index a0ba1dac6d..e3484857f4 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -325,6 +325,7 @@ impl GridController { false, true, true, + None, ); self.finalize_code_run(&mut transaction, current_sheet_pos, Some(data_table), None); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs index 9f2bc7499a..ab639f68b5 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs @@ -53,6 +53,40 @@ impl GridController { } } + pub(super) fn execute_set_chart_size( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) { + if let Operation::SetChartSize { + sheet_pos, + pixel_width, + pixel_height, + } = op + { + if let Some(sheet) = self.try_sheet(sheet_pos.sheet_id) { + if let Some((index, (_, dt))) = + sheet.data_tables.iter().enumerate().find(|(_, (pos, _))| { + **pos + == Pos { + x: sheet_pos.x, + y: sheet_pos.y, + } + }) + { + let mut new_data_table = dt.clone(); + new_data_table.chart_pixel_output = Some((pixel_width, pixel_height)); + self.finalize_code_run( + transaction, + sheet_pos, + Some(new_data_table), + Some(index), + ); + } + } + } + } + pub(super) fn execute_compute_code( &mut self, transaction: &mut PendingTransaction, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index b00e11c449..9c45131d55 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -633,6 +633,7 @@ mod tests { false, false, true, + None, ); let mut gc = GridController::test(); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs index 0a0c0f0b4d..fbbdd15eaf 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_formats.rs @@ -183,11 +183,11 @@ mod test { use super::*; use crate::wasm_bindings::js::expect_js_call; - use crate::{CellValue, CodeCellValue, Pos, SheetRect, Value}; + use crate::{CellValue, CodeCellValue, Pos, SheetPos, Value}; #[test] #[serial] - fn execute_set_formats_render_size() { + fn test_execute_set_chart_size() { let mut gc = GridController::test(); let sheet_id = gc.sheet_ids()[0]; let sheet = gc.sheet_mut(sheet_id); @@ -219,15 +219,18 @@ mod test { false, false, true, + None, )), ); - gc.set_cell_render_size( - SheetRect::from_numbers(0, 0, 1, 1, sheet_id), - Some(RenderSize { - w: "1".to_string(), - h: "2".to_string(), - }), + gc.set_chart_size( + SheetPos { + x: 0, + y: 0, + sheet_id, + }, + 1.0, + 2.0, None, ); let args = format!( @@ -236,8 +239,8 @@ mod test { 0, 0, true, - Some("1".to_string()), - Some("2".to_string()) + Some(1.0), + Some(2.0) ); expect_js_call("jsSendImage", args, true); } diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 9213bf58fb..4b38e4d95c 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -32,6 +32,7 @@ impl GridController { match op { Operation::SetCellValues { .. } => self.execute_set_cell_values(transaction, op), Operation::SetCodeRun { .. } => self.execute_set_code_run(transaction, op), + Operation::SetChartSize { .. } => self.execute_set_chart_size(transaction, op), Operation::SetDataTableAt { .. } => Self::handle_execution_operation_result( self.execute_set_data_table_at(transaction, op), ), diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 94642624a8..67881accfd 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -18,7 +18,7 @@ impl GridController { &mut self, transaction: &mut PendingTransaction, sheet_pos: SheetPos, - new_data_table: Option, + mut new_data_table: Option, index: Option, ) { let sheet_id = sheet_pos.sheet_id; @@ -37,6 +37,16 @@ impl GridController { .unwrap_or(sheet.data_tables.len()), ); + if let Some(new_data_table) = new_data_table.as_mut() { + if let Some((pixel_width, pixel_height)) = new_data_table.chart_pixel_output { + let chart_output = + sheet + .offsets + .calculate_grid_size(pos, pixel_width, pixel_height); + new_data_table.chart_output = Some(chart_output); + } + } + let old_data_table = if let Some(new_data_table) = &new_data_table { let (old_index, old_data_table) = sheet.data_tables.insert_full(pos, new_data_table.clone()); @@ -240,6 +250,7 @@ impl GridController { false, false, false, + None, ); transaction.waiting_for_async = None; self.finalize_code_run(transaction, sheet_pos, Some(new_data_table), None); @@ -279,7 +290,6 @@ impl GridController { std_err: None, cells_accessed: transaction.cells_accessed.clone(), }; - // todo: this should be true sometimes... let show_header = false; return DataTable::new( DataTableKind::CodeRun(code_run), @@ -288,6 +298,7 @@ impl GridController { false, false, show_header, + None, ); }; @@ -350,14 +361,21 @@ impl GridController { // todo: this should be true sometimes... let show_header = false; - let data_table = DataTable::new( + let mut data_table = DataTable::new( DataTableKind::CodeRun(code_run), table_name, value, false, false, show_header, + js_code_result.chart_pixel_output, ); + + // set alternating colors to false if chart_pixel_output is set. + if js_code_result.chart_pixel_output.is_some() { + data_table.alternating_colors = false; + } + transaction.cells_accessed.clear(); data_table } @@ -416,6 +434,7 @@ mod test { false, false, true, + None, ); gc.finalize_code_run(transaction, sheet_pos, Some(new_data_table.clone()), None); assert_eq!(transaction.forward_operations.len(), 1); @@ -451,6 +470,7 @@ mod test { false, false, true, + None, ); gc.finalize_code_run(transaction, sheet_pos, Some(new_data_table.clone()), None); assert_eq!(transaction.forward_operations.len(), 1); diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index 30d1c0a951..d7551cded6 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -39,6 +39,7 @@ impl GridController { false, false, false, + None, ); self.finalize_code_run(transaction, sheet_pos, Some(new_data_table), None); } @@ -265,7 +266,8 @@ mod test { Value::Single(CellValue::Number(12.into())), false, false, - false + false, + None, ) .with_last_modified(result.last_modified), ); @@ -334,7 +336,8 @@ mod test { Value::Array(array), false, false, - false + false, + None, ) .with_last_modified(result.last_modified), ); diff --git a/quadratic-core/src/controller/execution/run_code/run_python.rs b/quadratic-core/src/controller/execution/run_code/run_python.rs index 45eeb81cbc..247a15b18c 100644 --- a/quadratic-core/src/controller/execution/run_code/run_python.rs +++ b/quadratic-core/src/controller/execution/run_code/run_python.rs @@ -544,7 +544,7 @@ mod tests { line_number: None, output_display_type: None, cancel_compute: None, - chart_output: None, + chart_pixel_output: None, }); assert!(result.is_ok()); @@ -586,7 +586,7 @@ mod tests { line_number: None, output_display_type: None, cancel_compute: None, - chart_output: None, + chart_pixel_output: None, }); assert!(result.is_ok()); diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index e8551a341d..aa959ac95e 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -4,7 +4,7 @@ use crate::controller::active_transactions::pending_transaction::PendingTransact use crate::controller::operations::operation::Operation; use crate::controller::GridController; use crate::grid::SheetId; -use crate::{ArraySize, Rect}; +use crate::{ArraySize, Pos, Rect}; impl GridController { /// Changes the spill error for a code_cell and adds necessary operations @@ -16,9 +16,12 @@ impl GridController { spill_error: bool, send_client: bool, ) { + let mut code_cell_pos: Option = None; + let mut is_image = false; // change the spill for the first code_cell and then iterate the later code_cells. if let Some(sheet) = self.grid.try_sheet_mut(sheet_id) { if let Some((pos, run)) = sheet.data_tables.get_index_mut(index) { + code_cell_pos = Some(*pos); let sheet_pos = pos.to_sheet_pos(sheet.id); transaction.reverse_operations.push(Operation::SetCodeRun { sheet_pos, @@ -26,34 +29,35 @@ impl GridController { index, }); run.spill_error = spill_error; + is_image = run.is_image(); transaction.forward_operations.push(Operation::SetCodeRun { sheet_pos, code_run: Some(run.to_owned()), index, }); - - if (cfg!(target_family = "wasm") || cfg!(test)) - && !transaction.is_server() - && send_client + } + } + if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() && send_client { + if let (Some(pos), Some(sheet)) = (code_cell_pos, self.grid.try_sheet(sheet_id)) { + if let (Some(code_cell), Some(render_code_cell)) = + (sheet.edit_code_value(pos), sheet.get_render_code_cell(pos)) { - if let (Some(code_cell), Some(render_code_cell)) = ( - sheet.edit_code_value(sheet_pos.into()), - sheet.get_render_code_cell(sheet_pos.into()), + if let (Ok(code_cell), Ok(render_code_cell)) = ( + serde_json::to_string(&code_cell), + serde_json::to_string(&render_code_cell), ) { - if let (Ok(code_cell), Ok(render_code_cell)) = ( - serde_json::to_string(&code_cell), - serde_json::to_string(&render_code_cell), - ) { - crate::wasm_bindings::js::jsUpdateCodeCell( - sheet_id.to_string(), - sheet_pos.x, - sheet_pos.y, - Some(code_cell), - Some(render_code_cell), - ); - } + crate::wasm_bindings::js::jsUpdateCodeCell( + sheet_id.to_string(), + pos.x, + pos.y, + Some(code_cell), + Some(render_code_cell), + ); } } + if !spill_error && is_image { + sheet.send_image(pos); + } } } } @@ -112,6 +116,7 @@ mod tests { use serial_test::{parallel, serial}; use crate::controller::active_transactions::pending_transaction::PendingTransaction; + use crate::controller::transaction_types::JsCodeResult; use crate::controller::GridController; use crate::grid::js_types::{JsNumber, JsRenderCell, JsRenderCellSpecial}; use crate::grid::{CellAlign, CellWrap, CodeCellLanguage, CodeRun, DataTable, DataTableKind}; @@ -445,9 +450,58 @@ mod tests { false, false, true, + None, ); let pos = Pos { x: 0, y: 0 }; let sheet = gc.sheet_mut(sheet_id); sheet.set_data_table(pos, Some(data_table.clone())); } + + #[test] + #[parallel] + fn test_spill_from_js_chart() { + let mut gc = GridController::default(); + let sheet_id = gc.grid.sheet_ids()[0]; + gc.set_cell_value( + SheetPos { + x: 1, + y: 2, + sheet_id, + }, + "hello".to_string(), + None, + ); + gc.set_code_cell( + SheetPos { + x: 1, + y: 1, + sheet_id, + }, + CodeCellLanguage::Javascript, + "".into(), + None, + ); + let transaction_id = gc.last_transaction().unwrap().id; + let result = JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + chart_pixel_output: Some((100.0, 100.0)), + ..Default::default() + }; + gc.calculation_complete(result).unwrap(); + + let sheet = gc.sheet(sheet_id); + + let render_cells = sheet.get_render_cells(Rect::single_pos(Pos { x: 1, y: 1 })); + assert_eq!( + render_cells, + vec![JsRenderCell { + x: 1, + y: 1, + language: Some(CodeCellLanguage::Javascript), + special: Some(JsRenderCellSpecial::SpillError), + ..Default::default() + }] + ); + } } diff --git a/quadratic-core/src/controller/operations/code_cell.rs b/quadratic-core/src/controller/operations/code_cell.rs index de091ddb08..228c87ce48 100644 --- a/quadratic-core/src/controller/operations/code_cell.rs +++ b/quadratic-core/src/controller/operations/code_cell.rs @@ -148,6 +148,19 @@ impl GridController { pub fn rerun_code_cell_operations(&self, sheet_pos: SheetPos) -> Vec { vec![Operation::ComputeCode { sheet_pos }] } + + pub fn set_chart_size_operations( + &self, + sheet_pos: SheetPos, + width: f32, + height: f32, + ) -> Vec { + vec![Operation::SetChartSize { + sheet_pos, + pixel_width: width, + pixel_height: height, + }] + } } #[cfg(test)] diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 5287c84603..892ab01fd9 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -42,6 +42,11 @@ pub enum Operation { code_run: Option, index: usize, }, + SetChartSize { + sheet_pos: SheetPos, + pixel_width: f32, + pixel_height: f32, + }, SetDataTableAt { sheet_pos: SheetPos, values: CellValues, @@ -406,6 +411,15 @@ impl fmt::Display for Operation { "InsertRow {{ sheet_id: {sheet_id}, row: {row}, copy_formats: {copy_formats:?} }}" ) } + Operation::SetChartSize { + sheet_pos, + pixel_width, + pixel_height, + } => write!( + fmt, + "SetChartSize {{ sheet_pos: {}, pixel_width: {}, pixel_height: {} }}", + sheet_pos, pixel_width, pixel_height + ), } } } diff --git a/quadratic-core/src/controller/send_render.rs b/quadratic-core/src/controller/send_render.rs index d6f7c34a8c..df3a2a8b80 100644 --- a/quadratic-core/src/controller/send_render.rs +++ b/quadratic-core/src/controller/send_render.rs @@ -5,13 +5,13 @@ use itertools::Itertools; use crate::{ grid::{ js_types::{JsOffset, JsPos, JsRenderFill}, - RenderSize, SheetId, + SheetId, }, renderer_constants::{CELL_SHEET_HEIGHT, CELL_SHEET_WIDTH}, selection::Selection, viewport::ViewportBuffer, wasm_bindings::controller::sheet_info::{SheetBounds, SheetInfo}, - CellValue, Pos, Rect, SheetPos, SheetRect, + Pos, Rect, SheetPos, SheetRect, }; use super::{active_transactions::pending_transaction::PendingTransaction, GridController}; @@ -359,30 +359,7 @@ impl GridController { pub fn send_image(&self, sheet_pos: SheetPos) { if cfg!(target_family = "wasm") || cfg!(test) { if let Some(sheet) = self.try_sheet(sheet_pos.sheet_id) { - let image = sheet.data_table(sheet_pos.into()).and_then(|code_run| { - code_run - .cell_value_at(0, 0) - .and_then(|cell_value| match cell_value { - CellValue::Image(image) => Some(image.clone()), - _ => None, - }) - }); - let (w, h) = if let Some(size) = - sheet.get_formatting_value::(sheet_pos.into()) - { - (Some(size.w), Some(size.h)) - } else { - (None, None) - }; - - crate::wasm_bindings::js::jsSendImage( - sheet_pos.sheet_id.to_string(), - sheet_pos.x as i32, - sheet_pos.y as i32, - image, - w, - h, - ); + sheet.send_image(sheet_pos.into()); } } } @@ -401,11 +378,11 @@ mod test { validation::Validation, validation_rules::{validation_logical::ValidationLogical, ValidationRule}, }, - RenderSize, SheetId, + SheetId, }, selection::Selection, wasm_bindings::js::{clear_js_calls, expect_js_call, expect_js_call_count, hash_test}, - Pos, Rect, + Pos, Rect, SheetPos, }; use serial_test::serial; use std::collections::HashSet; @@ -613,12 +590,14 @@ mod test { ..Default::default() }); - gc.set_cell_render_size( - (0, 0, 1, 1, sheet_id).into(), - Some(RenderSize { - w: "1".to_string(), - h: "2".to_string(), - }), + gc.set_chart_size( + SheetPos { + x: 0, + y: 0, + sheet_id, + }, + 1.0, + 2.0, None, ); @@ -629,8 +608,8 @@ mod test { x: 0, y: 0, html: Some("".to_string()), - w: Some("1".to_string()), - h: Some("2".to_string()), + w: Some(1.0), + h: Some(2.0), }) .unwrap(), true, diff --git a/quadratic-core/src/controller/transaction_types.rs b/quadratic-core/src/controller/transaction_types.rs index 942dac8030..e0ffabb885 100644 --- a/quadratic-core/src/controller/transaction_types.rs +++ b/quadratic-core/src/controller/transaction_types.rs @@ -13,7 +13,7 @@ pub struct JsCodeResult { pub output_array: Option>>>, pub output_display_type: Option, pub cancel_compute: Option, - pub chart_output: Option<(u32, u32)>, + pub chart_pixel_output: Option<(f32, f32)>, } #[derive(Debug, Serialize, Deserialize, TS)] diff --git a/quadratic-core/src/controller/user_actions/code.rs b/quadratic-core/src/controller/user_actions/code.rs index b366953090..12a7c1d7d2 100644 --- a/quadratic-core/src/controller/user_actions/code.rs +++ b/quadratic-core/src/controller/user_actions/code.rs @@ -34,6 +34,17 @@ impl GridController { let ops = self.rerun_code_cell_operations(sheet_pos); self.start_user_transaction(ops, cursor, TransactionName::RunCode); } + + pub fn set_chart_size( + &mut self, + sheet_pos: SheetPos, + width: f32, + height: f32, + cursor: Option, + ) { + let ops = self.set_chart_size_operations(sheet_pos, width, height); + self.start_user_transaction(ops, cursor, TransactionName::SetFormats); + } } #[cfg(test)] diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index 07d195eb28..6e5ed1b4b5 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -122,6 +122,7 @@ mod tests { false, false, true, + None, ); let mut gc = GridController::test(); diff --git a/quadratic-core/src/controller/user_actions/formatting.rs b/quadratic-core/src/controller/user_actions/formatting.rs index e9dbde1e30..9d600b1c38 100644 --- a/quadratic-core/src/controller/user_actions/formatting.rs +++ b/quadratic-core/src/controller/user_actions/formatting.rs @@ -4,7 +4,7 @@ use crate::controller::GridController; use crate::grid::formatting::CellFmtArray; use crate::grid::{ Bold, CellAlign, CellFmtAttr, CellVerticalAlign, CellWrap, FillColor, Italic, NumericDecimals, - NumericFormat, RenderSize, StrikeThrough, TextColor, Underline, + NumericFormat, StrikeThrough, TextColor, Underline, }; use crate::{RunLengthEncoding, SheetPos, SheetRect}; @@ -61,18 +61,6 @@ impl GridController { let ops = self.toggle_commas_operations(source, sheet_rect); self.start_user_transaction(ops, cursor, TransactionName::SetFormats); } - - pub fn set_cell_render_size( - &mut self, - sheet_rect: SheetRect, - value: Option, - cursor: Option, - ) { - let attr = CellFmtArray::RenderSize(RunLengthEncoding::repeat(value, sheet_rect.len())); - let ops = vec![Operation::SetCellFormats { sheet_rect, attr }]; - self.start_user_transaction(ops, cursor, TransactionName::SetFormats); - self.send_html_output_rect(&sheet_rect); - } } macro_rules! impl_set_cell_fmt_method { @@ -105,8 +93,6 @@ impl_set_cell_fmt_method!(set_cell_fill_color(CellFmtArray::FillColor impl_set_cell_fmt_method!(set_cell_underline(CellFmtArray::Underline)); impl_set_cell_fmt_method!(set_cell_strike_through(CellFmtArray::StrikeThrough)); -// impl_set_cell_fmt_method!(set_cell_render_size(CellFmtArray::RenderSize)); - #[cfg(test)] mod test { use serial_test::parallel; @@ -115,7 +101,7 @@ mod test { use crate::grid::formats::format::Format; use crate::grid::formats::format_update::FormatUpdate; use crate::grid::js_types::{JsNumber, JsRenderCell}; - use crate::grid::{CellAlign, RenderSize, SheetId, TextColor}; + use crate::grid::{CellAlign, SheetId, TextColor}; use crate::{Pos, Rect, SheetPos, SheetRect}; #[test] @@ -436,57 +422,4 @@ mod test { None, ); } - - #[test] - #[parallel] - fn test_set_output_size() { - let mut gc = GridController::test(); - let sheet_id = gc.sheet_ids()[0]; - gc.set_cell_render_size( - SheetRect::single_pos(Pos { x: 0, y: 0 }, sheet_id), - Some(RenderSize { - w: "1".to_string(), - h: "2".to_string(), - }), - None, - ); - } - - #[test] - #[parallel] - fn test_set_cell_render_size() { - let mut gc = GridController::test(); - let sheet_id = gc.sheet_ids()[0]; - gc.set_cell_render_size( - SheetRect::single_pos(Pos { x: 0, y: 0 }, sheet_id), - Some(RenderSize { - w: "1".to_string(), - h: "2".to_string(), - }), - None, - ); - - let sheet = gc.sheet(sheet_id); - assert_eq!( - sheet.get_formatting_value::(Pos { x: 0, y: 0 }), - Some(RenderSize { - w: "1".to_string(), - h: "2".to_string() - }) - ); - - // ensure not found sheet_id fails silently - gc.set_cell_render_size( - SheetRect { - min: Pos { x: 0, y: 0 }, - max: Pos { x: 0, y: 0 }, - sheet_id: SheetId::new(), - }, - Some(RenderSize { - w: "1".to_string(), - h: "2".to_string(), - }), - None, - ); - } } diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index 7cb5ecdfba..9aea715a11 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -186,8 +186,9 @@ pub mod test { // test column headings taken from first row let value = Value::Array(values.clone()); - let mut data_table = DataTable::new(kind.clone(), "Table 1", value, false, true, true) - .with_last_modified(data_table.last_modified); + let mut data_table = + DataTable::new(kind.clone(), "Table 1", value, false, true, true, None) + .with_last_modified(data_table.last_modified); data_table.apply_first_row_as_header(); let expected_columns = vec![ @@ -237,6 +238,7 @@ pub mod test { alternating_colors: true, formats: Default::default(), chart_output: None, + chart_pixel_output: None, }; sheet.set_cell_value( pos, diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 82af628acb..8c90150111 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -1,4 +1,4 @@ -//! CodeRun is the output of a CellValue::Code type +//! CodeRun is the output of a CellValue::Code or CellValue::Import type //! //! This lives in sheet.data_tables. CodeRun is optional within sheet.data_tables for //! any given CellValue::Code type (ie, if it doesn't exist then a run hasn't been @@ -87,6 +87,7 @@ pub struct DataTable { pub formats: TableFormats, // width and height of the chart (html or image) output + pub chart_pixel_output: Option<(f32, f32)>, pub chart_output: Option<(u32, u32)>, } @@ -101,6 +102,7 @@ impl From<(Import, Array, &Grid)> for DataTable { false, false, true, + None, ) } } @@ -116,6 +118,7 @@ impl DataTable { spill_error: bool, header_is_first_row: bool, show_header: bool, + chart_pixel_output: Option<(f32, f32)>, ) -> Self { let readonly = match kind { DataTableKind::CodeRun(_) => true, @@ -137,6 +140,7 @@ impl DataTable { last_modified: Utc::now(), formats: Default::default(), chart_output: None, + chart_pixel_output, }; if header_is_first_row { @@ -250,7 +254,11 @@ impl DataTable { /// Note: this does not take spill_error into account. pub fn output_size(&self) -> ArraySize { if let Some((w, h)) = self.chart_output { - ArraySize::new(w, h).unwrap() + if w == 0 || h == 0 { + ArraySize::_1X1 + } else { + ArraySize::new(w, h).unwrap() + } } else { match &self.value { Value::Array(a) => { @@ -279,6 +287,10 @@ impl DataTable { } } + pub fn is_html_or_image(&self) -> bool { + self.is_html() || self.is_image() + } + /// returns a SheetRect for the output size of a code cell (defaults to 1x1) /// Note: this returns a 1x1 if there is a spill_error. pub fn output_sheet_rect(&self, sheet_pos: SheetPos, ignore_spill: bool) -> SheetRect { @@ -410,6 +422,7 @@ pub mod test { false, false, true, + None, ) .with_last_modified(data_table.last_modified); let expected_array_size = ArraySize::new(4, 5).unwrap(); @@ -445,6 +458,7 @@ pub mod test { false, false, true, + None, ); assert_eq!(data_table.output_size(), ArraySize::_1X1); @@ -478,6 +492,7 @@ pub mod test { false, false, true, + None, ); assert_eq!(data_table.output_size().w.get(), 10); @@ -516,6 +531,7 @@ pub mod test { true, false, true, + None, ); let sheet_pos = SheetPos::from((1, 2, sheet_id)); diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 3b1e5617cb..d019960eb6 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -242,6 +242,7 @@ pub(crate) fn import_data_table_builder( display_buffer: data_table.display_buffer, alternating_colors: data_table.alternating_colors, formats: import_data_table_formats(data_table.formats), + chart_pixel_output: data_table.chart_pixel_output, chart_output: data_table.chart_output, }; @@ -478,6 +479,7 @@ pub(crate) fn export_data_tables( value, alternating_colors: data_table.alternating_colors, formats: export_data_table_formats(data_table.formats), + chart_pixel_output: data_table.chart_pixel_output, chart_output: data_table.chart_output, }; diff --git a/quadratic-core/src/grid/file/v1_7/file.rs b/quadratic-core/src/grid/file/v1_7/file.rs index e82c6a100c..cf91e81392 100644 --- a/quadratic-core/src/grid/file/v1_7/file.rs +++ b/quadratic-core/src/grid/file/v1_7/file.rs @@ -5,8 +5,33 @@ use crate::grid::file::{ v1_8::schema::{self as v1_8}, }; +fn render_size_to_chart_size( + columns: &Vec<(i64, v1_7::ColumnSchema)>, + pos: v1_7::PosSchema, +) -> Option<(f32, f32)> { + columns + .iter() + .find(|(x, _)| *x == pos.x) + .and_then(|(_, column)| { + column.render_size.iter().find_map(|(y, render_size)| { + if let Ok(y) = y.parse::() { + if pos.y >= y && pos.y < y + render_size.len as i64 { + if let (Ok(w), Ok(h)) = ( + render_size.value.w.parse::(), + render_size.value.h.parse::(), + ) { + return Some((w, h)); + } + } + } + None + }) + }) +} + fn upgrade_code_runs( code_runs: Vec<(v1_7::PosSchema, v1_7::CodeRunSchema)>, + columns: &Vec<(i64, v1_7::ColumnSchema)>, ) -> Result> { code_runs .into_iter() @@ -45,6 +70,8 @@ fn upgrade_code_runs( } else { v1_8::OutputValueSchema::Single(v1_8::CellValueSchema::Blank) }; + + let chart_pixel_output = render_size_to_chart_size(columns, pos.clone()); let new_data_table = v1_8::DataTableSchema { kind: v1_8::DataTableKindSchema::CodeRun(new_code_run), name: format!("Table{}", i), @@ -59,6 +86,7 @@ fn upgrade_code_runs( last_modified: code_run.last_modified, alternating_colors: true, formats: Default::default(), + chart_pixel_output, chart_output: None, }; Ok((v1_8::PosSchema::from(pos), new_data_table)) @@ -73,8 +101,8 @@ pub fn upgrade_sheet(sheet: v1_7::SheetSchema) -> Result { color: sheet.color, order: sheet.order, offsets: sheet.offsets, + data_tables: upgrade_code_runs(sheet.code_runs, &sheet.columns)?, columns: sheet.columns, - data_tables: upgrade_code_runs(sheet.code_runs)?, formats_all: sheet.formats_all, formats_columns: sheet.formats_columns, formats_rows: sheet.formats_rows, diff --git a/quadratic-core/src/grid/file/v1_8/schema.rs b/quadratic-core/src/grid/file/v1_8/schema.rs index 3aea9ce75d..b4d075748e 100644 --- a/quadratic-core/src/grid/file/v1_8/schema.rs +++ b/quadratic-core/src/grid/file/v1_8/schema.rs @@ -142,6 +142,7 @@ pub struct DataTableSchema { pub last_modified: Option>, pub alternating_colors: bool, pub formats: TableFormatsSchema, + pub chart_pixel_output: Option<(f32, f32)>, pub chart_output: Option<(u32, u32)>, } diff --git a/quadratic-core/src/grid/formats/format.rs b/quadratic-core/src/grid/formats/format.rs index 5f809b6014..e6a1a3070f 100644 --- a/quadratic-core/src/grid/formats/format.rs +++ b/quadratic-core/src/grid/formats/format.rs @@ -19,10 +19,12 @@ pub struct Format { pub italic: Option, pub text_color: Option, pub fill_color: Option, - pub render_size: Option, pub date_time: Option, pub underline: Option, pub strike_through: Option, + + // deprecated but still used in existing Operations + pub render_size: Option, } impl Format { diff --git a/quadratic-core/src/grid/formatting.rs b/quadratic-core/src/grid/formatting.rs index ac00f812d9..c9328fd17d 100644 --- a/quadratic-core/src/grid/formatting.rs +++ b/quadratic-core/src/grid/formatting.rs @@ -2,6 +2,7 @@ use std::fmt; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; +use ts_rs::TS; use super::block::SameValue; use super::{Column, ColumnData}; @@ -231,8 +232,10 @@ pub struct NumericFormat { pub symbol: Option, } -#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "js", derive(ts_rs::TS))] +// Deprecated. This needs to remain here b/c Format and Formats rely on it and +// it is included in existing Operations. It can only be removed if we rework +// Format and Formats. +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, TS)] /// Measures DOM element size in pixels. pub struct RenderSize { pub w: String, diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index 80a53124c7..1cb72cc18b 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -208,14 +208,14 @@ pub struct JsRenderCodeCell { pub alternating_colors: bool, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub struct JsHtmlOutput { pub sheet_id: String, pub x: i64, pub y: i64, pub html: Option, - pub w: Option, - pub h: Option, + pub w: Option, + pub h: Option, } #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, TS)] diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 01f8b8f324..12304581d8 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -6,7 +6,7 @@ use crate::{ grid::{ data_table::DataTable, js_types::{JsCodeCell, JsReturnInfo}, - CodeCellLanguage, DataTableKind, RenderSize, + CodeCellLanguage, DataTableKind, }, CellValue, Pos, Rect, }; @@ -103,12 +103,6 @@ impl Sheet { }) } - /// returns the render-size for a html-like cell - pub fn render_size(&self, pos: Pos) -> Option { - let column = self.get_column(pos.x)?; - column.render_size.get(pos.y) - } - /// Returns whether a rect overlaps the output of a code cell. /// It will only check code_cells until it finds the data_table at code_pos (since later data_tables do not cause spills in earlier ones) pub fn has_code_cell_in_rect(&self, rect: &Rect, code_pos: Pos) -> bool { @@ -222,45 +216,13 @@ mod test { use super::*; use crate::{ controller::GridController, - grid::{js_types::JsRenderCellSpecial, CodeCellLanguage, CodeRun, RenderSize}, + grid::{js_types::JsRenderCellSpecial, CodeCellLanguage, CodeRun}, Array, CodeCellValue, SheetPos, Value, }; use bigdecimal::BigDecimal; use serial_test::parallel; use std::{collections::HashSet, vec}; - #[test] - #[parallel] - fn test_render_size() { - use crate::Pos; - - let mut gc = GridController::test(); - let sheet_id = gc.sheet_ids()[0]; - gc.set_cell_render_size( - SheetPos { - x: 0, - y: 0, - sheet_id, - } - .into(), - Some(crate::grid::RenderSize { - w: "10".to_string(), - h: "20".to_string(), - }), - None, - ); - - let sheet = gc.sheet(sheet_id); - assert_eq!( - sheet.render_size(Pos { x: 0, y: 0 }), - Some(RenderSize { - w: "10".to_string(), - h: "20".to_string() - }) - ); - assert_eq!(sheet.render_size(Pos { x: 1, y: 1 }), None); - } - #[test] #[parallel] fn test_get_data_table() { @@ -284,6 +246,7 @@ mod test { false, false, true, + None, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!( @@ -324,6 +287,7 @@ mod test { false, false, true, + None, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!( @@ -417,6 +381,7 @@ mod test { false, false, false, + None, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); sheet.set_data_table(Pos { x: 1, y: 1 }, Some(data_table.clone())); @@ -454,6 +419,7 @@ mod test { false, false, false, + None, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); sheet.set_data_table(Pos { x: 1, y: 1 }, Some(data_table.clone())); diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index 0bf1c20161..28cca848bb 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -21,19 +21,19 @@ impl Sheet { Ok(()) } - /// Returns a DatatTable at a Pos + /// Returns a DataTable at a Pos pub fn data_table(&self, pos: Pos) -> Option<&DataTable> { self.data_tables.get(&pos) } - /// Returns a DatatTable at a Pos + /// Returns a DataTable at a Pos pub fn data_table_result(&self, pos: Pos) -> Result<&DataTable> { self.data_tables .get(&pos) .ok_or_else(|| anyhow!("Data table not found at {:?}", pos)) } - /// Returns a mutable DatatTable at a Pos + /// Returns a mutable DataTable at a Pos pub fn data_table_mut(&mut self, pos: Pos) -> Result<&mut DataTable> { self.data_tables .get_mut(&pos) @@ -106,6 +106,7 @@ mod test { false, false, true, + None, ); let old = sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!(old, None); @@ -137,6 +138,7 @@ mod test { false, false, true, + None, ); sheet.set_data_table(Pos { x: 0, y: 0 }, Some(data_table.clone())); assert_eq!( diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 823df90ef8..cc592231c4 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -346,19 +346,15 @@ impl Sheet { if !run.is_html() { return None; } - let (w, h) = if let Some(render_size) = self.render_size(pos) { - (Some(render_size.w), Some(render_size.h)) - } else { - (None, None) - }; + let size = run.chart_pixel_output; let output = run.cell_value_at(0, 0)?; Some(JsHtmlOutput { sheet_id: self.id.to_string(), x: pos.x, y: pos.y, html: Some(output.to_display()), - w, - h, + w: size.map(|(w, _)| w), + h: size.map(|(_, h)| h), }) } @@ -370,18 +366,14 @@ impl Sheet { if !matches!(output, CellValue::Html(_)) { return None; } - let (w, h) = if let Some(render_size) = self.render_size(*pos) { - (Some(render_size.w), Some(render_size.h)) - } else { - (None, None) - }; + let size = run.chart_pixel_output; Some(JsHtmlOutput { sheet_id: self.id.to_string(), x: pos.x, y: pos.y, html: Some(output.to_display()), - w, - h, + w: size.map(|(w, _)| w), + h: size.map(|(_, h)| h), }) }) .collect() @@ -543,23 +535,48 @@ impl Sheet { self.data_tables.iter().for_each(|(pos, run)| { if let Some(CellValue::Image(image)) = run.cell_value_at(0, 0) { - let (w, h) = if let Some(render_size) = self.render_size(*pos) { - (Some(render_size.w), Some(render_size.h)) - } else { - (None, None) - }; + let size = run.chart_pixel_output; crate::wasm_bindings::js::jsSendImage( self.id.to_string(), pos.x as i32, pos.y as i32, Some(image), - w, - h, + size.map(|(w, _)| w), + size.map(|(_, h)| h), ); } }); } + /// Sends an image to the client. + pub fn send_image(&self, pos: Pos) { + let mut sent = false; + if let Some(table) = self.data_table(pos) { + if let Some(CellValue::Image(image)) = table.cell_value_at(0, 0) { + let chart_size = table.chart_pixel_output; + crate::wasm_bindings::js::jsSendImage( + self.id.to_string(), + pos.x as i32, + pos.y as i32, + Some(image), + chart_size.map(|(w, _)| w), + chart_size.map(|(_, h)| h), + ); + sent = true; + } + } + if !sent { + crate::wasm_bindings::js::jsSendImage( + self.id.to_string(), + pos.x as i32, + pos.y as i32, + None, + None, + None, + ); + } + } + /// Sends all validations for this sheet to the client. pub fn send_all_validations(&self) { if let Ok(validations) = serde_json::to_string(&self.validations.validations) { @@ -679,7 +696,7 @@ mod tests { validation::{Validation, ValidationStyle}, validation_rules::{validation_logical::ValidationLogical, ValidationRule}, }, - Bold, CellVerticalAlign, CellWrap, CodeRun, DataTableKind, Italic, RenderSize, + Bold, CellVerticalAlign, CellWrap, CodeRun, DataTableKind, Italic, }, selection::Selection, wasm_bindings::js::{clear_js_calls, expect_js_call, expect_js_call_count, hash_test}, @@ -731,6 +748,7 @@ mod tests { false, false, true, + None, )), ); assert!(sheet.has_render_cells(rect)); @@ -889,17 +907,14 @@ mod tests { h: None, } ); - gc.set_cell_render_size( + gc.set_chart_size( SheetPos { x: 1, y: 2, sheet_id, - } - .into(), - Some(RenderSize { - w: "1".into(), - h: "2".into(), - }), + }, + 1.0, + 2.0, None, ); let sheet = gc.sheet(sheet_id); @@ -912,8 +927,8 @@ mod tests { x: 1, y: 2, html: Some("".to_string()), - w: Some("1".into()), - h: Some("2".into()), + w: Some(1.0), + h: Some(2.0), } ); } @@ -945,6 +960,7 @@ mod tests { false, false, false, + None, ); // render rect is larger than code rect @@ -1001,6 +1017,7 @@ mod tests { false, false, false, + None, ); let code_cells = sheet.get_code_cells( &code_cell, @@ -1128,6 +1145,7 @@ mod tests { false, false, true, + None, ); sheet.set_data_table(pos, Some(data_table)); sheet.set_cell_value(pos, code); @@ -1189,6 +1207,7 @@ mod tests { false, false, false, + None, ); sheet.set_data_table(pos, Some(data_table)); sheet.set_cell_value(pos, code); @@ -1389,4 +1408,43 @@ mod tests { true, ); } + + #[test] + #[serial] + fn test_send_image() { + let mut sheet = Sheet::test(); + let sheet_id = sheet.id; + let pos = (0, 0).into(); + let code_run = CodeRun { + std_out: None, + std_err: None, + formatted_code_string: None, + cells_accessed: HashSet::new(), + error: None, + return_type: Some("image".into()), + line_number: None, + output_type: None, + }; + sheet.set_data_table( + pos, + Some(DataTable::new( + DataTableKind::CodeRun(code_run), + "Table 1", + Value::Single(CellValue::Image("image".to_string())), + false, + false, + true, + None, + )), + ); + sheet.send_image(pos); + expect_js_call( + "jsSendImage", + format!( + "{},{},{},{:?},{:?},{:?}", + sheet_id, pos.x as u32, pos.y as u32, true, None::, None:: + ), + true, + ); + } } diff --git a/quadratic-core/src/grid/sheet/search.rs b/quadratic-core/src/grid/sheet/search.rs index 63b8b03e1f..fa9e29f524 100644 --- a/quadratic-core/src/grid/sheet/search.rs +++ b/quadratic-core/src/grid/sheet/search.rs @@ -512,6 +512,7 @@ mod test { false, false, false, + None, ); sheet.set_data_table(Pos { x: 1, y: 2 }, Some(data_table)); @@ -560,6 +561,7 @@ mod test { false, false, false, + None, ); sheet.set_data_table(Pos { x: 1, y: 2 }, Some(data_table)); diff --git a/quadratic-core/src/grid/sheet/sheet_test.rs b/quadratic-core/src/grid/sheet/sheet_test.rs index 365770014c..39afdbe22b 100644 --- a/quadratic-core/src/grid/sheet/sheet_test.rs +++ b/quadratic-core/src/grid/sheet/sheet_test.rs @@ -76,6 +76,7 @@ impl Sheet { false, false, false, + None, )), ); } @@ -135,6 +136,7 @@ impl Sheet { false, false, false, + None, )), ); } @@ -181,6 +183,7 @@ impl Sheet { false, false, false, + None, )), ); } diff --git a/quadratic-core/src/sheet_offsets/mod.rs b/quadratic-core/src/sheet_offsets/mod.rs index 66cbfd787b..59096aab06 100644 --- a/quadratic-core/src/sheet_offsets/mod.rs +++ b/quadratic-core/src/sheet_offsets/mod.rs @@ -273,12 +273,22 @@ impl SheetOffsets { pub fn delete_row(&mut self, row: i64) -> (Vec<(i64, f64)>, Option) { self.row_heights.delete(row) } + + /// Calculates the grid width and height for a given grid position and pixel size. + pub fn calculate_grid_size(&self, pos: Pos, width: f32, height: f32) -> (u32, u32) { + let start = self.cell_offsets(pos.x, pos.y); + let (end_x, _) = self.column_from_x(start.x + width as f64); + let (end_y, _) = self.row_from_y(start.y + height as f64); + ((end_x - pos.x) as u32, (end_y - pos.y) as u32) + } } #[cfg(test)] mod test { use serial_test::parallel; + use crate::Pos; + #[test] #[parallel] fn screen_rect_cell_offsets() { @@ -316,4 +326,13 @@ mod test { assert_eq!(rect.max.x, 200); assert_eq!(rect.max.y, 42); } + + #[test] + #[parallel] + fn calculate_grid_size() { + let sheet = super::SheetOffsets::default(); + let (width, height) = sheet.calculate_grid_size(Pos { x: 0, y: 0 }, 100.0, 21.0); + assert_eq!(width, 1); + assert_eq!(height, 1); + } } diff --git a/quadratic-core/src/wasm_bindings/controller/formatting.rs b/quadratic-core/src/wasm_bindings/controller/formatting.rs index 9e09cdd15a..b608b8b2f6 100644 --- a/quadratic-core/src/wasm_bindings/controller/formatting.rs +++ b/quadratic-core/src/wasm_bindings/controller/formatting.rs @@ -174,29 +174,17 @@ impl GridController { } /// Sets cell render size (used for Html-style cells). - #[wasm_bindgen(js_name = "setCellRenderSize")] - pub fn js_set_render_size( + #[wasm_bindgen(js_name = "setChartSize")] + pub fn js_set_chart_size( &mut self, - sheet_id: String, - rect: String, - w: Option, - h: Option, + sheet_pos: String, + w: f32, + h: f32, cursor: Option, ) -> Result<(), JsValue> { - let rect = serde_json::from_str::(&rect).map_err(|_| "Invalid rect")?; - let Ok(sheet_id) = SheetId::from_str(&sheet_id) else { - return Result::Err("Invalid sheet id".into()); - }; - let value = if let (Some(w), Some(h)) = (w, h) { - Some(RenderSize { - w: w.to_owned(), - h: h.to_owned(), - }) - } else { - None - }; - - self.set_cell_render_size(rect.to_sheet_rect(sheet_id), value, cursor); + let sheet_pos = + serde_json::from_str::(&sheet_pos).map_err(|_| "Invalid sheet pos")?; + self.set_chart_size(sheet_pos, w, h, cursor); Ok(()) } diff --git a/quadratic-core/src/wasm_bindings/js.rs b/quadratic-core/src/wasm_bindings/js.rs index 87a5b92abc..613b685291 100644 --- a/quadratic-core/src/wasm_bindings/js.rs +++ b/quadratic-core/src/wasm_bindings/js.rs @@ -110,8 +110,8 @@ extern "C" { x: i32, y: i32, image: Option, - w: Option, - h: Option, + w: Option, + h: Option, ); // rows: Vec @@ -572,8 +572,8 @@ pub fn jsSendImage( x: i32, y: i32, image: Option, - w: Option, - h: Option, + w: Option, + h: Option, ) { TEST_ARRAY.lock().unwrap().push(TestFunction::new( "jsSendImage", @@ -584,7 +584,7 @@ pub fn jsSendImage( y, image.is_some(), w, - h + h, ), )); } From dccecaaf31036b021f5a052228146adf608dc78b Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 6 Nov 2024 05:48:55 -0800 Subject: [PATCH 210/373] update compiled file --- .../src/app/quadratic-core-types/index.d.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index b5dcaec718..857a97ff27 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -19,7 +19,7 @@ export interface ColumnRow { column: number, row: number, } export type ConnectionKind = "POSTGRES" | "MYSQL" | "MSSQL" | "SNOWFLAKE"; export type DateTimeRange = { "DateRange": [bigint | null, bigint | null] } | { "DateEqual": Array } | { "DateNotEqual": Array } | { "TimeRange": [number | null, number | null] } | { "TimeEqual": Array } | { "TimeNotEqual": Array }; export interface Duration { months: number, seconds: number, } -export interface Format { align: CellAlign | null, vertical_align: CellVerticalAlign | null, wrap: CellWrap | null, numeric_format: NumericFormat | null, numeric_decimals: number | null, numeric_commas: boolean | null, bold: boolean | null, italic: boolean | null, text_color: string | null, fill_color: string | null, render_size: RenderSize | null, date_time: string | null, underline: boolean | null, strike_through: boolean | null, } +export interface Format { align: CellAlign | null, vertical_align: CellVerticalAlign | null, wrap: CellWrap | null, numeric_format: NumericFormat | null, numeric_decimals: number | null, numeric_commas: boolean | null, bold: boolean | null, italic: boolean | null, text_color: string | null, fill_color: string | null, date_time: string | null, underline: boolean | null, strike_through: boolean | null, render_size: RenderSize | null, } export type GridBounds = { "type": "empty" } | { "type": "nonEmpty" } & Rect; export interface Instant { seconds: number, } export interface JsBorderHorizontal { color: Rgba, line: CellBorderLine, x: bigint, y: bigint, width: bigint, } @@ -28,20 +28,16 @@ export interface JsBordersSheet { all: BorderStyleCell | null, columns: Record | null, return_info: JsReturnInfo | null, cells_accessed: Array | null, } -<<<<<<< HEAD export interface JsCodeResult { transaction_id: string, success: boolean, std_out: string | null, std_err: string | null, line_number: number | null, output_value: Array | null, output_array: Array>> | null, output_display_type: string | null, cancel_compute: boolean | null, chart_pixel_output: [number, number] | null, } -======= -export interface JsCodeResult { transaction_id: string, success: boolean, std_out: string | null, std_err: string | null, line_number: number | null, output_value: Array | null, output_array: Array>> | null, output_display_type: string | null, cancel_compute: boolean | null, chart_output: [number, number] | null, } ->>>>>>> 5004a80fd2f0dc59f508ef3c19efd1772472f42f export interface JsDataTableColumn { name: string, display: boolean, valueIndex: number, } export interface JsGetCellResponse { x: bigint, y: bigint, value: string, type_name: string, } -export interface JsHtmlOutput { sheet_id: string, x: bigint, y: bigint, html: string | null, w: string | null, h: string | null, } +export interface JsHtmlOutput { sheet_id: string, x: bigint, y: bigint, html: string | null, w: number | null, h: number | null, } export interface JsNumber { decimals: number | null, commas: boolean | null, format: NumericFormat | null, } export interface JsOffset { column: number | null, row: number | null, size: number, } export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, display_buffer: Array | null, alternating_colors: boolean, } +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } From ab9069bc04cc816045ef9dd32af4d4bc653016f2 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 6 Nov 2024 05:51:31 -0800 Subject: [PATCH 211/373] fix type issue --- .../app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index 9bd0cae43d..9ef95bab0c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -11,8 +11,8 @@ import { HtmlCellResizing } from './HtmlCellResizing'; // number of screen pixels to trigger the resize cursor const tolerance = 5; -const DEFAULT_HTML_WIDTH = '600'; -const DEFAULT_HTML_HEIGHT = '460'; +const DEFAULT_HTML_WIDTH = 600; +const DEFAULT_HTML_HEIGHT = 460; export class HtmlCell { private right: HTMLDivElement; @@ -58,8 +58,8 @@ export class HtmlCell { this.iframe.style.pointerEvents = 'none'; this.iframe.srcdoc = htmlCell.html; this.iframe.title = `HTML from ${htmlCell.x}, ${htmlCell.y}}`; - this.iframe.width = this.width; - this.iframe.height = this.height; + this.iframe.width = this.width.toString(); + this.iframe.height = this.height.toString(); this.iframe.setAttribute('border', '0'); this.iframe.setAttribute('scrolling', 'no'); this.iframe.style.minWidth = `${CELL_WIDTH}px`; @@ -87,10 +87,10 @@ export class HtmlCell { return Number(this.htmlCell.y); } - private get width(): string { + private get width(): number { return this.htmlCell.w ?? DEFAULT_HTML_WIDTH; } - private get height(): string { + private get height(): number { return this.htmlCell.h ?? DEFAULT_HTML_HEIGHT; } @@ -133,8 +133,8 @@ export class HtmlCell { update(htmlCell: JsHtmlOutput) { if (!htmlCell.html) throw new Error('Expected html to be defined in HtmlCell.update'); if (htmlCell.w !== this.htmlCell.w && htmlCell.h !== this.htmlCell.h) { - this.iframe.width = htmlCell.w ?? DEFAULT_HTML_WIDTH; - this.iframe.height = htmlCell.h ?? DEFAULT_HTML_HEIGHT; + this.iframe.width = (htmlCell.w ?? DEFAULT_HTML_WIDTH).toString(); + this.iframe.height = (htmlCell.h ?? DEFAULT_HTML_HEIGHT).toString(); } if (htmlCell.html !== this.htmlCell.html) { this.iframe.srcdoc = htmlCell.html; From 815e00f6245f07475063902a69863cb425d34b62 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 6 Nov 2024 06:26:40 -0800 Subject: [PATCH 212/373] fix client updates for changes in code_cells, image, and html --- .../src/app/gridGL/cells/tables/Tables.ts | 2 + .../pending_transaction.rs | 35 +++++------ .../execute_operation/execute_col_rows.rs | 8 +-- .../execute_operation/execute_data_table.rs | 2 +- .../execute_operation/execute_values.rs | 2 +- .../src/controller/execution/run_code/mod.rs | 16 ++++- .../src/controller/execution/spills.rs | 62 ++++++------------- quadratic-core/src/grid/sheet/rendering.rs | 23 ++++++- 8 files changed, 78 insertions(+), 72 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 34963e2d62..72fc33699d 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -117,6 +117,7 @@ export class Tables extends Container
{ } else if (renderCodeCell) { this.addChild(new Table(this.sheet, renderCodeCell)); } + pixiApp.setViewportDirty(); } }; @@ -171,6 +172,7 @@ export class Tables extends Container
{ if (sheetId === this.sheet.id) { this.children.map((table) => table.updateCodeCell()); } + pixiApp.setViewportDirty(); }; private changeSheet = (sheetId: string) => { diff --git a/quadratic-core/src/controller/active_transactions/pending_transaction.rs b/quadratic-core/src/controller/active_transactions/pending_transaction.rs index 6f6c061c41..5d8a82857a 100644 --- a/quadratic-core/src/controller/active_transactions/pending_transaction.rs +++ b/quadratic-core/src/controller/active_transactions/pending_transaction.rs @@ -12,9 +12,7 @@ use crate::{ controller::{ execution::TransactionType, operations::operation::Operation, transaction::Transaction, }, - grid::{ - sheet::validations::validation::Validation, CodeCellLanguage, DataTable, Sheet, SheetId, - }, + grid::{sheet::validations::validation::Validation, CodeCellLanguage, Sheet, SheetId}, selection::Selection, Pos, SheetPos, SheetRect, }; @@ -276,23 +274,24 @@ impl PendingTransaction { } } - /// Adds a code cell, html cell and image cell to the transaction from a CodeRun + /// Adds a code cell, html cell and image cell to the transaction from a + /// CodeRun. If the code_cell no longer exists, then it sends the empty code + /// cell so the client can remove it. pub fn add_from_code_run( &mut self, sheet_id: SheetId, pos: Pos, - data_table: &Option, + is_image: bool, + is_html: bool, ) { - if let Some(data_table) = &data_table { - self.add_code_cell(sheet_id, pos); + self.add_code_cell(sheet_id, pos); - if data_table.is_html() { - self.add_html_cell(sheet_id, pos); - } + if is_html { + self.add_html_cell(sheet_id, pos); + } - if data_table.is_image() { - self.add_image_cell(sheet_id, pos); - } + if is_image { + self.add_image_cell(sheet_id, pos); } } @@ -360,7 +359,7 @@ impl PendingTransaction { mod tests { use crate::{ controller::operations::operation::Operation, - grid::{CodeRun, DataTableKind, Sheet, SheetId}, + grid::{CodeRun, DataTable, DataTableKind, Sheet, SheetId}, CellValue, Value, }; @@ -483,8 +482,8 @@ mod tests { let sheet_id = SheetId::new(); let pos = Pos { x: 0, y: 0 }; - transaction.add_from_code_run(sheet_id, pos, &None); - assert_eq!(transaction.code_cells.len(), 0); + transaction.add_from_code_run(sheet_id, pos, false, false); + assert_eq!(transaction.code_cells.len(), 1); assert_eq!(transaction.html_cells.len(), 0); assert_eq!(transaction.image_cells.len(), 0); @@ -508,7 +507,7 @@ mod tests { true, None, ); - transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); + transaction.add_from_code_run(sheet_id, pos, data_table.is_image(), data_table.is_html()); assert_eq!(transaction.code_cells.len(), 1); assert_eq!(transaction.html_cells.len(), 1); assert_eq!(transaction.image_cells.len(), 0); @@ -533,7 +532,7 @@ mod tests { true, None, ); - transaction.add_from_code_run(sheet_id, pos, &Some(data_table)); + transaction.add_from_code_run(sheet_id, pos, data_table.is_image(), data_table.is_html()); assert_eq!(transaction.code_cells.len(), 1); assert_eq!(transaction.html_cells.len(), 1); assert_eq!(transaction.image_cells.len(), 1); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs b/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs index 2af698f7ce..cc382832f2 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_col_rows.rs @@ -148,7 +148,7 @@ impl GridController { sheet_rect.min.x = column; self.check_deleted_data_tables(transaction, &sheet_rect); self.add_compute_operations(transaction, &sheet_rect, None); - self.check_all_spills(transaction, sheet_rect.sheet_id, true); + self.check_all_spills(transaction, sheet_rect.sheet_id); } } } @@ -185,7 +185,7 @@ impl GridController { sheet_rect.min.y = row; self.check_deleted_data_tables(transaction, &sheet_rect); self.add_compute_operations(transaction, &sheet_rect, None); - self.check_all_spills(transaction, sheet_rect.sheet_id, true); + self.check_all_spills(transaction, sheet_rect.sheet_id); } } } @@ -227,7 +227,7 @@ impl GridController { sheet_rect.min.x = column + 1; self.check_deleted_data_tables(transaction, &sheet_rect); self.add_compute_operations(transaction, &sheet_rect, None); - self.check_all_spills(transaction, sheet_rect.sheet_id, true); + self.check_all_spills(transaction, sheet_rect.sheet_id); } } } @@ -269,7 +269,7 @@ impl GridController { sheet_rect.min.y = row + 1; self.check_deleted_data_tables(transaction, &sheet_rect); self.add_compute_operations(transaction, &sheet_rect, None); - self.check_all_spills(transaction, sheet_rect.sheet_id, true); + self.check_all_spills(transaction, sheet_rect.sheet_id); } } } diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 6d18bb1473..6f96f77699 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -49,7 +49,7 @@ impl GridController { if transaction.is_user() { self.add_compute_operations(transaction, sheet_rect, None); - self.check_all_spills(transaction, sheet_rect.sheet_id, true); + self.check_all_spills(transaction, sheet_rect.sheet_id); } transaction.reverse_operations.extend(reverse_operations); diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_values.rs b/quadratic-core/src/controller/execution/execute_operation/execute_values.rs index 02c7c15fb7..fa81d5d0d5 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_values.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_values.rs @@ -52,7 +52,7 @@ impl GridController { if transaction.is_user() { self.check_deleted_data_tables(transaction, &sheet_rect); self.add_compute_operations(transaction, &sheet_rect, None); - self.check_all_spills(transaction, sheet_rect.sheet_id, true); + self.check_all_spills(transaction, sheet_rect.sheet_id); } transaction diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 67881accfd..150056d378 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -89,8 +89,18 @@ impl GridController { }; if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() { - transaction.add_from_code_run(sheet_id, pos, &old_data_table); - transaction.add_from_code_run(sheet_id, pos, &new_data_table); + transaction.add_from_code_run( + sheet_id, + pos, + old_data_table.as_ref().map_or(false, |dt| dt.is_image()), + old_data_table.as_ref().map_or(false, |dt| dt.is_html()), + ); + transaction.add_from_code_run( + sheet_id, + pos, + new_data_table.as_ref().map_or(false, |dt| dt.is_image()), + new_data_table.as_ref().map_or(false, |dt| dt.is_html()), + ); self.send_updated_bounds_rect(&sheet_rect, false); transaction.add_dirty_hashes_from_sheet_rect(sheet_rect); @@ -120,7 +130,7 @@ impl GridController { if transaction.is_user() { self.add_compute_operations(transaction, &sheet_rect, Some(sheet_pos)); - self.check_all_spills(transaction, sheet_pos.sheet_id, true); + self.check_all_spills(transaction, sheet_pos.sheet_id); } } diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index aa959ac95e..f9f61b9061 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -14,14 +14,11 @@ impl GridController { sheet_id: SheetId, index: usize, spill_error: bool, - send_client: bool, ) { - let mut code_cell_pos: Option = None; - let mut is_image = false; // change the spill for the first code_cell and then iterate the later code_cells. if let Some(sheet) = self.grid.try_sheet_mut(sheet_id) { + let mut code_pos: Option = None; if let Some((pos, run)) = sheet.data_tables.get_index_mut(index) { - code_cell_pos = Some(*pos); let sheet_pos = pos.to_sheet_pos(sheet.id); transaction.reverse_operations.push(Operation::SetCodeRun { sheet_pos, @@ -29,34 +26,21 @@ impl GridController { index, }); run.spill_error = spill_error; - is_image = run.is_image(); transaction.forward_operations.push(Operation::SetCodeRun { sheet_pos, code_run: Some(run.to_owned()), index, }); + code_pos = Some(*pos); } - } - if (cfg!(target_family = "wasm") || cfg!(test)) && !transaction.is_server() && send_client { - if let (Some(pos), Some(sheet)) = (code_cell_pos, self.grid.try_sheet(sheet_id)) { - if let (Some(code_cell), Some(render_code_cell)) = - (sheet.edit_code_value(pos), sheet.get_render_code_cell(pos)) - { - if let (Ok(code_cell), Ok(render_code_cell)) = ( - serde_json::to_string(&code_cell), - serde_json::to_string(&render_code_cell), - ) { - crate::wasm_bindings::js::jsUpdateCodeCell( - sheet_id.to_string(), - pos.x, - pos.y, - Some(code_cell), - Some(render_code_cell), - ); - } - } - if !spill_error && is_image { - sheet.send_image(pos); + if let Some(code_pos) = code_pos { + if let Some(data_table) = sheet.data_tables.get(&code_pos) { + transaction.add_from_code_run( + sheet_id, + code_pos, + data_table.is_image(), + data_table.is_html(), + ); } } } @@ -93,16 +77,11 @@ impl GridController { } /// Checks all data_tables for changes in spill_errors. - pub fn check_all_spills( - &mut self, - transaction: &mut PendingTransaction, - sheet_id: SheetId, - send_client: bool, - ) { + pub fn check_all_spills(&mut self, transaction: &mut PendingTransaction, sheet_id: SheetId) { if let Some(sheet) = self.grid.try_sheet(sheet_id) { for index in 0..sheet.data_tables.len() { if let Some(spill_error) = self.check_spill(sheet_id, index) { - self.change_spill(transaction, sheet_id, index, spill_error, send_client); + self.change_spill(transaction, sheet_id, index, spill_error); } } } @@ -113,14 +92,14 @@ impl GridController { mod tests { use std::collections::HashSet; - use serial_test::{parallel, serial}; + use serial_test::parallel; use crate::controller::active_transactions::pending_transaction::PendingTransaction; use crate::controller::transaction_types::JsCodeResult; use crate::controller::GridController; use crate::grid::js_types::{JsNumber, JsRenderCell, JsRenderCellSpecial}; use crate::grid::{CellAlign, CellWrap, CodeCellLanguage, CodeRun, DataTable, DataTableKind}; - use crate::wasm_bindings::js::{clear_js_calls, expect_js_call_count}; + use crate::wasm_bindings::js::clear_js_calls; use crate::{Array, CellValue, Pos, Rect, SheetPos, Value}; fn output_spill_error(x: i64, y: i64) -> Vec { @@ -182,14 +161,14 @@ mod tests { let sheet = gc.grid.try_sheet(sheet_id).unwrap(); assert!(!sheet.data_tables[0].spill_error); - gc.check_all_spills(&mut transaction, sheet_id, false); + gc.check_all_spills(&mut transaction, sheet_id); let sheet = gc.grid.try_sheet(sheet_id).unwrap(); assert!(sheet.data_tables[0].spill_error); } #[test] - #[serial] + #[parallel] fn test_check_all_spills() { let mut gc = GridController::test(); let mut transaction = PendingTransaction::default(); @@ -222,17 +201,16 @@ mod tests { let sheet = gc.sheet(sheet_id); assert!(!sheet.data_tables[0].spill_error); - gc.check_all_spills(&mut transaction, sheet_id, false); + gc.check_all_spills(&mut transaction, sheet_id); let sheet = gc.sheet(sheet_id); assert!(sheet.data_tables[0].spill_error); - expect_js_call_count("jsUpdateCodeCell", 0, true); // remove the cell causing the spill error let sheet = gc.sheet_mut(sheet_id); sheet.set_cell_value(Pos { x: 1, y: 1 }, CellValue::Blank); assert_eq!(sheet.cell_value(Pos { x: 1, y: 1 }), None); - gc.check_all_spills(&mut transaction, sheet_id, true); - expect_js_call_count("jsUpdateCodeCell", 1, true); + gc.check_all_spills(&mut transaction, sheet_id); + let sheet = gc.sheet(sheet_id); assert!(!sheet.data_tables[0].spill_error); } @@ -270,7 +248,7 @@ mod tests { sheet.set_cell_value(Pos { x: 0, y: 1 }, CellValue::Text("hello".into())); let transaction = &mut PendingTransaction::default(); - gc.check_all_spills(transaction, sheet_id, false); + gc.check_all_spills(transaction, sheet_id); let sheet = gc.sheet(sheet_id); let code_run = sheet.data_table(Pos { x: 0, y: 0 }).unwrap(); diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 57a3d29057..4146ae7f00 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -447,6 +447,15 @@ impl Sheet { None, ) }; + let alternating_colors = if data_table.spill_error + || data_table.has_error() + || data_table.is_image() + || data_table.is_html() + { + false + } else { + data_table.alternating_colors + }; Some(JsRenderCodeCell { x: pos.x as i32, y: pos.y as i32, @@ -464,7 +473,7 @@ impl Sheet { first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, sort: data_table.sort.clone(), - alternating_colors: data_table.alternating_colors, + alternating_colors, }) } @@ -498,7 +507,15 @@ impl Sheet { None, ) }; - + let alternating_colors = if data_table.spill_error + || data_table.has_error() + || data_table.is_image() + || data_table.is_html() + { + false + } else { + data_table.alternating_colors + }; Some(JsRenderCodeCell { x: pos.x as i32, y: pos.y as i32, @@ -512,7 +529,7 @@ impl Sheet { first_row_header: data_table.header_is_first_row, show_header: data_table.show_header, sort: data_table.sort.clone(), - alternating_colors: data_table.alternating_colors, + alternating_colors, }) } _ => None, // this should not happen. A CodeRun should always have a CellValue::Code. From 8c471dd28ec3b14fe47db82524e05636fe2c0da3 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 6 Nov 2024 09:28:43 -0800 Subject: [PATCH 213/373] resize/spill work on charts and image --- .../src/app/grid/sheet/GridOverflowLines.ts | 6 +- .../app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 48 +++++++-- .../gridGL/HTMLGrid/htmlCells/HtmlCells.css | 1 + .../HTMLGrid/htmlCells/htmlCellsHandler.ts | 13 +-- .../app/gridGL/cells/tables/TableOutline.ts | 15 ++- .../src/app/gridGL/cells/tables/Tables.ts | 2 +- .../interaction/pointer/PointerHtmlCells.ts | 1 - .../src/app/quadratic-core-types/index.d.ts | 2 +- quadratic-core/src/grid/js_types.rs | 2 + quadratic-core/src/grid/sheet/rendering.rs | 97 +++++-------------- quadratic-core/src/sheet_offsets/mod.rs | 2 +- 11 files changed, 91 insertions(+), 98 deletions(-) diff --git a/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts b/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts index 5a5bfe93e0..aa4bdf304f 100644 --- a/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts +++ b/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts @@ -2,6 +2,7 @@ //! of overflow of text, images, and html tables.. import { Sheet } from '@/app/grid/sheet/Sheet'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Coordinate } from '@/app/gridGL/types/size'; import { Rectangle } from 'pixi.js'; @@ -34,10 +35,6 @@ export class GridOverflowLines { }); } - resizeImage(x: number, y: number, width: number, height: number) { - this.updateImageHtml(x, y, width, height); - } - // updates the hash with a rectangle of an image or html table updateImageHtml(column: number, row: number, width?: number, height?: number) { if (width === undefined || height === undefined) { @@ -47,6 +44,7 @@ export class GridOverflowLines { const start = this.sheet.offsets.getCellOffsets(column, row); const end = this.sheet.getColumnRow(start.x + width, start.y + height); this.overflowImageHtml.set(`${column},${row}`, new Rectangle(column, row, end.x - column, end.y - row)); + pixiApp.gridLines.dirty = true; } // returns a list of ranges of y-values that need to be drawn (excluding the diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index 9ef95bab0c..b370866a42 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -21,6 +21,16 @@ export class HtmlCell { private hoverSide: 'right' | 'bottom' | 'corner' | undefined; private offset: Point; + // used during resizing to store the temporary width and height + private temporaryWidth: number | undefined; + private temporaryHeight: number | undefined; + + // whether pointer events are allowed on the iframe (currently when selected + // but not resizing) + pointerEvents: 'auto' | 'none' = 'none'; + + border: HTMLDivElement; + htmlCell: JsHtmlOutput; gridBounds: Rectangle; @@ -40,7 +50,7 @@ export class HtmlCell { this.div = document.createElement('div'); this.div.className = 'html-cell'; - this.div.style.boxShadow = 'inset 0 0 0 1px hsl(var(--primary))'; + const offset = this.sheet.getCellOffsets(Number(htmlCell.x), Number(htmlCell.y)); this.offset = new Point(offset.x, offset.y); this.gridBounds = new Rectangle(Number(htmlCell.x), Number(htmlCell.y), 0, 0); @@ -58,16 +68,21 @@ export class HtmlCell { this.iframe.style.pointerEvents = 'none'; this.iframe.srcdoc = htmlCell.html; this.iframe.title = `HTML from ${htmlCell.x}, ${htmlCell.y}}`; - this.iframe.width = this.width.toString(); - this.iframe.height = this.height.toString(); + this.iframe.width = `${this.width}px`; + this.iframe.height = `${this.height}px`; this.iframe.setAttribute('border', '0'); this.iframe.setAttribute('scrolling', 'no'); this.iframe.style.minWidth = `${CELL_WIDTH}px`; this.iframe.style.minHeight = `${CELL_HEIGHT}px`; + this.border = document.createElement('div'); + this.border.className = 'w-full h-full absolute top-0 left-0'; + this.border.style.border = '1px solid hsl(var(--primary))'; + this.div.append(this.right); this.div.append(this.iframe); this.div.append(this.bottom); + this.div.append(this.border); if (this.iframe.contentWindow?.document.readyState === 'complete') { this.afterLoad(); @@ -75,11 +90,18 @@ export class HtmlCell { this.iframe.addEventListener('load', this.afterLoad); } + this.sheet.gridOverflowLines.updateImageHtml(this.x, this.y, this.width, this.height); + if (this.sheet.id !== sheets.sheet.id) { this.div.style.visibility = 'hidden'; } } + destroy() { + this.div.remove(); + this.sheet.gridOverflowLines.updateImageHtml(this.x, this.y, 0, 0); + } + get x(): number { return Number(this.htmlCell.x); } @@ -132,15 +154,18 @@ export class HtmlCell { update(htmlCell: JsHtmlOutput) { if (!htmlCell.html) throw new Error('Expected html to be defined in HtmlCell.update'); - if (htmlCell.w !== this.htmlCell.w && htmlCell.h !== this.htmlCell.h) { - this.iframe.width = (htmlCell.w ?? DEFAULT_HTML_WIDTH).toString(); - this.iframe.height = (htmlCell.h ?? DEFAULT_HTML_HEIGHT).toString(); - } if (htmlCell.html !== this.htmlCell.html) { this.iframe.srcdoc = htmlCell.html; } this.htmlCell = htmlCell; + this.iframe.width = this.width.toString(); + this.iframe.height = this.height.toString(); + this.border.style.width = `${this.width}px`; + this.border.style.height = `${this.height}px`; this.calculateGridBounds(); + this.sheet.gridOverflowLines.updateImageHtml(this.x, this.y, this.width, this.height); + this.temporaryWidth = undefined; + this.temporaryHeight = undefined; } private calculateGridBounds() { @@ -227,6 +252,7 @@ export class HtmlCell { if (!this.hoverSide) { throw new Error('Expected hoverSide to be defined in HtmlCell.startResizing'); } + this.iframe.style.pointerEvents = 'none'; this.resizing = new HtmlCellResizing( this, this.hoverSide, @@ -245,6 +271,7 @@ export class HtmlCell { this.bottom.classList.remove('html-resize-control-bottom-corner'); this.resizing.cancelResizing(); this.resizing = undefined; + this.iframe.style.pointerEvents = this.pointerEvents; } completeResizing() { @@ -253,14 +280,21 @@ export class HtmlCell { } this.resizing.completeResizing(); this.resizing = undefined; + this.iframe.style.pointerEvents = this.pointerEvents; } setWidth(width: number) { + this.temporaryWidth = width; this.iframe.width = width.toString(); + this.border.style.width = `${width}px`; + this.sheet.gridOverflowLines.updateImageHtml(this.x, this.y, width, this.temporaryHeight ?? this.height); } setHeight(height: number) { + this.temporaryHeight = height; this.iframe.height = height.toString(); + this.border.style.height = `${height}px`; + this.sheet.gridOverflowLines.updateImageHtml(this.x, this.y, this.temporaryWidth ?? this.width, height); } updateOffsets() { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css index 759539aaa5..2ca228bff6 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css @@ -13,6 +13,7 @@ .html-cell-iframe { margin: 0; + overflow: hidden; } .html-resize-control-right, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts index d83d1f83d8..0221c201a9 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts @@ -9,7 +9,6 @@ import { HtmlCell } from './HtmlCell'; class HTMLCellsHandler { // used to attach the html-cells to react private div: HTMLDivElement; - private cells: Set = new Set(); constructor() { @@ -43,7 +42,7 @@ class HTMLCellsHandler { if (data.html) { cell.update(data); } else { - this.getParent().removeChild(cell.div); + cell.destroy(); this.cells.delete(cell); } return; @@ -158,8 +157,9 @@ class HTMLCellsHandler { (cell) => cell.x === codeCell.x && cell.y === codeCell.y && cell.sheet.id === sheets.sheet.id ); if (cell) { - cell.div.style.boxShadow = 'inset 0 0 0 2px hsl(var(--primary))'; - cell.iframe.style.pointerEvents = isSelected ? 'auto' : 'none'; + cell.border.style.border = '2px solid hsl(var(--primary))'; + cell.pointerEvents = isSelected ? 'auto' : 'none'; + cell.iframe.style.pointerEvents = cell.pointerEvents; } } @@ -168,8 +168,9 @@ class HTMLCellsHandler { (cell) => cell.x === codeCell.x && cell.y === codeCell.y && cell.sheet.id === sheets.sheet.id ); if (cell) { - cell.div.style.boxShadow = 'inset 0 0 0 1px hsl(var(--primary))'; - cell.iframe.style.pointerEvents = 'none'; + cell.border.style.border = '1px solid hsl(var(--primary))'; + cell.pointerEvents = 'none'; + cell.iframe.style.pointerEvents = cell.pointerEvents; } } } diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts index 59d512c540..e16cee974c 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts @@ -34,8 +34,12 @@ export class TableOutline extends Graphics { // draw the table selected outline const width = this.active ? 2 : 1; - this.lineStyle({ color: getCSSVariableTint('primary'), width, alignment: 0 }); - this.drawShape(new Rectangle(0, 0, this.table.tableBounds.width, this.table.tableBounds.height)); + const image = this.table.codeCell.state === 'Image'; + const chart = this.table.codeCell.state === 'HTML'; + if (!chart) { + this.lineStyle({ color: getCSSVariableTint('primary'), width, alignment: 0 }); + this.drawShape(new Rectangle(0, 0, this.table.tableBounds.width, this.table.tableBounds.height)); + } // draw the spill error boundaries if (this.active && this.table.codeCell.spill_error) { @@ -48,7 +52,12 @@ export class TableOutline extends Graphics { // draw outline around where the code cell would spill this.lineStyle({ color: getCSSVariableTint('primary'), width: 1, alignment: 0 }); - this.drawRect(0, 0, full.width, full.height); + this.drawRect( + 0, + 0, + chart || image ? this.table.codeCell.w : full.width, + chart || image ? this.table.codeCell.h : full.height + ); // box and shade what is causing the spill errors this.table.codeCell.spill_error.forEach((error) => { diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 72fc33699d..57a6803124 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -432,7 +432,7 @@ export class Tables extends Container
{ const table = this.children.find((table) => table.codeCell.x === x && table.codeCell.y === y); if (table) { table.resize(width, height); - sheets.sheet.gridOverflowLines.resizeImage(x, y, width, height); + sheets.sheet.gridOverflowLines.updateImageHtml(x, y, width, height); pixiApp.gridLines.dirty = true; } } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHtmlCells.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHtmlCells.ts index 5e9bd141fe..46701b9598 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHtmlCells.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHtmlCells.ts @@ -25,7 +25,6 @@ export class PointerHtmlCells { } this.clicked = undefined; - if (this.active) return true; const cells = htmlCellsHandler.getCells(); for (const cell of cells) { diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 857a97ff27..0dc183785e 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -38,7 +38,7 @@ export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } -export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; +export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success" | "HTML" | "Image"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } export interface JsSheetFill { columns: Array<[bigint, [string, bigint]]>, rows: Array<[bigint, [string, bigint]]>, all: string | null, } diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index aa9cc4436c..aa74ff92b7 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -224,6 +224,8 @@ pub enum JsRenderCodeCellState { RunError, SpillError, Success, + HTML, + Image, } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index 4146ae7f00..a2817634b7 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -423,8 +423,8 @@ impl Sheet { JsSheetFill { columns, rows, all } } - pub fn get_render_code_cell(&self, pos: Pos) -> Option { - let data_table = self.data_tables.get(&pos)?; + // Returns data for rendering a code cell. + fn render_code_cell(&self, pos: Pos, data_table: &DataTable) -> Option { let code = self.cell_value(pos)?; let output_size = data_table.output_size(); let (state, w, h, spill_error) = if data_table.spill_error { @@ -440,22 +440,21 @@ impl Sheet { { (JsRenderCodeCellState::RunError, 1, 1, None) } else { - ( - JsRenderCodeCellState::Success, - output_size.w.get(), - output_size.h.get(), - None, - ) - }; - let alternating_colors = if data_table.spill_error - || data_table.has_error() - || data_table.is_image() - || data_table.is_html() - { - false - } else { - data_table.alternating_colors + let state = if data_table.is_image() { + JsRenderCodeCellState::Image + } else if data_table.is_html() { + JsRenderCodeCellState::HTML + } else { + JsRenderCodeCellState::Success + }; + (state, output_size.w.get(), output_size.h.get(), None) }; + let alternating_colors = !data_table.spill_error + && !data_table.has_error() + && !data_table.is_image() + && !data_table.is_html() + && data_table.alternating_colors; + Some(JsRenderCodeCell { x: pos.x as i32, y: pos.y as i32, @@ -477,67 +476,17 @@ impl Sheet { }) } + // Returns a single code cell for rendering. + pub fn get_render_code_cell(&self, pos: Pos) -> Option { + let data_table = self.data_tables.get(&pos)?; + self.render_code_cell(pos, data_table) + } + /// Returns data for all rendering code cells pub fn get_all_render_code_cells(&self) -> Vec { self.data_tables .iter() - .filter_map(|(pos, data_table)| { - if let Some(code) = self.cell_value(*pos) { - match code.code_cell_value() { - Some(code_cell_value) => { - let output_size = data_table.output_size(); - let (state, w, h, spill_error) = if data_table.spill_error { - let reasons = self.find_spill_error_reasons( - &data_table.output_rect(*pos, true), - *pos, - ); - ( - JsRenderCodeCellState::SpillError, - output_size.w.get(), - output_size.h.get(), - Some(reasons), - ) - } else if data_table.has_error() { - (JsRenderCodeCellState::RunError, 1, 1, None) - } else { - ( - JsRenderCodeCellState::Success, - output_size.w.get(), - output_size.h.get(), - None, - ) - }; - let alternating_colors = if data_table.spill_error - || data_table.has_error() - || data_table.is_image() - || data_table.is_html() - { - false - } else { - data_table.alternating_colors - }; - Some(JsRenderCodeCell { - x: pos.x as i32, - y: pos.y as i32, - w, - h, - language: code_cell_value.language, - state, - spill_error, - name: data_table.name.clone(), - columns: data_table.send_columns(), - first_row_header: data_table.header_is_first_row, - show_header: data_table.show_header, - sort: data_table.sort.clone(), - alternating_colors, - }) - } - _ => None, // this should not happen. A CodeRun should always have a CellValue::Code. - } - } else { - None // this should not happen. A CodeRun should always have a CellValue::Code. - } - }) + .filter_map(|(pos, data_table)| self.render_code_cell(*pos, data_table)) .collect() } diff --git a/quadratic-core/src/sheet_offsets/mod.rs b/quadratic-core/src/sheet_offsets/mod.rs index 59096aab06..c19b8e535a 100644 --- a/quadratic-core/src/sheet_offsets/mod.rs +++ b/quadratic-core/src/sheet_offsets/mod.rs @@ -279,7 +279,7 @@ impl SheetOffsets { let start = self.cell_offsets(pos.x, pos.y); let (end_x, _) = self.column_from_x(start.x + width as f64); let (end_y, _) = self.row_from_y(start.y + height as f64); - ((end_x - pos.x) as u32, (end_y - pos.y) as u32) + ((end_x - pos.x + 1) as u32, (end_y - pos.y + 1) as u32) } } From 28d7b470749206207e5298e8acd75063404a16ca Mon Sep 17 00:00:00 2001 From: David Figatner Date: Wed, 6 Nov 2024 09:47:29 -0800 Subject: [PATCH 214/373] fix visual bug --- .../src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css index 2ca228bff6..44fcf4b048 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCells.css @@ -33,7 +33,7 @@ } .html-resize-control-right-corner { - height: calc(100% + 7px); + height: calc(100% + 5px); } .html-resize-control-bottom-corner { From dc466299b6877d416e08b165ff75b64d742ad2b8 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 6 Nov 2024 14:35:49 -0700 Subject: [PATCH 215/373] Add original index to the data table pretty print --- .../src/app/quadratic-core-types/index.d.ts | 2 +- quadratic-core/src/grid/data_table/mod.rs | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 6436bfddc9..84d3b716d9 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -37,7 +37,7 @@ export interface JsOffset { column: number | null, row: number | null, size: num export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, display_buffer: Array | null, alternating_colors: boolean, } +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 82af628acb..fa0bca99dc 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -308,9 +308,15 @@ impl DataTable { let array = data_table.display_value().unwrap().into_array().unwrap(); let max = max.unwrap_or(array.height() as usize); let title = title.unwrap_or("Data Table"); + let display_buffer = data_table + .display_buffer + .clone() + .unwrap_or((0..array.height() as u64).collect::>()); for (index, row) in array.rows().take(max).enumerate() { let row = row.iter().map(|s| s.to_string()).collect::>(); + let display_index = vec![display_buffer[index].to_string()]; + let row = [display_index, row].concat(); if index == 0 && data_table.header_is_first_row { builder.set_header(row); @@ -324,12 +330,14 @@ impl DataTable { // bold the headers if they exist if data_table.header_is_first_row { + table.with(Modify::new((0, 0)).with(Color::BOLD)); + (0..table.count_columns()) .collect::>() .iter() .enumerate() .for_each(|(index, _)| { - table.with(Modify::new((0, index)).with(Color::BOLD)); + table.with(Modify::new((0, index + 1)).with(Color::BOLD)); }); } From c469161a6ca6fba4e0b2ac9949b35786bc5e8f2c Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 6 Nov 2024 16:44:49 -0700 Subject: [PATCH 216/373] Edit sorted data table cell --- Cargo.lock | 8 +++---- .../src/app/quadratic-core-types/index.d.ts | 4 ---- .../execute_operation/execute_data_table.rs | 6 +++-- .../src/grid/data_table/display_value.rs | 22 ++++++++++++++++++- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f68f62fa9b..247417e364 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6172,9 +6172,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap 2.3.0", "toml_datetime", @@ -6992,9 +6992,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index e7c06b52e4..0dc183785e 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -38,11 +38,7 @@ export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } -<<<<<<< HEAD -export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success"; -======= export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success" | "HTML" | "Image"; ->>>>>>> 28d7b470749206207e5298e8acd75063404a16ca export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } export interface JsSheetFill { columns: Array<[bigint, [string, bigint]]>, rows: Array<[bigint, [string, bigint]]>, all: string | null, } diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index c258bbe631..c10a356424 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -5,6 +5,7 @@ use crate::{ operations::operation::Operation, GridController, }, grid::{DataTable, DataTableKind}, + util::dbgjs, ArraySize, CellValue, Pos, Rect, SheetRect, }; @@ -122,11 +123,12 @@ impl GridController { if let Some(display_buffer) = &data_table.display_buffer { // if there is a display buffer, use it to find the source row index + let index_to_find = pos.y - data_table_pos.y; let row_index = *display_buffer - .get(pos.y as usize) + .get(index_to_find as usize) .unwrap_or(&(pos.y as u64)); - pos.y = row_index as i64; + pos.y = row_index as i64 + data_table_pos.y; } if data_table.show_header && !data_table.header_is_first_row { diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs index 72d50e1cb6..1bbd760083 100644 --- a/quadratic-core/src/grid/data_table/display_value.rs +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -18,10 +18,30 @@ use super::DataTable; impl DataTable { pub fn display_value_from_buffer(&self, display_buffer: &[u64]) -> Result { let value = self.value.to_owned().into_array()?; + let columns_to_show = self + .columns + .iter() + .flatten() + .enumerate() + .filter(|(_, c)| c.display) + .map(|(index, _)| index) + .collect::>(); let values = display_buffer .iter() - .filter_map(|index| value.get_row(*index as usize).map(|row| row.to_vec()).ok()) + .filter_map(|index| { + value + .get_row(*index as usize) + .map(|row| { + row.to_vec() + .into_iter() + .enumerate() + .filter(|(i, _)| columns_to_show.contains(&i)) + .map(|(_, v)| v) + .collect::>() + }) + .ok() + }) .collect::>>(); let array = Array::from(values); From 3878a8222bf349f55dfa7896eecb86b47cd0da47 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 7 Nov 2024 05:12:05 -0800 Subject: [PATCH 217/373] keep chart size consistent between runs --- .../execute_operation/execute_data_table.rs | 1 - .../src/controller/execution/run_code/mod.rs | 61 ++++++++++++++++++- quadratic-core/src/sheet_offsets/mod.rs | 4 +- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index c10a356424..0446a06a4d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -5,7 +5,6 @@ use crate::{ operations::operation::Operation, GridController, }, grid::{DataTable, DataTableKind}, - util::dbgjs, ArraySize, CellValue, Pos, Rect, SheetRect, }; diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 150056d378..9266d27c94 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -157,7 +157,7 @@ impl GridController { } Some(waiting_for_async) => match waiting_for_async { CodeCellLanguage::Python | CodeCellLanguage::Javascript => { - let new_data_table = self.js_code_result_to_code_cell_value( + let mut new_data_table = self.js_code_result_to_code_cell_value( transaction, result, current_sheet_pos, @@ -165,6 +165,20 @@ impl GridController { ); transaction.waiting_for_async = None; + + // this is a hack to ensure that the chart size remains the + // same if there is an existing chart in the same cell + if let Some(sheet) = self.try_sheet(current_sheet_pos.sheet_id) { + if let Some(old_chart_pixel_output) = sheet + .data_tables + .iter() + .find(|(p, _)| **p == current_sheet_pos.into()) + .and_then(|(_, dt)| dt.chart_pixel_output) + { + new_data_table.chart_pixel_output = Some(old_chart_pixel_output); + } + } + self.finalize_code_run( transaction, current_sheet_pos, @@ -535,4 +549,49 @@ mod test { gc.calculation_complete(result).unwrap(); expect_js_call_count("jsSendImage", 1, true); } + + #[test] + #[parallel] + fn ensure_chart_size_remains_same_if_same_cell() { + let mut gc = GridController::test(); + let sheet_id = gc.sheet_ids()[0]; + let sheet_pos = SheetPos { + x: 1, + y: 1, + sheet_id, + }; + + let languages = vec![CodeCellLanguage::Javascript, CodeCellLanguage::Python]; + + for language in languages { + gc.set_code_cell(sheet_pos, language.clone(), "code".to_string(), None); + let transaction = gc.last_transaction().unwrap(); + let result = JsCodeResult { + transaction_id: transaction.id.to_string(), + success: true, + output_value: Some(vec!["test".into(), "image".into()]), + chart_pixel_output: Some((100.0, 100.0)), + ..Default::default() + }; + gc.calculation_complete(result).unwrap(); + let sheet = gc.try_sheet(sheet_id).unwrap(); + let dt = sheet.data_table(sheet_pos.into()).unwrap(); + assert_eq!(dt.chart_pixel_output, Some((100.0, 100.0))); + + // change the cell + gc.set_code_cell(sheet_pos, language, "code".to_string(), None); + let transaction = gc.last_transaction().unwrap(); + let result = JsCodeResult { + transaction_id: transaction.id.to_string(), + success: true, + output_value: Some(vec!["test".into(), "image".into()]), + chart_pixel_output: Some((200.0, 200.0)), + ..Default::default() + }; + gc.calculation_complete(result).unwrap(); + let sheet = gc.try_sheet(sheet_id).unwrap(); + let dt = sheet.data_table(sheet_pos.into()).unwrap(); + assert_eq!(dt.chart_pixel_output, Some((100.0, 100.0))); + } + } } diff --git a/quadratic-core/src/sheet_offsets/mod.rs b/quadratic-core/src/sheet_offsets/mod.rs index c19b8e535a..1f16963fb6 100644 --- a/quadratic-core/src/sheet_offsets/mod.rs +++ b/quadratic-core/src/sheet_offsets/mod.rs @@ -332,7 +332,7 @@ mod test { fn calculate_grid_size() { let sheet = super::SheetOffsets::default(); let (width, height) = sheet.calculate_grid_size(Pos { x: 0, y: 0 }, 100.0, 21.0); - assert_eq!(width, 1); - assert_eq!(height, 1); + assert_eq!(width, 2); + assert_eq!(height, 2); } } From 06a4c1188862c2250a6970db7d0f346a86210788 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 7 Nov 2024 05:14:33 -0800 Subject: [PATCH 218/373] clippy --- quadratic-core/src/grid/file/v1_7/file.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadratic-core/src/grid/file/v1_7/file.rs b/quadratic-core/src/grid/file/v1_7/file.rs index cf91e81392..f9b3ed7c3d 100644 --- a/quadratic-core/src/grid/file/v1_7/file.rs +++ b/quadratic-core/src/grid/file/v1_7/file.rs @@ -6,7 +6,7 @@ use crate::grid::file::{ }; fn render_size_to_chart_size( - columns: &Vec<(i64, v1_7::ColumnSchema)>, + columns: &[(i64, v1_7::ColumnSchema)], pos: v1_7::PosSchema, ) -> Option<(f32, f32)> { columns @@ -31,7 +31,7 @@ fn render_size_to_chart_size( fn upgrade_code_runs( code_runs: Vec<(v1_7::PosSchema, v1_7::CodeRunSchema)>, - columns: &Vec<(i64, v1_7::ColumnSchema)>, + columns: &[(i64, v1_7::ColumnSchema)], ) -> Result> { code_runs .into_iter() From fba8177678c2349b8b52f51fef0e596ce8870d5e Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 7 Nov 2024 05:54:11 -0800 Subject: [PATCH 219/373] when offsets change, recalculate intersecting chart sizes --- .../execute_operation/execute_offsets.rs | 22 ++++ .../src/controller/operations/code_cell.rs | 118 +++++++++++++++++- 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_offsets.rs b/quadratic-core/src/controller/execution/execute_operation/execute_offsets.rs index 7c84dc26dc..f85c4a6a36 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_offsets.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_offsets.rs @@ -62,6 +62,9 @@ impl GridController { let resize_rows = transaction.resize_rows.entry(sheet_id).or_default(); resize_rows.extend(rows); } + transaction + .operations + .extend(self.check_chart_size_column_change(sheet_id, column)); } if !transaction.is_server() { @@ -119,6 +122,14 @@ impl GridController { .insert((None, Some(row)), new_size); } + if transaction.is_user() { + let changes = self.check_chart_size_row_change(sheet_id, row); + if !changes.is_empty() { + transaction.operations.extend(changes); + self.check_all_spills(transaction, sheet_id); + } + } + if !transaction.is_server() { transaction.generate_thumbnail |= self.thumbnail_dirty_sheet_pos(SheetPos { x: 0, @@ -174,6 +185,17 @@ impl GridController { }); } + if transaction.is_user() { + let mut changes = vec![]; + row_heights.iter().for_each(|&JsRowHeight { row, .. }| { + changes.extend(self.check_chart_size_row_change(sheet_id, row)); + }); + if !changes.is_empty() { + transaction.operations.extend(changes); + self.check_all_spills(transaction, sheet_id); + } + } + if !transaction.is_server() { row_heights.iter().any(|JsRowHeight { row, .. }| { transaction.generate_thumbnail |= self.thumbnail_dirty_sheet_pos(SheetPos { diff --git a/quadratic-core/src/controller/operations/code_cell.rs b/quadratic-core/src/controller/operations/code_cell.rs index 228c87ce48..ba6bf03b57 100644 --- a/quadratic-core/src/controller/operations/code_cell.rs +++ b/quadratic-core/src/controller/operations/code_cell.rs @@ -161,6 +161,50 @@ impl GridController { pixel_height: height, }] } + + /// Creates operations if changes to the column width would affect the chart + /// size. + pub fn check_chart_size_column_change(&self, sheet_id: SheetId, column: i64) -> Vec { + let mut ops = vec![]; + if let Some(sheet) = self.try_sheet(sheet_id) { + sheet.data_tables.iter().for_each(|(pos, dt)| { + if let (Some((width, _)), Some((pixel_width, pixel_height))) = + (dt.chart_output, dt.chart_pixel_output) + { + if column >= pos.x && column < pos.x + width as i64 { + ops.push(Operation::SetChartSize { + sheet_pos: pos.to_sheet_pos(sheet_id), + pixel_width, + pixel_height, + }); + } + } + }); + } + ops + } + + /// Creates operations if changes to the row height would affect the chart + /// size. + pub fn check_chart_size_row_change(&self, sheet_id: SheetId, row: i64) -> Vec { + let mut ops = vec![]; + if let Some(sheet) = self.try_sheet(sheet_id) { + sheet.data_tables.iter().for_each(|(pos, dt)| { + if let (Some((_, height)), Some((pixel_width, pixel_height))) = + (dt.chart_output, dt.chart_pixel_output) + { + if row >= pos.y && row < pos.y + height as i64 { + ops.push(Operation::SetChartSize { + sheet_pos: pos.to_sheet_pos(sheet_id), + pixel_width, + pixel_height, + }); + } + } + }) + } + ops + } } #[cfg(test)] @@ -168,7 +212,10 @@ mod test { use bigdecimal::BigDecimal; use super::*; - use crate::Pos; + use crate::{ + grid::{CodeRun, DataTableKind}, + Pos, Value, + }; use serial_test::parallel; #[test] @@ -409,4 +456,73 @@ mod test { gc.rerun_code_cell(sheet_pos, None); gc.rerun_sheet_code_cells(sheet_id, None); } + + #[test] + #[parallel] + fn test_check_chart_size_changes() { + let mut gc = GridController::default(); + let sheet_id = gc.sheet_ids()[0]; + let pos = Pos { x: 1, y: 1 }; + let sheet_pos = pos.to_sheet_pos(sheet_id); + + // Set up a data table with chart output + let mut dt = DataTable::new( + DataTableKind::CodeRun(CodeRun::default()), + "Table", + Value::Single(CellValue::Image("image".to_string())), + false, + false, + true, + None, + ); + dt.chart_output = Some((2, 3)); + dt.chart_pixel_output = Some((100.0, 150.0)); + gc.grid_mut() + .try_sheet_mut(sheet_id) + .unwrap() + .data_tables + .insert(pos, dt); + + // Test column changes + let ops = gc.check_chart_size_column_change(sheet_id, 1); + assert_eq!(ops.len(), 1); + assert_eq!( + ops[0], + Operation::SetChartSize { + sheet_pos, + pixel_width: 100.0, + pixel_height: 150.0, + } + ); + + let ops = gc.check_chart_size_column_change(sheet_id, 2); // Change within chart + assert_eq!(ops.len(), 1); + + let ops = gc.check_chart_size_column_change(sheet_id, 0); // Change before chart + assert_eq!(ops.len(), 0); + + let ops = gc.check_chart_size_column_change(sheet_id, 4); // Change after chart + assert_eq!(ops.len(), 0); + + // Test row changes + let ops = gc.check_chart_size_row_change(sheet_id, 1); // Change at start of chart + assert_eq!(ops.len(), 1); + assert_eq!( + ops[0], + Operation::SetChartSize { + sheet_pos, + pixel_width: 100.0, + pixel_height: 150.0, + } + ); + + let ops = gc.check_chart_size_row_change(sheet_id, 2); // Change within chart + assert_eq!(ops.len(), 1); + + let ops = gc.check_chart_size_row_change(sheet_id, 0); // Change before chart + assert_eq!(ops.len(), 0); + + let ops = gc.check_chart_size_row_change(sheet_id, 5); // Change after chart + assert_eq!(ops.len(), 0); + } } From b96bb5d449285af17567c096e513a42575ebdac7 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 7 Nov 2024 08:27:39 -0800 Subject: [PATCH 220/373] working through ctrl+arrow around tables --- .../src/grid/data_table/display_value.rs | 58 ++++++++++++-- quadratic-core/src/grid/data_table/mod.rs | 20 +++-- quadratic-core/src/grid/sheet/bounds.rs | 75 +++++++++++++++++-- quadratic-core/src/grid/sheet/data_table.rs | 33 ++++++++ 4 files changed, 166 insertions(+), 20 deletions(-) diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs index 1bbd760083..38ccfc61b0 100644 --- a/quadratic-core/src/grid/data_table/display_value.rs +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -70,14 +70,15 @@ impl DataTable { } pub fn display_value_at(&self, mut pos: Pos) -> Result<&CellValue> { - // println!("pos: {:?}", pos); - // println!("self.columns: {:?}", self.columns); + // If the DataTable contains HTML or images, then only the first cell is + // displayed. + if self.is_html_or_image() && (pos.x != 0 || pos.y != 0) { + return Ok(&CellValue::Blank); + } if pos.y == 0 && self.show_header { if let Some(columns) = &self.columns { - // println!("columns: {:?}", columns); if let Some(column) = columns.get(pos.x as usize) { - // println!("column: {:?}", column); return Ok(column.name.as_ref()); } } @@ -95,4 +96,51 @@ impl DataTable { } #[cfg(test)] -pub mod test {} +pub mod test { + use serial_test::parallel; + + use crate::{ + controller::{transaction_types::JsCodeResult, GridController}, + grid::CodeCellLanguage, + CellValue, Pos, SheetPos, + }; + + #[test] + #[parallel] + fn test_display_value_at_html_or_image() { + let mut gc = GridController::test(); + let sheet_id = gc.sheet_ids()[0]; + let sheet_pos = SheetPos { + x: 1, + y: 1, + sheet_id, + }; + gc.set_code_cell( + sheet_pos, + CodeCellLanguage::Python, + "code".to_string(), + None, + ); + let transaction_id = gc.last_transaction().unwrap().id; + gc.calculation_complete(JsCodeResult { + transaction_id: transaction_id.to_string(), + success: true, + output_value: Some(vec!["".to_string(), "text".to_string()]), + ..Default::default() + }) + .unwrap(); + gc.set_chart_size(sheet_pos, 100.0, 100.0, None); + + let sheet = gc.sheet(sheet_id); + + assert_eq!( + sheet.display_value(sheet_pos.into()).unwrap(), + CellValue::Html("".to_string()) + ); + + assert_eq!( + sheet.display_value(Pos { x: 2, y: 1 }).unwrap(), + CellValue::Blank + ) + } +} diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 0c7df19d1d..d6c98eb63c 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -274,21 +274,27 @@ impl DataTable { } pub fn is_html(&self) -> bool { - match self.cell_value_at(0, 0) { - Some(code_cell_value) => code_cell_value.is_html(), - None => false, + if let Value::Single(value) = &self.value { + matches!(value, CellValue::Html(_)) + } else { + false } } pub fn is_image(&self) -> bool { - match self.cell_value_at(0, 0) { - Some(code_cell_value) => code_cell_value.is_image(), - None => false, + if let Value::Single(value) = &self.value { + matches!(value, CellValue::Image(_)) + } else { + false } } pub fn is_html_or_image(&self) -> bool { - self.is_html() || self.is_image() + if let Value::Single(value) = &self.value { + matches!(value, CellValue::Html(_) | CellValue::Image(_)) + } else { + false + } } /// returns a SheetRect for the output size of a code cell (defaults to 1x1) diff --git a/quadratic-core/src/grid/sheet/bounds.rs b/quadratic-core/src/grid/sheet/bounds.rs index 8b1b7a8a6b..002b6890fe 100644 --- a/quadratic-core/src/grid/sheet/bounds.rs +++ b/quadratic-core/src/grid/sheet/bounds.rs @@ -279,6 +279,8 @@ impl Sheet { /// if reverse is true it searches to the left of the start /// if with_content is true it searches for a column with content; otherwise it searches for a column without content /// + /// For charts, is uses the chart's bounds for intersection test (since charts are considered a single cell) + /// /// Returns the found column matching the criteria of with_content pub fn find_next_column( &self, @@ -296,7 +298,20 @@ impl Sheet { }; let mut x = column_start; while (reverse && x >= bounds.0) || (!reverse && x <= bounds.1) { - let has_content = self.display_value(Pos { x, y: row }); + let mut has_content = self.display_value(Pos { x, y: row }); + if !has_content.is_some() + || has_content + .as_ref() + .is_some_and(|cell_value| *cell_value == CellValue::Blank) + { + if self.table_intersects(x, row, Some(column_start), None) { + // we use a dummy CellValue::Logical to share that there is + // content here (so we don't have to check for the actual + // Table content--as it's not really needed except for a + // Blank check) + has_content = Some(CellValue::Logical(true)); + } + } if has_content.is_some_and(|cell_value| cell_value != CellValue::Blank) { if with_content { return Some(x); @@ -306,8 +321,11 @@ impl Sheet { } x += if reverse { -1 } else { 1 }; } - let has_content = self.display_value(Pos { x, y: row }); - if with_content == has_content.is_some() { + + // final check when we've exited the loop + let has_content = self.display_value(Pos { x, y: row }).is_some() + || self.table_intersects(x, row, Some(column_start), None); + if with_content == has_content { Some(x) } else { None @@ -331,7 +349,16 @@ impl Sheet { }; let mut y = row_start; while (reverse && y >= bounds.0) || (!reverse && y <= bounds.1) { - let has_content = self.display_value(Pos { x: column, y }); + let mut has_content = self.display_value(Pos { x: column, y }); + if !has_content.is_some() { + // we use a dummy CellValue::Logical to share that there is + // content here (so we don't have to check for the actual + // Table content--as it's not really needed except for a + // Blank check) + if self.table_intersects(column, y, None, Some(row_start)) { + has_content = Some(CellValue::Logical(true)); + } + } if has_content.is_some_and(|cell_value| cell_value != CellValue::Blank) { if with_content { return Some(y); @@ -341,8 +368,11 @@ impl Sheet { } y += if reverse { -1 } else { 1 }; } - let has_content = self.display_value(Pos { x: column, y }); - if with_content == has_content.is_some() { + + // final check when we've exited the loop + let has_content = self.display_value(Pos { x: column, y }).is_some() + || self.table_intersects(column, y, None, Some(row_start)); + if with_content == has_content { Some(y) } else { None @@ -397,10 +427,11 @@ mod test { validation::Validation, validation_rules::{validation_logical::ValidationLogical, ValidationRule}, }, - BorderSelection, BorderStyle, CellAlign, CellWrap, CodeCellLanguage, GridBounds, Sheet, + BorderSelection, BorderStyle, CellAlign, CellWrap, CodeCellLanguage, CodeRun, + DataTable, DataTableKind, GridBounds, Sheet, }, selection::Selection, - CellValue, Pos, Rect, SheetPos, SheetRect, + CellValue, Pos, Rect, SheetPos, SheetRect, Value, }; use proptest::proptest; use serial_test::parallel; @@ -979,4 +1010,32 @@ mod test { // Check that the data bounds are still empty assert_eq!(sheet.data_bounds, GridBounds::Empty); } + + #[test] + #[parallel] + fn find_next_column_with_table() { + let mut sheet = Sheet::test(); + let mut dt = DataTable::new( + DataTableKind::CodeRun(CodeRun::default()), + "test", + Value::Single(CellValue::Html("html".to_string())), + false, + false, + true, + // make the chart take up 5x5 cells + Some((100.0 * 5.0, 20.0 * 5.0)), + ); + dt.chart_output = Some((5, 5)); + sheet.set_data_table(Pos { x: 5, y: 5 }, Some(dt)); + sheet.recalculate_bounds(); + + // should find the anchor of the table + assert_eq!(sheet.find_next_column(1, 5, false, true), Some(5)); + + // should find the chart-sized table + assert_eq!(sheet.find_next_column(1, 7, false, true), Some(5)); + + // should not find the table if we're already inside the table + assert_eq!(sheet.find_next_column(6, 5, false, true), None); + } } diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index 28cca848bb..f48c653d0a 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -69,6 +69,39 @@ impl Sheet { None => bail!("No data tables found within {:?}", pos), } } + + /// Checks whether a table intersects a position. We ignore the table if it + /// includes either exclude_x or exclude_y. + pub fn table_intersects( + &self, + x: i64, + y: i64, + exclude_x: Option, + exclude_y: Option, + ) -> bool { + self.data_tables.iter().any(|(data_table_pos, data_table)| { + // we only care about html or image tables + if !data_table.is_html_or_image() { + return false; + } + let output_rect = data_table.output_rect(*data_table_pos, false); + if output_rect.contains(Pos { x, y }) { + if let Some(exclude_x) = exclude_x { + if exclude_x >= output_rect.min.x && exclude_x <= output_rect.max.x { + return false; + } + } + if let Some(exclude_y) = exclude_y { + if exclude_y >= output_rect.min.y && exclude_y <= output_rect.max.y { + return false; + } + } + true + } else { + false + } + }) + } } #[cfg(test)] From 909f26d7ecdbe30f5fac24f0835a9e490aa529d0 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 7 Nov 2024 10:26:45 -0800 Subject: [PATCH 221/373] fix bug with find_next_row --- quadratic-core/src/grid/sheet.rs | 66 +++++++++++- quadratic-core/src/grid/sheet/bounds.rs | 131 ++++++++++++++++++++++-- quadratic-core/src/grid/sheet/code.rs | 11 +- 3 files changed, 199 insertions(+), 9 deletions(-) diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index c768924d4e..513aa2db0c 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -216,6 +216,19 @@ impl Sheet { .flat_map(|(pos, data_table)| data_table.code_run().map(|code_run| (pos, code_run))) } + /// Returns true if the cell at Pos has content (ie, not blank). Also checks + /// tables. Ignores Blanks. + pub fn has_content(&self, pos: Pos) -> bool { + if self + .get_column(pos.x) + .and_then(|column| column.values.get(&pos.y)) + .is_some_and(|cell_value| !cell_value.is_blank_or_empty_string()) + { + return true; + } + self.has_table_content(pos) + } + /// Returns the cell_value at a Pos using both column.values and data_tables (i.e., what would be returned if code asked /// for it). pub fn display_value(&self, pos: Pos) -> Option { @@ -521,10 +534,10 @@ mod test { use crate::controller::GridController; use crate::grid::formats::format_update::FormatUpdate; use crate::grid::formats::Formats; - use crate::grid::{Bold, CodeCellLanguage, Italic, NumericFormat}; + use crate::grid::{Bold, CodeCellLanguage, DataTableKind, Italic, NumericFormat}; use crate::selection::Selection; use crate::test_util::print_table; - use crate::{CodeCellValue, SheetPos}; + use crate::{CodeCellValue, SheetPos, Value}; fn test_setup(selection: &Rect, vals: &[&str]) -> (GridController, SheetId) { let mut grid_controller = GridController::test(); @@ -1125,4 +1138,53 @@ mod test { }) ); } + + #[test] + #[parallel] + fn test_has_content() { + let mut sheet = Sheet::test(); + let pos = Pos { x: 0, y: 0 }; + + // Empty cell should have no content + assert!(!sheet.has_content(pos)); + + // Text content + sheet.set_cell_value(pos, "test"); + assert!(sheet.has_content(pos)); + + // Blank value should count as no content + sheet.set_cell_value(pos, CellValue::Blank); + assert!(!sheet.has_content(pos)); + + // Empty string should count as no content + sheet.set_cell_value(pos, ""); + assert!(!sheet.has_content(pos)); + + // Number content + sheet.set_cell_value(pos, CellValue::Text("test".to_string())); + assert!(sheet.has_content(pos)); + + // Table content + let dt = DataTable::new( + DataTableKind::CodeRun(CodeRun::default()), + "test", + Value::Array(Array::from(vec![vec!["test", "test"]])), + false, + false, + true, + None, + ); + sheet.data_tables.insert(pos, dt.clone()); + assert!(sheet.has_content(pos)); + assert!(sheet.has_content(Pos { x: 1, y: 1 })); + assert!(!sheet.has_content(Pos { x: 2, y: 1 })); + + let mut dt = dt.clone(); + dt.chart_output = Some((5, 5)); + sheet.data_tables.insert(pos, dt); + assert!(sheet.has_content(pos)); + assert!(sheet.has_content(Pos { x: 1, y: 1 })); + assert!(sheet.has_content(Pos { x: 5, y: 1 })); + assert!(!sheet.has_content(Pos { x: 6, y: 1 })); + } } diff --git a/quadratic-core/src/grid/sheet/bounds.rs b/quadratic-core/src/grid/sheet/bounds.rs index 002b6890fe..bf76bdaeb9 100644 --- a/quadratic-core/src/grid/sheet/bounds.rs +++ b/quadratic-core/src/grid/sheet/bounds.rs @@ -350,7 +350,11 @@ impl Sheet { let mut y = row_start; while (reverse && y >= bounds.0) || (!reverse && y <= bounds.1) { let mut has_content = self.display_value(Pos { x: column, y }); - if !has_content.is_some() { + if has_content.is_none() + || has_content + .as_ref() + .is_some_and(|cell_value| *cell_value == CellValue::Blank) + { // we use a dummy CellValue::Logical to share that there is // content here (so we don't have to check for the actual // Table content--as it's not really needed except for a @@ -431,7 +435,7 @@ mod test { DataTable, DataTableKind, GridBounds, Sheet, }, selection::Selection, - CellValue, Pos, Rect, SheetPos, SheetRect, Value, + CellValue, CodeCellValue, Pos, Rect, SheetPos, SheetRect, Value, }; use proptest::proptest; use serial_test::parallel; @@ -1011,10 +1015,7 @@ mod test { assert_eq!(sheet.data_bounds, GridBounds::Empty); } - #[test] - #[parallel] - fn find_next_column_with_table() { - let mut sheet = Sheet::test(); + fn chart_5x5_dt() -> DataTable { let mut dt = DataTable::new( DataTableKind::CodeRun(CodeRun::default()), "test", @@ -1026,16 +1027,134 @@ mod test { Some((100.0 * 5.0, 20.0 * 5.0)), ); dt.chart_output = Some((5, 5)); + dt + } + + #[test] + #[parallel] + fn find_next_column_with_table() { + let mut sheet = Sheet::test(); + let dt = chart_5x5_dt(); + sheet.set_cell_value( + Pos { x: 5, y: 5 }, + CellValue::Code(CodeCellValue::new( + CodeCellLanguage::Javascript, + "".to_string(), + )), + ); sheet.set_data_table(Pos { x: 5, y: 5 }, Some(dt)); sheet.recalculate_bounds(); // should find the anchor of the table assert_eq!(sheet.find_next_column(1, 5, false, true), Some(5)); + // ensure we're not finding the table if we're in the wrong row + assert_eq!(sheet.find_next_column(1, 1, false, true), None); + // should find the chart-sized table assert_eq!(sheet.find_next_column(1, 7, false, true), Some(5)); // should not find the table if we're already inside the table assert_eq!(sheet.find_next_column(6, 5, false, true), None); } + + #[test] + #[parallel] + fn find_next_column_with_two_tables() { + let mut sheet = Sheet::test(); + sheet.set_cell_value( + Pos { x: 5, y: 5 }, + CellValue::Code(CodeCellValue::new( + CodeCellLanguage::Javascript, + "".to_string(), + )), + ); + sheet.set_data_table(Pos { x: 5, y: 5 }, Some(chart_5x5_dt())); + sheet.set_cell_value( + Pos { x: 20, y: 5 }, + CellValue::Code(CodeCellValue::new( + CodeCellLanguage::Javascript, + "".to_string(), + )), + ); + sheet.set_data_table(Pos { x: 20, y: 5 }, Some(chart_5x5_dt())); + sheet.recalculate_bounds(); + + // should find the first table + assert_eq!(sheet.find_next_column(1, 6, false, true), Some(5)); + + // should find the second table even though we're inside the first + assert_eq!(sheet.find_next_column(6, 6, false, true), Some(20)); + + // should find the second table moving backwards + assert_eq!(sheet.find_next_column(30, 6, true, true), Some(24)); + + // should find the first table moving backwards even though we're inside + // the second table + assert_eq!(sheet.find_next_column(24, 6, true, true), Some(9)); + } + + #[test] + #[parallel] + fn find_next_row_with_table() { + let mut sheet = Sheet::test(); + let dt = chart_5x5_dt(); + sheet.set_cell_value( + Pos { x: 5, y: 5 }, + CellValue::Code(CodeCellValue::new( + CodeCellLanguage::Javascript, + "".to_string(), + )), + ); + sheet.set_data_table(Pos { x: 5, y: 5 }, Some(dt)); + sheet.recalculate_bounds(); + + // should find the anchor of the table + assert_eq!(sheet.find_next_row(1, 5, false, true), Some(5)); + + // ensure we're not finding the table if we're in the wrong column + assert_eq!(sheet.find_next_column(1, 1, false, true), None); + + // should find the chart-sized table + assert_eq!(sheet.find_next_row(1, 7, false, true), Some(5)); + + // should not find the table if we're already inside the table + assert_eq!(sheet.find_next_row(6, 6, false, true), None); + } + + #[test] + #[parallel] + fn find_next_row_with_two_tables() { + let mut sheet = Sheet::test(); + sheet.set_cell_value( + Pos { x: 5, y: 5 }, + CellValue::Code(CodeCellValue::new( + CodeCellLanguage::Javascript, + "".to_string(), + )), + ); + sheet.set_data_table(Pos { x: 5, y: 5 }, Some(chart_5x5_dt())); + sheet.set_cell_value( + Pos { x: 5, y: 20 }, + CellValue::Code(CodeCellValue::new( + CodeCellLanguage::Javascript, + "".to_string(), + )), + ); + sheet.set_data_table(Pos { x: 5, y: 20 }, Some(chart_5x5_dt())); + sheet.recalculate_bounds(); + + // should find the first table + assert_eq!(sheet.find_next_row(1, 6, false, true), Some(5)); + + // should find the second table even though we're inside the first + assert_eq!(sheet.find_next_row(6, 6, false, true), Some(20)); + + // should find the second table moving backwards + assert_eq!(sheet.find_next_row(30, 6, true, true), Some(24)); + + // should find the first table moving backwards even though we're inside + // the second table + assert_eq!(sheet.find_next_row(24, 6, true, true), Some(9)); + } } diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 12304581d8..f8a1d87dba 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -12,7 +12,7 @@ use crate::{ }; impl Sheet { - /// Gets column bounds for data_tables that output to the columns + /// Gets column bounds (ie, a range of rows) for data_tables that output to the columns pub fn code_columns_bounds(&self, column_start: i64, column_end: i64) -> Option> { let mut min: Option = None; let mut max: Option = None; @@ -56,6 +56,15 @@ impl Sheet { } } + /// Returns true if the tables contain any cell at Pos (ie, not blank). Uses + /// the DataTable's output_rect for the check to ensure that charts are + /// included. + pub fn has_table_content(&self, pos: Pos) -> bool { + self.data_tables.iter().any(|(code_cell_pos, data_table)| { + data_table.output_rect(*code_cell_pos, false).contains(pos) + }) + } + /// Returns the CellValue for a CodeRun (if it exists) at the Pos. /// /// Note: spill error will return a CellValue::Blank to ensure calculations can continue. From 3b8d8c28cfaa1b19f8f31df09db633d78c1bf71d Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 7 Nov 2024 11:57:05 -0700 Subject: [PATCH 222/373] Abstract display_columns and add transmute_index() --- .../execute_operation/execute_data_table.rs | 1 - quadratic-core/src/grid/data_table/column.rs | 17 +++- .../src/grid/data_table/display_value.rs | 82 ++++++++++++++----- 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index c10a356424..0446a06a4d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -5,7 +5,6 @@ use crate::{ operations::operation::Operation, GridController, }, grid::{DataTable, DataTableKind}, - util::dbgjs, ArraySize, CellValue, Pos, Rect, SheetRect, }; diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index 9aea715a11..2befabcde3 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -161,7 +161,10 @@ pub mod test { use super::*; use crate::{ cellvalue::Import, - grid::{test::new_data_table, DataTableKind, Sheet}, + grid::{ + test::{new_data_table, pretty_print_data_table}, + DataTableKind, Sheet, + }, Array, Pos, }; use chrono::Utc; @@ -265,4 +268,16 @@ pub mod test { Some(CellValue::Text("second".into())) ); } + + #[test] + #[parallel] + fn test_hide_column() { + let (_, mut data_table) = new_data_table(); + data_table.apply_first_row_as_header(); + let mut columns = data_table.columns.clone().unwrap(); + columns[0].display = false; + data_table.columns = Some(columns); + + pretty_print_data_table(&data_table, None, None); + } } diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs index 1bbd760083..816189d85f 100644 --- a/quadratic-core/src/grid/data_table/display_value.rs +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -16,30 +16,17 @@ use anyhow::{anyhow, Ok, Result}; use super::DataTable; impl DataTable { + /// Get the display value from the display buffer. pub fn display_value_from_buffer(&self, display_buffer: &[u64]) -> Result { let value = self.value.to_owned().into_array()?; - let columns_to_show = self - .columns - .iter() - .flatten() - .enumerate() - .filter(|(_, c)| c.display) - .map(|(index, _)| index) - .collect::>(); + let columns_to_show = self.columns_to_show(); let values = display_buffer .iter() .filter_map(|index| { value .get_row(*index as usize) - .map(|row| { - row.to_vec() - .into_iter() - .enumerate() - .filter(|(i, _)| columns_to_show.contains(&i)) - .map(|(_, v)| v) - .collect::>() - }) + .map(|row| self.display_columns(&columns_to_show, row)) .ok() }) .collect::>>(); @@ -49,6 +36,7 @@ impl DataTable { Ok(array.into()) } + /// Get the display value from the display buffer at a given position. pub fn display_value_from_buffer_at( &self, display_buffer: &[u64], @@ -62,22 +50,43 @@ impl DataTable { Ok(cell_value) } + /// Get the display value from the source valuer. + pub fn display_value_from_value(&self) -> Result { + let columns_to_show = self.columns_to_show(); + + let values = self + .value + .to_owned() + .into_array()? + .rows() + .map(|row| { + row.to_vec() + .into_iter() + .enumerate() + .filter(|(i, _)| columns_to_show.contains(&i)) + .map(|(_, v)| v) + .collect::>() + }) + .collect::>>(); + let array = Array::from(values); + + Ok(array.into()) + } + + /// Get the display value from the display buffer, falling back to the + /// source value if the display buffer is not set. pub fn display_value(&self) -> Result { match self.display_buffer { Some(ref display_buffer) => self.display_value_from_buffer(display_buffer), - None => Ok(self.value.to_owned()), + None => self.display_value_from_value(), } } + /// Get the display value at a given position. pub fn display_value_at(&self, mut pos: Pos) -> Result<&CellValue> { - // println!("pos: {:?}", pos); - // println!("self.columns: {:?}", self.columns); - if pos.y == 0 && self.show_header { if let Some(columns) = &self.columns { - // println!("columns: {:?}", columns); if let Some(column) = columns.get(pos.x as usize) { - // println!("column: {:?}", column); return Ok(column.name.as_ref()); } } @@ -92,6 +101,35 @@ impl DataTable { None => Ok(self.value.get(pos.x as u32, pos.y as u32)?), } } + + /// Get the indices of the columns to show. + pub fn columns_to_show(&self) -> Vec { + self.columns + .iter() + .flatten() + .enumerate() + .filter(|(_, c)| c.display) + .map(|(index, _)| index) + .collect::>() + } + + /// For a given row of CellValues, return only the columns that should be displayed + pub fn display_columns(&self, columns_to_show: &[usize], row: &[CellValue]) -> Vec { + row.to_vec() + .into_iter() + .enumerate() + .filter(|(i, _)| columns_to_show.contains(&i)) + .map(|(_, v)| v) + .collect::>() + } + + /// Transmute an index from the display buffer to the source index. + pub fn transmute_index(&self, index: u64) -> u64 { + match self.display_buffer { + Some(ref display_buffer) => *display_buffer.get(index as usize).unwrap_or(&index), + None => index, + } + } } #[cfg(test)] From d510fa38b394478eee774289c399412015cad711 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 7 Nov 2024 12:10:34 -0700 Subject: [PATCH 223/373] Finish hide column testing --- quadratic-core/src/grid/data_table/column.rs | 17 +------------ .../src/grid/data_table/display_value.rs | 24 ++++++++++++++++++- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index 2befabcde3..9aea715a11 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -161,10 +161,7 @@ pub mod test { use super::*; use crate::{ cellvalue::Import, - grid::{ - test::{new_data_table, pretty_print_data_table}, - DataTableKind, Sheet, - }, + grid::{test::new_data_table, DataTableKind, Sheet}, Array, Pos, }; use chrono::Utc; @@ -268,16 +265,4 @@ pub mod test { Some(CellValue::Text("second".into())) ); } - - #[test] - #[parallel] - fn test_hide_column() { - let (_, mut data_table) = new_data_table(); - data_table.apply_first_row_as_header(); - let mut columns = data_table.columns.clone().unwrap(); - columns[0].display = false; - data_table.columns = Some(columns); - - pretty_print_data_table(&data_table, None, None); - } } diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs index ee79f43ee1..da6e8636b8 100644 --- a/quadratic-core/src/grid/data_table/display_value.rs +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -143,7 +143,12 @@ pub mod test { use crate::{ controller::{transaction_types::JsCodeResult, GridController}, - grid::CodeCellLanguage, + grid::{ + test::{ + assert_data_table_row, new_data_table, pretty_print_data_table, test_csv_values, + }, + CodeCellLanguage, + }, CellValue, Pos, SheetPos, }; @@ -185,4 +190,21 @@ pub mod test { CellValue::Blank ) } + + #[test] + #[parallel] + fn test_hide_column() { + let (_, mut data_table) = new_data_table(); + data_table.apply_first_row_as_header(); + let mut columns = data_table.columns.clone().unwrap(); + columns[0].display = false; + data_table.columns = Some(columns); + + pretty_print_data_table(&data_table, None, None); + + let mut values = test_csv_values(); + values[0].remove(0); + + assert_data_table_row(&data_table, 0, values[0].clone()); + } } From ebac40a9c603ed7128668e8534e638cdd1f95aea Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 7 Nov 2024 13:27:18 -0800 Subject: [PATCH 224/373] WIP jumpCursor port to Rust --- quadratic-core/src/grid/sheet.rs | 1 + quadratic-core/src/grid/sheet/code.rs | 39 +++++ quadratic-core/src/grid/sheet/offsets.rs | 189 +++++++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 quadratic-core/src/grid/sheet/offsets.rs diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 513aa2db0c..fe2a3ca191 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -31,6 +31,7 @@ pub mod col_row; pub mod data_table; pub mod formats; pub mod formatting; +pub mod offsets; pub mod rendering; pub mod rendering_date_time; pub mod row_resize; diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index f8a1d87dba..15b286397c 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -56,6 +56,14 @@ impl Sheet { } } + /// Returns the DataTable that overlaps the Pos if it is an HTML or image chart. + pub fn chart_at(&self, pos: Pos) -> Option<(&Pos, &DataTable)> { + self.data_tables.iter().find(|(code_cell_pos, data_table)| { + data_table.is_html_or_image() + && data_table.output_rect(**code_cell_pos, false).contains(pos) + }) + } + /// Returns true if the tables contain any cell at Pos (ie, not blank). Uses /// the DataTable's output_rect for the check to ensure that charts are /// included. @@ -442,4 +450,35 @@ mod test { assert_eq!(sheet.code_rows_bounds(2, 5), Some(3..6)); assert_eq!(sheet.code_rows_bounds(10, 10), None); } + + #[test] + #[parallel] + fn chart_at() { + let mut sheet = Sheet::test(); + assert_eq!(sheet.chart_at(Pos { x: 1, y: 1 }), None); + + let mut dt = DataTable::new( + DataTableKind::CodeRun(CodeRun::default()), + "Table 1", + CellValue::Html("".to_string()).into(), + false, + false, + false, + None, + ); + dt.chart_output = Some((2, 2)); + + let pos = Pos { x: 1, y: 1 }; + sheet.set_cell_value( + pos, + CellValue::Code(CodeCellValue { + code: "".to_string(), + language: CodeCellLanguage::Javascript, + }), + ); + sheet.set_data_table(pos, Some(dt.clone())); + + assert_eq!(sheet.chart_at(pos), Some((&pos, &dt))); + assert_eq!(sheet.chart_at(Pos { x: 2, y: 2 }), Some((&pos, &dt))); + } } diff --git a/quadratic-core/src/grid/sheet/offsets.rs b/quadratic-core/src/grid/sheet/offsets.rs new file mode 100644 index 0000000000..3d5e185a06 --- /dev/null +++ b/quadratic-core/src/grid/sheet/offsets.rs @@ -0,0 +1,189 @@ +//! Handles the logic for jumping between cells (ctrl/cmd + arrow key). +//! +//! Algorithm: +//! - if on an empty cell then select to the first cell with a value +//! - if on a filled cell then select to the cell before the next empty cell +//! - if on a filled cell but the next cell is empty then select to the first +//! cell with a value +//! - if there are no more cells then select the next cell over (excel selects +//! to the end of the sheet; we don’t have an end (yet) so right now I select +//! one cell over) +//! +//! The above checks are always made relative to the original cursor position +//! (the highlighted cell) + +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::Pos; + +use super::Sheet; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, TS)] +pub enum JumpDirection { + Up, + Down, + Left, + Right, +} + +impl Sheet { + fn jump_up(&self, current: Pos) -> Pos { + current + } + + fn jump_down(&self, current: Pos) -> Pos { + current + } + + fn jump_left(&self, current: Pos) -> Pos { + let mut x = current.x; + let y = current.y; + + // adjust the jump position if it is inside a chart to the left-most + // edge of the chart + if let Some((chart_pos, dt)) = self.chart_at(Pos { x, y }) { + if let Some((_, h)) = dt.chart_output { + x = chart_pos.x; + } + } + + // handle case of cell with content + let mut prev_x: Option = None; + if self.has_content(Pos { x, y }) { + // if previous cell is empty, find the next cell with content + if x - 1 == 0 { + return Pos { x: 1, y }; + } + if !self.has_content(Pos { x: x - 1, y }) { + prev_x = self.find_prev_column(x - 2, y, true, true); + } + } + } + + fn jump_right(&self, current: Pos) -> Pos { + let mut x = current.x; + let y = current.y; + + // adjust the jump position if it is inside a chart to the right-most + // edge of the chart + if let Some((chart_pos, dt)) = self.chart_at(Pos { x, y }) { + if let Some((w, _)) = dt.chart_output { + x = chart_pos.x + (w as i64) - 1; + } + } + + // handle case of cell with content + let mut next_x: Option = None; + if self.has_content(Pos { x, y }) { + // if next cell is empty, find the next cell with content + if !self.has_content(Pos { x: x + 1, y }) { + if let Some(next) = self.find_next_column(x + 2, y, false, true) { + next_x = Some(next); + } + } + // if next cell is not empty, find the next empty cell + else { + if let Some(next) = self.find_next_column(x + 2, y, false, false) { + next_x = Some(next - 1); + } else { + next_x = Some(x + 1); + } + } + } + // otherwise find the next cell with content + else { + if let Some(next) = self.find_next_column(x + 1, y, false, true) { + next_x = Some(next); + } + } + + x = if let Some(next_x) = next_x { + next_x + } else { + x + 1 + }; + + Pos { x, y } + } + + /// Returns the Pos after a jump (ctrl/cmd + arrow key) + pub fn jump_cursor(&self, current: Pos, direction: JumpDirection) -> Pos { + match direction { + JumpDirection::Up => self.jump_up(current), + JumpDirection::Down => self.jump_down(current), + JumpDirection::Left => self.jump_left(current), + JumpDirection::Right => self.jump_right(current), + } + } +} + +#[cfg(test)] +mod tests { + use serial_test::parallel; + + use crate::{ + grid::{CodeCellLanguage, CodeRun, DataTable, DataTableKind}, + CellValue, CodeCellValue, + }; + + use super::*; + + // creates a 2x2 chart at the given position + fn create_2x2_chart(sheet: &mut Sheet, pos: Pos) { + let mut dt = DataTable::new( + DataTableKind::CodeRun(CodeRun::default()), + "Table 1", + CellValue::Html("".to_string()).into(), + false, + false, + false, + None, + ); + dt.chart_output = Some((3, 3)); + + sheet.set_cell_value( + pos, + CellValue::Code(CodeCellValue { + code: "".to_string(), + language: CodeCellLanguage::Javascript, + }), + ); + sheet.set_data_table(pos, Some(dt)); + } + + #[test] + #[parallel] + fn jump_right_empty() { + let sheet = Sheet::test(); + + let pos = Pos { x: 1, y: 1 }; + let new_pos = sheet.jump_right(pos); + assert_eq!(new_pos, Pos { x: 2, y: 1 }); + let new_pos = sheet.jump_right(Pos { x: 2, y: 2 }); + assert_eq!(new_pos, Pos { x: 3, y: 2 }); + } + + #[test] + #[parallel] + fn jump_right_filled() { + let mut sheet = Sheet::test(); + + sheet.set_cell_value(Pos { x: 1, y: 1 }, CellValue::Number(1.into())); + sheet.set_cell_value(Pos { x: 5, y: 1 }, CellValue::Number(1.into())); + sheet.set_cell_value(Pos { x: 10, y: 1 }, CellValue::Number(1.into())); + + create_2x2_chart(&mut sheet, Pos { x: 20, y: 1 }); + create_2x2_chart(&mut sheet, Pos { x: 25, y: 1 }); + + assert_eq!(sheet.jump_right(Pos { x: 0, y: 1 }), Pos { x: 1, y: 1 }); + assert_eq!(sheet.jump_right(Pos { x: 1, y: 1 }), Pos { x: 5, y: 1 }); + assert_eq!(sheet.jump_right(Pos { x: 5, y: 1 }), Pos { x: 10, y: 1 }); + + // jump to the first cell in the first chart + assert_eq!(sheet.jump_right(Pos { x: 10, y: 1 }), Pos { x: 20, y: 1 }); + + // jump to the first cell in the second chart + assert_eq!(sheet.jump_right(Pos { x: 20, y: 1 }), Pos { x: 25, y: 1 }); + } +} From a5ce2b9ac4ffb07307bb50ee1d6db15f236825d5 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 7 Nov 2024 17:39:27 -0700 Subject: [PATCH 225/373] Add column hiding + tests --- .../src/app/actions/dataTableSpec.ts | 6 +- .../web-workers/quadraticCore/worker/core.ts | 1 - .../execute_operation/execute_data_table.rs | 9 +- quadratic-core/src/grid/data_table/column.rs | 1 + .../src/grid/data_table/display_value.rs | 116 +++++++++++++++--- quadratic-core/src/grid/data_table/mod.rs | 8 +- quadratic-core/src/grid/sheet/code.rs | 6 +- quadratic-core/src/grid/sheet/rendering.rs | 1 + .../wasm_bindings/controller/data_table.rs | 7 +- 9 files changed, 123 insertions(+), 32 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 30c0b1ce60..660ef43e37 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -247,10 +247,10 @@ export const dataTableSpec: DataTableSpec = { Icon: HideIcon, run: () => { const table = getTable(); - const columns = getColumns(); + const columns = JSON.parse(JSON.stringify(getColumns())); const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; - if (table && columns && selectedColumn) { + if (table && columns && selectedColumn !== undefined && columns[selectedColumn]) { columns[selectedColumn].display = false; quadraticCore.dataTableMeta( @@ -271,7 +271,7 @@ export const dataTableSpec: DataTableSpec = { Icon: ShowIcon, run: () => { const table = getTable(); - const columns = getColumns(); + let columns = getColumns(); if (table && columns) { columns.forEach((column) => { diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index b8747d25c4..c75e9837bb 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1155,7 +1155,6 @@ class Core { cursor: string ) { if (!this.gridController) throw new Error('Expected gridController to be defined'); - console.log('sortDataTable', sheetId, x, y, sort, cursor); this.gridController.sortDataTable(sheetId, posToPos(x, y), JSON.stringify(sort), cursor); } diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 0446a06a4d..aa112c950b 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -120,13 +120,10 @@ impl GridController { let data_table = sheet.data_table_result(data_table_pos)?; - if let Some(display_buffer) = &data_table.display_buffer { - // if there is a display buffer, use it to find the source row index + // if there is a display buffer, use it to find the source row index + if data_table.display_buffer.is_some() { let index_to_find = pos.y - data_table_pos.y; - let row_index = *display_buffer - .get(index_to_find as usize) - .unwrap_or(&(pos.y as u64)); - + let row_index = data_table.transmute_index(index_to_find as u64); pos.y = row_index as i64 + data_table_pos.y; } diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index 9aea715a11..650012488b 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -142,6 +142,7 @@ impl DataTable { match self.columns.as_ref() { Some(columns) => columns .iter() + .filter(|column| column.display) .map(|column| JsDataTableColumn::from(column.to_owned())) .collect(), // TODO(ddimaria): refacor this to use the default columns diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs index da6e8636b8..07885940f9 100644 --- a/quadratic-core/src/grid/data_table/display_value.rs +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -12,6 +12,7 @@ use crate::{Array, CellValue, Pos, Value}; use anyhow::{anyhow, Ok, Result}; +use arrow_array::ArrowNativeTypeOp; use super::DataTable; @@ -45,12 +46,16 @@ impl DataTable { let y = display_buffer .get(pos.y as usize) .ok_or_else(|| anyhow!("Y {} out of bounds: {}", pos.y, display_buffer.len()))?; - let cell_value = self.value.get(pos.x as u32, *y as u32)?; + let new_pos = Pos { + x: pos.x, + y: *y as i64, + }; + let cell_value = self.display_value_from_value_at(new_pos)?; Ok(cell_value) } - /// Get the display value from the source valuer. + /// Get the display value from the source value. pub fn display_value_from_value(&self) -> Result { let columns_to_show = self.columns_to_show(); @@ -59,20 +64,39 @@ impl DataTable { .to_owned() .into_array()? .rows() - .map(|row| { - row.to_vec() - .into_iter() - .enumerate() - .filter(|(i, _)| columns_to_show.contains(&i)) - .map(|(_, v)| v) - .collect::>() - }) + .map(|row| self.display_columns(&columns_to_show, row)) .collect::>>(); let array = Array::from(values); Ok(array.into()) } + /// Get the display value from the source value at a given position. + pub fn display_value_from_value_at(&self, pos: Pos) -> Result<&CellValue> { + let output_size = self.output_size(); + let x = pos.x as u32; + let y = pos.y as u32; + let mut new_x = x; + + // if the x position is out of bounds, return a blank value + if x >= output_size.w.get() { + return Ok(&CellValue::Blank); + } + + let columns = self.columns.iter().flatten().collect::>(); + + // increase the x position if the column before it is not displayed + for i in 0..columns.len() { + if !columns[i].display && i <= x as usize { + new_x = new_x.add_checked(1).unwrap_or(new_x); + } + } + + let cell_value = self.value.get(new_x, y)?; + + Ok(cell_value) + } + /// Get the display value from the display buffer, falling back to the /// source value if the display buffer is not set. pub fn display_value(&self) -> Result { @@ -91,7 +115,9 @@ impl DataTable { if pos.y == 0 && self.show_header { if let Some(columns) = &self.columns { - if let Some(column) = columns.get(pos.x as usize) { + let display_columns = columns.iter().filter(|c| c.display).collect::>(); + + if let Some(column) = display_columns.get(pos.x as usize) { return Ok(column.name.as_ref()); } } @@ -103,7 +129,7 @@ impl DataTable { match self.display_buffer { Some(ref display_buffer) => self.display_value_from_buffer_at(display_buffer, pos), - None => Ok(self.value.get(pos.x as u32, pos.y as u32)?), + None => self.display_value_from_value_at(pos), } } @@ -147,7 +173,7 @@ pub mod test { test::{ assert_data_table_row, new_data_table, pretty_print_data_table, test_csv_values, }, - CodeCellLanguage, + CodeCellLanguage, DataTable, }, CellValue, Pos, SheetPos, }; @@ -196,15 +222,73 @@ pub mod test { fn test_hide_column() { let (_, mut data_table) = new_data_table(); data_table.apply_first_row_as_header(); + let width = data_table.output_size().w.get(); let mut columns = data_table.columns.clone().unwrap(); - columns[0].display = false; - data_table.columns = Some(columns); pretty_print_data_table(&data_table, None, None); + // validate display_value() + columns[0].display = false; + data_table.columns = Some(columns.clone()); let mut values = test_csv_values(); values[0].remove(0); - assert_data_table_row(&data_table, 0, values[0].clone()); + + // reset values + columns[0].display = true; + data_table.columns = Some(columns.clone()); + + let remove_column = |remove_at: usize, row: u32, data_table: &mut DataTable| { + let mut column = columns.clone(); + column[remove_at].display = false; + data_table.columns = Some(column); + + let title = Some(format!("Remove column {}", remove_at)); + pretty_print_data_table(&data_table, title.as_deref(), None); + + let expected_output_width = data_table.columns_to_show().len(); + assert_eq!( + data_table.output_size().w.get(), + expected_output_width as u32 + ); + + (0..width) + .map(|x| { + data_table + .display_value_at((x, row).into()) + .unwrap() + .to_string() + }) + .collect::>() + }; + + // validate display_value_at() + let remove_city = remove_column(0, 0, &mut data_table); + assert_eq!(remove_city, vec!["region", "country", "population", ""]); + + let remove_region = remove_column(1, 0, &mut data_table); + assert_eq!(remove_region, vec!["city", "country", "population", ""]); + + let remove_county = remove_column(2, 0, &mut data_table); + assert_eq!(remove_county, vec!["city", "region", "population", ""]); + + let remove_population = remove_column(3, 0, &mut data_table); + assert_eq!(remove_population, vec!["city", "region", "country", ""]); + + // "Southborough", "MA", "United States", "1000" + let remove_city = remove_column(0, 1, &mut data_table); + assert_eq!(remove_city, vec!["MA", "United States", "1000", ""]); + + let remove_city = remove_column(1, 1, &mut data_table); + assert_eq!( + remove_city, + vec!["Southborough", "United States", "1000", ""] + ); + + let remove_city = remove_column(2, 1, &mut data_table); + assert_eq!(remove_city, vec!["Southborough", "MA", "1000", ""]); + + let remove_city = remove_column(3, 1, &mut data_table); + assert_eq!(remove_city, vec!["Southborough", "MA", "United States", ""]); } } diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index d6c98eb63c..688fba5c0e 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -257,15 +257,19 @@ impl DataTable { if w == 0 || h == 0 { ArraySize::_1X1 } else { - ArraySize::new(w, h).unwrap() + ArraySize::new(w, h).unwrap_or(ArraySize::_1X1) } } else { match &self.value { Value::Array(a) => { let mut size = a.size(); + if self.show_header && !self.header_is_first_row { - size.h = NonZeroU32::new(size.h.get() + 1).unwrap(); + size.h = NonZeroU32::new(size.h.get() + 1).unwrap_or(ArraySize::_1X1.h); } + + let width = self.columns_to_show().len(); + size.w = NonZeroU32::new(width as u32).unwrap_or(ArraySize::_1X1.w); size } Value::Single(_) | Value::Tuple(_) => ArraySize::_1X1, diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index f8a1d87dba..bf4b077d03 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -74,10 +74,12 @@ impl Sheet { .iter() .find_map(|(code_cell_pos, data_table)| { if data_table.output_rect(*code_cell_pos, false).contains(pos) { - data_table.cell_value_at( + let val = data_table.cell_value_at( (pos.x - code_cell_pos.x) as u32, (pos.y - code_cell_pos.y) as u32, - ) + ); + dbgjs!(format!("get_code_cell_value: {:?}", &val)); + val } else { None } diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index a2817634b7..d57b0a4875 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -245,6 +245,7 @@ impl Sheet { (x - code_rect.min.x) as u32, (y - code_rect.min.y) as u32, ); + if let Some(value) = value { let language = if x == code_rect.min.x && y == code_rect.min.y { Some(code_cell_value.language.to_owned()) diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index 860e5c9dcb..e71bc61316 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -1,4 +1,3 @@ -use grid::data_table::column::DataTableColumn; use selection::Selection; use sort::DataTableSort; @@ -108,7 +107,11 @@ impl GridController { let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; let columns = columns_js - .map(|c| serde_json::from_str::>(&c).map_err(|e| e.to_string())) + .map(|c| { + serde_json::from_str::>(&c) + .map_err(|e| e.to_string()) + .map(|c| c.into_iter().map(|c| c.into()).collect()) + }) .transpose()?; self.data_table_meta( From 283c253a66c2753567b3eb9bb378cbc95cb26182 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 7 Nov 2024 18:30:50 -0700 Subject: [PATCH 226/373] Show all columns and rename column --- .../src/app/actions/dataTableSpec.ts | 17 +++++- .../contextMenus/TableColumnHeaderRename.tsx | 7 ++- .../HTMLGrid/contextMenus/TableMenu.tsx | 10 ++-- .../gridGL/cells/tables/TableColumnHeaders.ts | 58 ++++++++++--------- quadratic-core/src/grid/data_table/column.rs | 1 - 5 files changed, 54 insertions(+), 39 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 660ef43e37..7d86fd5246 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -42,7 +42,7 @@ type DataTableSpec = Pick< | Action.EditTableCode >; -const getTable = (): JsRenderCodeCell | undefined => { +export const getTable = (): JsRenderCodeCell | undefined => { return pixiAppSettings.contextMenu?.table ?? pixiApp.cellsSheet().cursorOnDataTable(); }; @@ -51,6 +51,11 @@ export const getColumns = (): { name: string; display: boolean; valueIndex: numb return table?.columns; }; +export const getDisplayColumns = (): { name: string; display: boolean; valueIndex: number }[] | undefined => { + const table = getTable(); + return table?.columns.filter((c) => c.display); +}; + const isHeadingShowing = (): boolean => { const table = getTable(); return !!table?.show_header; @@ -247,7 +252,7 @@ export const dataTableSpec: DataTableSpec = { Icon: HideIcon, run: () => { const table = getTable(); - const columns = JSON.parse(JSON.stringify(getColumns())); + const columns = JSON.parse(JSON.stringify(getDisplayColumns())); const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; if (table && columns && selectedColumn !== undefined && columns[selectedColumn]) { @@ -271,13 +276,19 @@ export const dataTableSpec: DataTableSpec = { Icon: ShowIcon, run: () => { const table = getTable(); - let columns = getColumns(); + const columns = JSON.parse(JSON.stringify(getColumns())) as { + name: string; + display: boolean; + valueIndex: number; + }[]; if (table && columns) { columns.forEach((column) => { column.display = true; }); + console.log('show all columns', columns); + quadraticCore.dataTableMeta( sheets.sheet.id, table.x, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx index a96840d3fe..0e27c1b0ec 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -1,4 +1,4 @@ -import { getColumns } from '@/app/actions/dataTableSpec'; +import { getDisplayColumns } from '@/app/actions/dataTableSpec'; import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; import { PixiRename } from '@/app/gridGL/HTMLGrid/contextMenus/PixiRename'; @@ -54,11 +54,12 @@ export const TableColumnHeaderRename = () => { backgroundColor: 'var(--table-column-header-background)', }} onSave={(value: string) => { - if (contextMenu.table && contextMenu.selectedColumn && pixiApp.cellsSheets.current) { - const columns = getColumns(); + if (contextMenu.table && contextMenu.selectedColumn !== undefined && pixiApp.cellsSheets.current) { + const columns = JSON.parse(JSON.stringify(getDisplayColumns())); if (columns) { columns[contextMenu.selectedColumn].name = value; + console.log('rename columns', columns); quadraticCore.dataTableMeta( pixiApp.cellsSheets.current?.sheetId, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index 0ad0225ba8..cd5d0d2602 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -1,4 +1,5 @@ import { Action } from '@/app/actions/actions'; +import { getColumns } from '@/app/actions/dataTableSpec'; import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; import { ContextMenuItem, ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; @@ -19,12 +20,13 @@ export const TableMenu = (props: Props) => { const { defaultRename, codeCell, selectedColumn } = props; const cell = getCodeCell(codeCell?.language); const isCodeCell = cell && cell.id !== 'Import'; + const hiddenColumns = useMemo(() => getColumns()?.filter((c) => !c.display), []); const hasHiddenColumns = useMemo(() => { - console.log('TODO: hasHiddenColumns', codeCell); - return false; - // return codeCell?.; - }, [codeCell]); + if (!hiddenColumns) return false; + + return hiddenColumns?.length > 0; + }, [hiddenColumns]); const isImageOrHtmlCell = useMemo(() => { if (!codeCell) return false; diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 017b45f144..0cb12922a0 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -82,40 +82,42 @@ export class TableColumnHeaders extends Container { this.columns.visible = false; return; } - while (this.columns.children.length > this.table.codeCell.columns.length) { + while (this.columns.children.length > this.table.codeCell.columns.filter((c) => c.display).length) { this.columns.children.pop(); } let x = 0; const codeCell = this.table.codeCell; - codeCell.columns.forEach((column, index) => { - const width = this.table.sheet.offsets.getColumnWidth(codeCell.x + index); - if (index >= this.columns.children.length) { - // if this is a new column, then add it - this.columns.addChild( - new TableColumnHeader({ - table: this.table, - index, + codeCell.columns + .filter((c) => c.display) + .forEach((column, index) => { + const width = this.table.sheet.offsets.getColumnWidth(codeCell.x + index); + if (index >= this.columns.children.length) { + // if this is a new column, then add it + this.columns.addChild( + new TableColumnHeader({ + table: this.table, + index, + x, + width, + height: this.headerHeight, + name: column.name, + sort: codeCell.sort?.find((s) => s.column_index === column.valueIndex), + onSortPressed: () => this.onSortPressed(column), + }) + ); + } else { + // otherwise, update the existing column header (this is needed to keep + // the sort button hover working properly) + this.columns.children[index].updateHeader( x, width, - height: this.headerHeight, - name: column.name, - sort: codeCell.sort?.find((s) => s.column_index === column.valueIndex), - onSortPressed: () => this.onSortPressed(column), - }) - ); - } else { - // otherwise, update the existing column header (this is needed to keep - // the sort button hover working properly) - this.columns.children[index].updateHeader( - x, - width, - this.height, - column.name, - codeCell.sort?.find((s) => s.column_index === column.valueIndex) - ); - } - x += width; - }); + this.height, + column.name, + codeCell.sort?.find((s) => s.column_index === column.valueIndex) + ); + } + x += width; + }); } // update appearance when there is an updated code cell diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index 650012488b..9aea715a11 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -142,7 +142,6 @@ impl DataTable { match self.columns.as_ref() { Some(columns) => columns .iter() - .filter(|column| column.display) .map(|column| JsDataTableColumn::from(column.to_owned())) .collect(), // TODO(ddimaria): refacor this to use the default columns From 740b50db078331d1c3b91a98b9aca8b311f9f330 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 8 Nov 2024 05:13:14 -0800 Subject: [PATCH 227/373] finish jump_cursor logic --- quadratic-core/src/grid/sheet.rs | 2 +- quadratic-core/src/grid/sheet/code.rs | 1 - quadratic-core/src/grid/sheet/jump_cursor.rs | 490 +++++++++++++++++++ quadratic-core/src/grid/sheet/offsets.rs | 189 ------- 4 files changed, 491 insertions(+), 191 deletions(-) create mode 100644 quadratic-core/src/grid/sheet/jump_cursor.rs delete mode 100644 quadratic-core/src/grid/sheet/offsets.rs diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index fe2a3ca191..0d4fb40db1 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -31,7 +31,7 @@ pub mod col_row; pub mod data_table; pub mod formats; pub mod formatting; -pub mod offsets; +pub mod jump_cursor; pub mod rendering; pub mod rendering_date_time; pub mod row_resize; diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 1029d455c5..cceecf04f1 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -86,7 +86,6 @@ impl Sheet { (pos.x - code_cell_pos.x) as u32, (pos.y - code_cell_pos.y) as u32, ); - dbgjs!(format!("get_code_cell_value: {:?}", &val)); val } else { None diff --git a/quadratic-core/src/grid/sheet/jump_cursor.rs b/quadratic-core/src/grid/sheet/jump_cursor.rs new file mode 100644 index 0000000000..875e09f234 --- /dev/null +++ b/quadratic-core/src/grid/sheet/jump_cursor.rs @@ -0,0 +1,490 @@ +//! Handles the logic for jumping between cells (ctrl/cmd + arrow key). +//! +//! Algorithm: +//! - if on an empty cell then select to the first cell with a value +//! - if on a filled cell then select to the cell before the next empty cell +//! - if on a filled cell but the next cell is empty then select to the first +//! cell with a value +//! - if there are no more cells then select the next cell over (excel selects +//! to the end of the sheet; we don’t have an end (yet) so right now I select +//! one cell over) +//! +//! The above checks are always made relative to the original cursor position +//! (the highlighted cell) + +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::Pos; + +use super::Sheet; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, TS)] +pub enum JumpDirection { + Up, + Down, + Left, + Right, +} + +impl Sheet { + fn jump_up(&self, current: Pos) -> Pos { + let mut y = current.y; + let x = current.x; + + // if we're close to the edge, return the edge + if y <= 2 { + return Pos { x, y: 1 }; + } + + // adjust the jump position if it is inside a chart to the top-most + // edge of the chart + if let Some((chart_pos, _)) = self.chart_at(Pos { x, y }) { + y = chart_pos.y; + } + + let prev_y: Option; + + // handle case of cell with content + if self.has_content(Pos { x, y }) { + // if previous cell is empty, find the next cell with content + if y - 1 == 0 { + return Pos { x, y: 1 }; + } + + // if prev cell is empty, find the next cell with content + if !self.has_content(Pos { x, y: y - 1 }) { + if let Some(prev) = self.find_next_row(y - 2, x, true, true) { + prev_y = Some(prev); + } else { + prev_y = Some(1); + } + } + // if prev cell is not empty, find the next empty cell + else { + if let Some(prev) = self.find_next_row(y - 2, x, true, false) { + prev_y = Some(prev + 1); + } else { + prev_y = Some(y - 1); + } + } + } + // otherwise find the next cell with content + else { + if let Some(prev) = self.find_next_row(y - 1, x, true, true) { + prev_y = Some(prev); + } else { + prev_y = Some(1); + } + } + y = if let Some(prev_y) = prev_y { + prev_y + } else { + (y - 1).max(1) + }; + Pos { x, y } + } + + fn jump_down(&self, current: Pos) -> Pos { + let mut y = current.y; + let x = current.x; + + // adjust the jump position if it is inside a chart to the bottom-most + // edge of the chart + if let Some((chart_pos, dt)) = self.chart_at(Pos { x, y }) { + if let Some((_, h)) = dt.chart_output { + y = chart_pos.y + (h as i64) - 1; + } + } + + let mut next_y: Option = None; + + // handle case of cell with content + if self.has_content(Pos { x, y }) { + // if next cell is empty, find the next cell with content + if !self.has_content(Pos { x, y: y + 1 }) { + if let Some(next) = self.find_next_row(y + 2, x, false, true) { + next_y = Some(next); + } + } + // if next cell is not empty, find the next empty cell + else { + if let Some(next) = self.find_next_row(y + 2, x, false, false) { + next_y = Some(next - 1); + } else { + next_y = Some(y + 1); + } + } + } + // otherwise find the next cell with content + else { + if let Some(next) = self.find_next_row(y + 1, x, false, true) { + next_y = Some(next); + } + } + + y = if let Some(next_y) = next_y { + next_y + } else { + y + 1 + }; + + Pos { x, y } + } + + fn jump_left(&self, current: Pos) -> Pos { + let mut x = current.x; + let y = current.y; + + // if we're close to the edge, return the edge + if x <= 2 { + return Pos { x: 1, y }; + } + + // adjust the jump position if it is inside a chart to the left-most + // edge of the chart + if let Some((chart_pos, _)) = self.chart_at(Pos { x, y }) { + x = chart_pos.x; + } + + let mut prev_x: Option = None; + + // handle case of cell with content + if self.has_content(Pos { x, y }) { + // if previous cell is empty, find the next cell with content + if x - 1 == 0 { + return Pos { x: 1, y }; + } + if !self.has_content(Pos { x: x - 1, y }) { + if x - 2 == 0 { + return Pos { x: 1, y }; + } + if let Some(prev) = self.find_next_column(x - 2, y, true, true) { + prev_x = Some(prev); + } + } + // if next cell is not empty, find the next empty cell + else { + if let Some(prev) = self.find_next_column(x - 1, y, true, false) { + prev_x = Some(prev + 1); + } else { + prev_x = Some(x - 1); + } + } + } + // otherwise find the previous cell with content + else { + if let Some(prev) = self.find_next_column(x - 1, y, true, true) { + prev_x = Some(prev); + } else { + prev_x = Some(1); + } + } + x = if let Some(prev_x) = prev_x { + prev_x + } else { + (x - 1).max(1) + }; + + Pos { x, y } + } + + fn jump_right(&self, current: Pos) -> Pos { + let mut x = current.x; + let y = current.y; + + // adjust the jump position if it is inside a chart to the right-most + // edge of the chart + if let Some((chart_pos, dt)) = self.chart_at(Pos { x, y }) { + if let Some((w, _)) = dt.chart_output { + x = chart_pos.x + (w as i64) - 1; + } + } + + // handle case of cell with content + let mut next_x: Option = None; + if self.has_content(Pos { x, y }) { + // if next cell is empty, find the next cell with content + if !self.has_content(Pos { x: x + 1, y }) { + if let Some(next) = self.find_next_column(x + 2, y, false, true) { + next_x = Some(next); + } + } + // if next cell is not empty, find the next empty cell + else { + if let Some(next) = self.find_next_column(x + 2, y, false, false) { + next_x = Some(next - 1); + } else { + next_x = Some(x + 1); + } + } + } + // otherwise find the next cell with content + else { + if let Some(next) = self.find_next_column(x + 1, y, false, true) { + next_x = Some(next); + } + } + + x = if let Some(next_x) = next_x { + next_x + } else { + x + 1 + }; + + Pos { x, y } + } + + /// Returns the Pos after a jump (ctrl/cmd + arrow key) + pub fn jump_cursor(&self, current: Pos, direction: JumpDirection) -> Pos { + match direction { + JumpDirection::Up => self.jump_up(current), + JumpDirection::Down => self.jump_down(current), + JumpDirection::Left => self.jump_left(current), + JumpDirection::Right => self.jump_right(current), + } + } +} + +#[cfg(test)] +mod tests { + use serial_test::parallel; + + use crate::{ + grid::{CodeCellLanguage, CodeRun, DataTable, DataTableKind}, + CellValue, CodeCellValue, + }; + + use super::*; + + // creates a 3x3 chart at the given position + fn create_3x3_chart(sheet: &mut Sheet, pos: Pos) { + let mut dt = DataTable::new( + DataTableKind::CodeRun(CodeRun::default()), + "Table 1", + CellValue::Html("".to_string()).into(), + false, + false, + false, + None, + ); + dt.chart_output = Some((3, 3)); + + sheet.set_cell_value( + pos, + CellValue::Code(CodeCellValue { + code: "".to_string(), + language: CodeCellLanguage::Javascript, + }), + ); + sheet.set_data_table(pos, Some(dt)); + } + + #[test] + #[parallel] + fn jump_right_empty() { + let sheet = Sheet::test(); + + let pos = Pos { x: 1, y: 1 }; + let new_pos = sheet.jump_right(pos); + assert_eq!(new_pos, Pos { x: 2, y: 1 }); + let new_pos = sheet.jump_right(Pos { x: 2, y: 2 }); + assert_eq!(new_pos, Pos { x: 3, y: 2 }); + } + + #[test] + #[parallel] + fn jump_right_filled() { + let mut sheet = Sheet::test(); + + sheet.set_cell_value(Pos { x: 1, y: 1 }, CellValue::Number(1.into())); + sheet.set_cell_value(Pos { x: 5, y: 1 }, CellValue::Number(1.into())); + sheet.set_cell_value(Pos { x: 10, y: 1 }, CellValue::Number(1.into())); + + create_3x3_chart(&mut sheet, Pos { x: 20, y: 1 }); + create_3x3_chart(&mut sheet, Pos { x: 25, y: 1 }); + + assert_eq!(sheet.jump_right(Pos { x: 0, y: 1 }), Pos { x: 1, y: 1 }); + assert_eq!(sheet.jump_right(Pos { x: 1, y: 1 }), Pos { x: 5, y: 1 }); + assert_eq!(sheet.jump_right(Pos { x: 5, y: 1 }), Pos { x: 10, y: 1 }); + + // jump to the first cell in the first chart + assert_eq!(sheet.jump_right(Pos { x: 10, y: 1 }), Pos { x: 20, y: 1 }); + + // jump to the first cell in the second chart + assert_eq!(sheet.jump_right(Pos { x: 20, y: 1 }), Pos { x: 25, y: 1 }); + assert_eq!(sheet.jump_right(Pos { x: 21, y: 1 }), Pos { x: 25, y: 1 }); + } + + #[test] + #[parallel] + fn jump_left_empty() { + let sheet = Sheet::test(); + + let pos = Pos { x: 2, y: 1 }; + let new_pos = sheet.jump_left(pos); + assert_eq!(new_pos, Pos { x: 1, y: 1 }); + let new_pos = sheet.jump_left(Pos { x: 5, y: 3 }); + assert_eq!(new_pos, Pos { x: 1, y: 3 }); + } + + #[test] + #[parallel] + fn jump_left_filled() { + let mut sheet = Sheet::test(); + + sheet.set_cell_value(Pos { x: 2, y: 1 }, CellValue::Number(1.into())); + sheet.set_cell_value(Pos { x: 5, y: 1 }, CellValue::Number(1.into())); + sheet.set_cell_value(Pos { x: 10, y: 1 }, CellValue::Number(1.into())); + + create_3x3_chart(&mut sheet, Pos { x: 20, y: 1 }); + create_3x3_chart(&mut sheet, Pos { x: 25, y: 1 }); + + assert_eq!(sheet.jump_left(Pos { x: 11, y: 1 }), Pos { x: 10, y: 1 }); + assert_eq!(sheet.jump_left(Pos { x: 10, y: 1 }), Pos { x: 5, y: 1 }); + assert_eq!(sheet.jump_left(Pos { x: 5, y: 1 }), Pos { x: 2, y: 1 }); + assert_eq!(sheet.jump_left(Pos { x: 2, y: 1 }), Pos { x: 1, y: 1 }); + assert_eq!(sheet.jump_left(Pos { x: 1, y: 1 }), Pos { x: 1, y: 1 }); + + // jump to the last cell in the second chart + assert_eq!(sheet.jump_left(Pos { x: 28, y: 1 }), Pos { x: 25, y: 1 }); + + // jump to the first cell in the second chart + assert_eq!(sheet.jump_left(Pos { x: 25, y: 1 }), Pos { x: 22, y: 1 }); + + // Add more edge case tests + assert_eq!(sheet.jump_left(Pos { x: 2, y: 1 }), Pos { x: 1, y: 1 }); + assert_eq!(sheet.jump_left(Pos { x: 1, y: 1 }), Pos { x: 1, y: 1 }); + } + + #[test] + #[parallel] + fn jump_up_empty() { + let sheet = Sheet::test(); + + let pos = Pos { x: 1, y: 2 }; + let new_pos = sheet.jump_up(pos); + assert_eq!(new_pos, Pos { x: 1, y: 1 }); + let new_pos = sheet.jump_up(Pos { x: 1, y: 5 }); + assert_eq!(new_pos, Pos { x: 1, y: 1 }); + } + + #[test] + #[parallel] + fn jump_up_filled() { + let mut sheet = Sheet::test(); + + sheet.set_cell_value(Pos { x: 3, y: 5 }, CellValue::Number(1.into())); + sheet.set_cell_value(Pos { x: 3, y: 10 }, CellValue::Number(1.into())); + + create_3x3_chart(&mut sheet, Pos { x: 3, y: 20 }); + create_3x3_chart(&mut sheet, Pos { x: 3, y: 25 }); + + // jump to the last cell in the second chart + assert_eq!(sheet.jump_up(Pos { x: 3, y: 30 }), Pos { x: 3, y: 27 }); + + // jump to the last cell in the first chart + assert_eq!(sheet.jump_up(Pos { x: 3, y: 27 }), Pos { x: 3, y: 22 }); + assert_eq!(sheet.jump_up(Pos { x: 3, y: 23 }), Pos { x: 3, y: 20 }); + + assert_eq!(sheet.jump_up(Pos { x: 3, y: 22 }), Pos { x: 3, y: 10 }); + assert_eq!(sheet.jump_up(Pos { x: 3, y: 12 }), Pos { x: 3, y: 10 }); + assert_eq!(sheet.jump_up(Pos { x: 3, y: 10 }), Pos { x: 3, y: 5 }); + assert_eq!(sheet.jump_up(Pos { x: 3, y: 9 }), Pos { x: 3, y: 5 }); + assert_eq!(sheet.jump_up(Pos { x: 3, y: 5 }), Pos { x: 3, y: 1 }); + assert_eq!(sheet.jump_up(Pos { x: 3, y: 3 }), Pos { x: 3, y: 1 }); + + // Add edge case tests + assert_eq!(sheet.jump_up(Pos { x: 3, y: 2 }), Pos { x: 3, y: 1 }); + assert_eq!(sheet.jump_up(Pos { x: 3, y: 1 }), Pos { x: 3, y: 1 }); + } + + #[test] + #[parallel] + fn jump_down_empty() { + let sheet = Sheet::test(); + + let pos = Pos { x: 1, y: 1 }; + let new_pos = sheet.jump_down(pos); + assert_eq!(new_pos, Pos { x: 1, y: 2 }); + let new_pos = sheet.jump_down(Pos { x: 1, y: 5 }); + assert_eq!(new_pos, Pos { x: 1, y: 6 }); + } + + #[test] + #[parallel] + fn jump_down_filled() { + let mut sheet = Sheet::test(); + + sheet.set_cell_value(Pos { x: 3, y: 5 }, CellValue::Number(1.into())); + sheet.set_cell_value(Pos { x: 3, y: 10 }, CellValue::Number(1.into())); + + create_3x3_chart(&mut sheet, Pos { x: 3, y: 20 }); + create_3x3_chart(&mut sheet, Pos { x: 3, y: 25 }); + + assert_eq!(sheet.jump_down(Pos { x: 3, y: 1 }), Pos { x: 3, y: 5 }); + assert_eq!(sheet.jump_down(Pos { x: 3, y: 5 }), Pos { x: 3, y: 10 }); + assert_eq!(sheet.jump_down(Pos { x: 3, y: 6 }), Pos { x: 3, y: 10 }); + + // jump to the first cell in the first chart + assert_eq!(sheet.jump_down(Pos { x: 3, y: 10 }), Pos { x: 3, y: 20 }); + assert_eq!(sheet.jump_down(Pos { x: 3, y: 13 }), Pos { x: 3, y: 20 }); + + // jump to the first cell in the second chart + assert_eq!(sheet.jump_down(Pos { x: 3, y: 20 }), Pos { x: 3, y: 25 }); + assert_eq!(sheet.jump_down(Pos { x: 3, y: 23 }), Pos { x: 3, y: 25 }); + + assert_eq!(sheet.jump_down(Pos { x: 3, y: 26 }), Pos { x: 3, y: 28 }); + } + + #[test] + #[parallel] + fn jump_with_consecutive_filled_cells() { + let mut sheet = Sheet::test(); + + // Create vertical sequence of filled cells + sheet.set_cell_value(Pos { x: 1, y: 1 }, CellValue::Number(1.into())); + sheet.set_cell_value(Pos { x: 1, y: 2 }, CellValue::Number(2.into())); + sheet.set_cell_value(Pos { x: 1, y: 3 }, CellValue::Number(3.into())); + + // Test jumping down through consecutive filled cells + assert_eq!(sheet.jump_down(Pos { x: 1, y: 1 }), Pos { x: 1, y: 3 }); + + // Test jumping up through consecutive filled cells + assert_eq!(sheet.jump_up(Pos { x: 1, y: 3 }), Pos { x: 1, y: 1 }); + + // Create horizontal sequence of filled cells + sheet.set_cell_value(Pos { x: 5, y: 5 }, CellValue::Number(1.into())); + sheet.set_cell_value(Pos { x: 6, y: 5 }, CellValue::Number(2.into())); + sheet.set_cell_value(Pos { x: 7, y: 5 }, CellValue::Number(3.into())); + + // Test jumping right through consecutive filled cells + assert_eq!(sheet.jump_right(Pos { x: 5, y: 5 }), Pos { x: 7, y: 5 }); + + // Test jumping left through consecutive filled cells + assert_eq!(sheet.jump_left(Pos { x: 7, y: 5 }), Pos { x: 5, y: 5 }); + } + + #[test] + #[parallel] + fn test_jump_chart_on_edge() { + let mut sheet = Sheet::test(); + + create_3x3_chart(&mut sheet, Pos { x: 1, y: 1 }); + + assert_eq!(sheet.jump_left(Pos { x: 5, y: 1 }), Pos { x: 3, y: 1 }); + assert_eq!(sheet.jump_left(Pos { x: 2, y: 1 }), Pos { x: 1, y: 1 }); + + assert_eq!(sheet.jump_right(Pos { x: 1, y: 1 }), Pos { x: 4, y: 1 }); + assert_eq!(sheet.jump_right(Pos { x: 2, y: 1 }), Pos { x: 4, y: 1 }); + assert_eq!(sheet.jump_right(Pos { x: 3, y: 1 }), Pos { x: 4, y: 1 }); + + assert_eq!(sheet.jump_up(Pos { x: 1, y: 5 }), Pos { x: 1, y: 3 }); + assert_eq!(sheet.jump_up(Pos { x: 2, y: 4 }), Pos { x: 2, y: 1 }); + assert_eq!(sheet.jump_up(Pos { x: 3, y: 3 }), Pos { x: 3, y: 1 }); + + assert_eq!(sheet.jump_down(Pos { x: 1, y: 1 }), Pos { x: 1, y: 4 }); + assert_eq!(sheet.jump_down(Pos { x: 2, y: 2 }), Pos { x: 2, y: 4 }); + assert_eq!(sheet.jump_down(Pos { x: 3, y: 4 }), Pos { x: 3, y: 5 }); + } +} diff --git a/quadratic-core/src/grid/sheet/offsets.rs b/quadratic-core/src/grid/sheet/offsets.rs deleted file mode 100644 index 3d5e185a06..0000000000 --- a/quadratic-core/src/grid/sheet/offsets.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! Handles the logic for jumping between cells (ctrl/cmd + arrow key). -//! -//! Algorithm: -//! - if on an empty cell then select to the first cell with a value -//! - if on a filled cell then select to the cell before the next empty cell -//! - if on a filled cell but the next cell is empty then select to the first -//! cell with a value -//! - if there are no more cells then select the next cell over (excel selects -//! to the end of the sheet; we don’t have an end (yet) so right now I select -//! one cell over) -//! -//! The above checks are always made relative to the original cursor position -//! (the highlighted cell) - -use serde::{Deserialize, Serialize}; -use ts_rs::TS; - -use crate::Pos; - -use super::Sheet; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, TS)] -pub enum JumpDirection { - Up, - Down, - Left, - Right, -} - -impl Sheet { - fn jump_up(&self, current: Pos) -> Pos { - current - } - - fn jump_down(&self, current: Pos) -> Pos { - current - } - - fn jump_left(&self, current: Pos) -> Pos { - let mut x = current.x; - let y = current.y; - - // adjust the jump position if it is inside a chart to the left-most - // edge of the chart - if let Some((chart_pos, dt)) = self.chart_at(Pos { x, y }) { - if let Some((_, h)) = dt.chart_output { - x = chart_pos.x; - } - } - - // handle case of cell with content - let mut prev_x: Option = None; - if self.has_content(Pos { x, y }) { - // if previous cell is empty, find the next cell with content - if x - 1 == 0 { - return Pos { x: 1, y }; - } - if !self.has_content(Pos { x: x - 1, y }) { - prev_x = self.find_prev_column(x - 2, y, true, true); - } - } - } - - fn jump_right(&self, current: Pos) -> Pos { - let mut x = current.x; - let y = current.y; - - // adjust the jump position if it is inside a chart to the right-most - // edge of the chart - if let Some((chart_pos, dt)) = self.chart_at(Pos { x, y }) { - if let Some((w, _)) = dt.chart_output { - x = chart_pos.x + (w as i64) - 1; - } - } - - // handle case of cell with content - let mut next_x: Option = None; - if self.has_content(Pos { x, y }) { - // if next cell is empty, find the next cell with content - if !self.has_content(Pos { x: x + 1, y }) { - if let Some(next) = self.find_next_column(x + 2, y, false, true) { - next_x = Some(next); - } - } - // if next cell is not empty, find the next empty cell - else { - if let Some(next) = self.find_next_column(x + 2, y, false, false) { - next_x = Some(next - 1); - } else { - next_x = Some(x + 1); - } - } - } - // otherwise find the next cell with content - else { - if let Some(next) = self.find_next_column(x + 1, y, false, true) { - next_x = Some(next); - } - } - - x = if let Some(next_x) = next_x { - next_x - } else { - x + 1 - }; - - Pos { x, y } - } - - /// Returns the Pos after a jump (ctrl/cmd + arrow key) - pub fn jump_cursor(&self, current: Pos, direction: JumpDirection) -> Pos { - match direction { - JumpDirection::Up => self.jump_up(current), - JumpDirection::Down => self.jump_down(current), - JumpDirection::Left => self.jump_left(current), - JumpDirection::Right => self.jump_right(current), - } - } -} - -#[cfg(test)] -mod tests { - use serial_test::parallel; - - use crate::{ - grid::{CodeCellLanguage, CodeRun, DataTable, DataTableKind}, - CellValue, CodeCellValue, - }; - - use super::*; - - // creates a 2x2 chart at the given position - fn create_2x2_chart(sheet: &mut Sheet, pos: Pos) { - let mut dt = DataTable::new( - DataTableKind::CodeRun(CodeRun::default()), - "Table 1", - CellValue::Html("".to_string()).into(), - false, - false, - false, - None, - ); - dt.chart_output = Some((3, 3)); - - sheet.set_cell_value( - pos, - CellValue::Code(CodeCellValue { - code: "".to_string(), - language: CodeCellLanguage::Javascript, - }), - ); - sheet.set_data_table(pos, Some(dt)); - } - - #[test] - #[parallel] - fn jump_right_empty() { - let sheet = Sheet::test(); - - let pos = Pos { x: 1, y: 1 }; - let new_pos = sheet.jump_right(pos); - assert_eq!(new_pos, Pos { x: 2, y: 1 }); - let new_pos = sheet.jump_right(Pos { x: 2, y: 2 }); - assert_eq!(new_pos, Pos { x: 3, y: 2 }); - } - - #[test] - #[parallel] - fn jump_right_filled() { - let mut sheet = Sheet::test(); - - sheet.set_cell_value(Pos { x: 1, y: 1 }, CellValue::Number(1.into())); - sheet.set_cell_value(Pos { x: 5, y: 1 }, CellValue::Number(1.into())); - sheet.set_cell_value(Pos { x: 10, y: 1 }, CellValue::Number(1.into())); - - create_2x2_chart(&mut sheet, Pos { x: 20, y: 1 }); - create_2x2_chart(&mut sheet, Pos { x: 25, y: 1 }); - - assert_eq!(sheet.jump_right(Pos { x: 0, y: 1 }), Pos { x: 1, y: 1 }); - assert_eq!(sheet.jump_right(Pos { x: 1, y: 1 }), Pos { x: 5, y: 1 }); - assert_eq!(sheet.jump_right(Pos { x: 5, y: 1 }), Pos { x: 10, y: 1 }); - - // jump to the first cell in the first chart - assert_eq!(sheet.jump_right(Pos { x: 10, y: 1 }), Pos { x: 20, y: 1 }); - - // jump to the first cell in the second chart - assert_eq!(sheet.jump_right(Pos { x: 20, y: 1 }), Pos { x: 25, y: 1 }); - } -} From cd0d0a72ea2068f71820e430b24db6bc3e7edcdd Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 8 Nov 2024 05:46:51 -0800 Subject: [PATCH 228/373] jump_cursor is working --- .../interaction/keyboard/keyboardPosition.ts | 111 ++++++------------ .../src/app/quadratic-core-types/index.d.ts | 1 + .../quadraticCore/coreClientMessages.ts | 23 ++-- .../quadraticCore/quadraticCore.ts | 49 ++------ .../web-workers/quadraticCore/worker/core.ts | 28 ++--- .../quadraticCore/worker/coreClient.ts | 14 +-- quadratic-core/src/bin/export_types.rs | 2 + quadratic-core/src/grid/sheet/bounds.rs | 22 +++- quadratic-core/src/grid/sheet/jump_cursor.rs | 21 ++++ .../src/wasm_bindings/controller/bounds.rs | 47 +++----- 10 files changed, 131 insertions(+), 187 deletions(-) diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts index 762883df41..7f0d19a7f2 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts @@ -9,6 +9,7 @@ import { moveViewport } from '@/app/gridGL/interaction/viewportHelper'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { matchShortcut } from '@/app/helpers/keyboardShortcuts.js'; +import { JumpDirection } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Rectangle } from 'pixi.js'; @@ -33,7 +34,7 @@ function setCursorPosition(x: number, y: number) { // - if on a filled cell but the next cell is empty then select to the first cell with a value // - if there are no more cells then select the next cell over (excel selects to the end of the sheet; we don’t have an end (yet) so right now I select one cell over) // the above checks are always made relative to the original cursor position (the highlighted cell) -async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { +async function jumpCursor(direction: JumpDirection, select: boolean) { const cursor = sheets.sheet.cursor; const sheetId = sheets.sheet.id; @@ -45,6 +46,26 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { const keyboardX = cursor.keyboardMovePosition.x; const keyboardY = cursor.keyboardMovePosition.y; + const position = await quadraticCore.jumpCursor(sheetId, { x: keyboardX, y: keyboardY }, direction); + + // something went wrong + if (!position) return; + + if (select) { + lastMultiCursor.x = Math.min(cursor.cursorPosition.x, position.x); + lastMultiCursor.y = Math.min(cursor.cursorPosition.y, position.y); + lastMultiCursor.height = Math.abs(cursor.cursorPosition.y - position.y) + 1; + cursor.changePosition({ + multiCursor, + keyboardMovePosition: { x: position.x, y: position.y }, + ensureVisible: { x: lastMultiCursor.x, y: lastMultiCursor.y }, + }); + } else { + setCursorPosition(position.x, position.y); + } + + /* + if (deltaX === 1) { let x = keyboardX; const y = cursor.keyboardMovePosition.y; @@ -295,18 +316,8 @@ async function jumpCursor(deltaX: number, deltaY: number, select: boolean) { y = Math.max(y, 0); } if (y === keyboardY) y--; - if (select) { - lastMultiCursor.y = Math.min(cursor.cursorPosition.y, y); - lastMultiCursor.height = Math.abs(cursor.cursorPosition.y - y) + 1; - cursor.changePosition({ - multiCursor, - keyboardMovePosition: { x, y }, - ensureVisible: { x, y: lastMultiCursor.y }, - }); - } else { - setCursorPosition(x, y); - } - } + +*/ } // use arrow to select when shift key is pressed @@ -404,7 +415,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Move cursor to the top of the content block of cursor cell if (matchShortcut(Action.JumpCursorContentTop, event)) { - jumpCursor(0, -1, false); + jumpCursor('Up', false); return true; } @@ -416,7 +427,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Expand selection to the top of the content block of cursor cell if (matchShortcut(Action.ExpandSelectionContentTop, event)) { - jumpCursor(0, -1, true); + jumpCursor('Down', true); return true; } @@ -428,7 +439,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Move cursor to the bottom of the content block of cursor cell if (matchShortcut(Action.JumpCursorContentBottom, event)) { - jumpCursor(0, 1, false); + jumpCursor('Down', false); return true; } @@ -440,7 +451,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Expand selection to the bottom of the content block of cursor cell if (matchShortcut(Action.ExpandSelectionContentBottom, event)) { - jumpCursor(0, 1, true); + jumpCursor('Down', true); return true; } @@ -452,7 +463,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Move cursor to the left of the content block of cursor cell if (matchShortcut(Action.JumpCursorContentLeft, event)) { - jumpCursor(-1, 0, false); + jumpCursor('Left', false); return true; } @@ -464,7 +475,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Expand selection to the left of the content block of cursor cell if (matchShortcut(Action.ExpandSelectionContentLeft, event)) { - jumpCursor(-1, 0, true); + jumpCursor('Left', true); return true; } @@ -476,7 +487,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Move cursor to the right of the content block of cursor cell if (matchShortcut(Action.JumpCursorContentRight, event)) { - jumpCursor(1, 0, false); + jumpCursor('Right', false); return true; } @@ -488,7 +499,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Expand selection to the right of the content block of cursor cell if (matchShortcut(Action.ExpandSelectionContentRight, event)) { - jumpCursor(1, 0, true); + jumpCursor('Right', true); return true; } @@ -504,46 +515,16 @@ export function keyboardPosition(event: KeyboardEvent): boolean { const sheet = sheets.sheet; const bounds = sheet.getBounds(true); if (bounds) { - const y = bounds.bottom; - quadraticCore - .findNextColumn({ - sheetId: sheet.id, - columnStart: bounds.right, - row: y, - reverse: true, - withContent: true, - }) - .then((x) => { - x = x ?? bounds.right; - setCursorPosition(x, y); - }); + setCursorPosition(bounds.right, bounds.bottom); + } else { + setCursorPosition(1, 1); } return true; } // Move cursor to the start of the row content if (matchShortcut(Action.GotoRowStart, event)) { - const sheet = sheets.sheet; - const bounds = sheet.getBounds(true); - if (bounds) { - const y = sheet.cursor.cursorPosition.y; - quadraticCore - .findNextColumn({ - sheetId: sheet.id, - columnStart: bounds.left, - row: y, - reverse: false, - withContent: true, - }) - .then((x) => { - x = x ?? bounds.left; - quadraticCore.cellHasContent(sheet.id, x, y).then((hasContent) => { - if (hasContent) { - setCursorPosition(x, y); - } - }); - }); - } + setCursorPosition(1, sheets.sheet.cursor.cursorPosition.y); return true; } @@ -551,25 +532,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { if (matchShortcut(Action.GotoRowEnd, event)) { const sheet = sheets.sheet; const bounds = sheet.getBounds(true); - if (bounds) { - const y = sheet.cursor.cursorPosition.y; - quadraticCore - .findNextColumn({ - sheetId: sheet.id, - columnStart: bounds.right, - row: y, - reverse: true, - withContent: true, - }) - .then((x) => { - x = x ?? bounds.right; - quadraticCore.cellHasContent(sheet.id, x, y).then((hasContent) => { - if (hasContent) { - setCursorPosition(x, y); - } - }); - }); - } + setCursorPosition(bounds?.right ?? 1, sheets.sheet.cursor.cursorPosition.y); return true; } diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 0dc183785e..fc23843467 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -43,6 +43,7 @@ export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, colo export interface JsRowHeight { row: bigint, height: number, } export interface JsSheetFill { columns: Array<[bigint, [string, bigint]]>, rows: Array<[bigint, [string, bigint]]>, all: string | null, } export interface JsValidationWarning { x: bigint, y: bigint, validation: string | null, style: ValidationStyle | null, } +export type JumpDirection = "Up" | "Down" | "Left" | "Right"; export interface MinMax { min: number, max: number, } export type NumberRange = { "Range": [number | null, number | null] } | { "Equal": Array } | { "NotEqual": Array }; export interface NumericFormat { type: NumericFormatKind, symbol: string | null, } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index eebe9a7874..769fe0c220 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -18,6 +18,7 @@ import { JsRenderFill, JsSheetFill, JsValidationWarning, + JumpDirection, MinMax, SearchOptions, Selection, @@ -740,20 +741,18 @@ export interface CoreClientGetRowsBounds { id: number; } -export interface ClientCoreFindNextColumn { - type: 'clientCoreFindNextColumn'; +export interface ClientCoreJumpCursor { + type: 'clientCoreJumpCursor'; id: number; sheetId: string; - columnStart: number; - row: number; - reverse: boolean; - withContent: boolean; + current: Coordinate; + direction: JumpDirection; } -export interface CoreClientFindNextColumn { - type: 'coreClientFindNextColumn'; +export interface CoreClientJumpCursor { + type: 'coreClientJumpCursor'; id: number; - column?: number; + coordinate?: Coordinate; } export interface ClientCoreFindNextRow { @@ -1123,8 +1122,7 @@ export type ClientCoreMessage = | ClientCoreExportCsvSelection | ClientCoreGetColumnsBounds | ClientCoreGetRowsBounds - | ClientCoreFindNextColumn - | ClientCoreFindNextRow + | ClientCoreJumpCursor | ClientCoreCommitTransientResize | ClientCoreCommitSingleResize | ClientCoreInit @@ -1185,8 +1183,7 @@ export type CoreClientMessage = | CoreClientExportCsvSelection | CoreClientGetColumnsBounds | CoreClientGetRowsBounds - | CoreClientFindNextColumn - | CoreClientFindNextRow + | CoreClientJumpCursor | CoreClientGenerateThumbnail | CoreClientLoad | CoreClientSheetRenderCells diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index d89bf742ef..33d14665a4 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -7,6 +7,7 @@ import { debugShowFileIO, debugWebWorkersMessages } from '@/app/debugFlags'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +import { Coordinate } from '@/app/gridGL/types/size'; import { BorderSelection, BorderStyle, @@ -19,6 +20,7 @@ import { JsCellValue, JsCodeCell, JsRenderCell, + JumpDirection, MinMax, PasteSpecial, SearchOptions, @@ -53,6 +55,7 @@ import { CoreClientGetRowsBounds, CoreClientGetValidationList, CoreClientHasRenderCells, + CoreClientJumpCursor, CoreClientLoad, CoreClientMessage, CoreClientNeighborText, @@ -955,52 +958,18 @@ class QuadraticCore { }); } - findNextColumn(options: { - sheetId: string; - columnStart: number; - row: number; - reverse: boolean; - withContent: boolean; - }): Promise { - const { sheetId, columnStart, row, reverse, withContent } = options; + jumpCursor(sheetId: string, current: Coordinate, direction: JumpDirection): Promise { return new Promise((resolve) => { const id = this.id++; - this.waitingForResponse[id] = (message: { column: number | number }) => { - resolve(message.column); + this.waitingForResponse[id] = (message: CoreClientJumpCursor) => { + resolve(message.coordinate); }; this.send({ - type: 'clientCoreFindNextColumn', - id, + type: 'clientCoreJumpCursor', sheetId, - columnStart, - row, - reverse, - withContent, - }); - }); - } - - findNextRow(options: { - sheetId: string; - column: number; - rowStart: number; - reverse: boolean; - withContent: boolean; - }): Promise { - const { sheetId, column, rowStart, reverse, withContent } = options; - return new Promise((resolve) => { - const id = this.id++; - this.waitingForResponse[id] = (message: { row: number | undefined }) => { - resolve(message.row); - }; - this.send({ - type: 'clientCoreFindNextRow', + current, + direction, id, - sheetId, - column, - rowStart, - reverse, - withContent, }); }); } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index c75e9837bb..1050613452 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -6,6 +6,7 @@ */ import { debugWebWorkers } from '@/app/debugFlags'; +import { Coordinate } from '@/app/gridGL/types/size'; import { BorderSelection, BorderStyle, @@ -19,6 +20,7 @@ import { JsCodeCell, JsCodeResult, JsRenderCell, + JumpDirection, MinMax, SearchOptions, Selection, @@ -34,8 +36,6 @@ import { import * as Sentry from '@sentry/react'; import { Buffer } from 'buffer'; import { - ClientCoreFindNextColumn, - ClientCoreFindNextRow, ClientCoreImportFile, ClientCoreLoad, ClientCoreMoveCells, @@ -849,24 +849,18 @@ class Core { }); } - findNextColumn(data: ClientCoreFindNextColumn): Promise { + jumpCursor(sheetId: string, current: Coordinate, direction: JumpDirection): Promise { return new Promise((resolve) => { this.clientQueue.push(() => { if (!this.gridController) throw new Error('Expected gridController to be defined'); - resolve( - this.gridController.findNextColumn(data.sheetId, data.columnStart, data.row, data.reverse, data.withContent) - ); - }); - }); - } - - findNextRow(data: ClientCoreFindNextRow): Promise { - return new Promise((resolve) => { - this.clientQueue.push(() => { - if (!this.gridController) throw new Error('Expected gridController to be defined'); - resolve( - this.gridController.findNextRow(data.sheetId, data.rowStart, data.column, data.reverse, data.withContent) - ); + try { + const result = this.gridController.jumpCursor(sheetId, posToPos(current.x, current.y), JSON.stringify(direction)); + const pos = JSON.parse(result); + resolve({ x: Number(pos.x), y: Number(pos.y) }); + } catch (error: any) { + console.warn(error); + resolve(undefined); + } }); }); } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index 556f3f4497..4c3f11378b 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -424,19 +424,11 @@ class CoreClient { }); return; - case 'clientCoreFindNextColumn': + case 'clientCoreJumpCursor': this.send({ - type: 'coreClientFindNextColumn', + type: 'coreClientJumpCursor', id: e.data.id, - column: await core.findNextColumn(e.data), - }); - return; - - case 'clientCoreFindNextRow': - this.send({ - type: 'coreClientFindNextRow', - id: e.data.id, - row: await core.findNextRow(e.data), + coordinate: await core.jumpCursor(e.data.sheetId, e.data.current, e.data.direction), }); return; diff --git a/quadratic-core/src/bin/export_types.rs b/quadratic-core/src/bin/export_types.rs index 67959ad91b..e0a899d689 100644 --- a/quadratic-core/src/bin/export_types.rs +++ b/quadratic-core/src/bin/export_types.rs @@ -10,6 +10,7 @@ use grid::js_types::{ JsRowHeight, JsSheetFill, JsValidationWarning, }; use grid::sheet::borders::{BorderStyleCell, BorderStyleTimestamp}; +use grid::sheet::jump_cursor::JumpDirection; use grid::sheet::validations::validation::{ Validation, ValidationDisplay, ValidationDisplaySheet, ValidationError, ValidationMessage, ValidationStyle, @@ -109,6 +110,7 @@ fn main() { JsRowHeight, JsSheetFill, JsValidationWarning, + JumpDirection, MinMax, NumberRange, NumericFormat, diff --git a/quadratic-core/src/grid/sheet/bounds.rs b/quadratic-core/src/grid/sheet/bounds.rs index bf76bdaeb9..2bede4838f 100644 --- a/quadratic-core/src/grid/sheet/bounds.rs +++ b/quadratic-core/src/grid/sheet/bounds.rs @@ -304,7 +304,16 @@ impl Sheet { .as_ref() .is_some_and(|cell_value| *cell_value == CellValue::Blank) { - if self.table_intersects(x, row, Some(column_start), None) { + if self.table_intersects( + x, + row, + Some(if reverse { + column_start + 1 + } else { + column_start - 1 + }), + None, + ) { // we use a dummy CellValue::Logical to share that there is // content here (so we don't have to check for the actual // Table content--as it's not really needed except for a @@ -359,7 +368,16 @@ impl Sheet { // content here (so we don't have to check for the actual // Table content--as it's not really needed except for a // Blank check) - if self.table_intersects(column, y, None, Some(row_start)) { + if self.table_intersects( + column, + y, + None, + Some(if reverse { + row_start + 1 + } else { + row_start - 1 + }), + ) { has_content = Some(CellValue::Logical(true)); } } diff --git a/quadratic-core/src/grid/sheet/jump_cursor.rs b/quadratic-core/src/grid/sheet/jump_cursor.rs index 875e09f234..7f1099e47d 100644 --- a/quadratic-core/src/grid/sheet/jump_cursor.rs +++ b/quadratic-core/src/grid/sheet/jump_cursor.rs @@ -71,6 +71,7 @@ impl Sheet { } // otherwise find the next cell with content else { + // this is wrong: the table is being excluded where it's starting from (y - 1 instead of y) if let Some(prev) = self.find_next_row(y - 1, x, true, true) { prev_y = Some(prev); } else { @@ -357,6 +358,16 @@ mod tests { assert_eq!(sheet.jump_left(Pos { x: 1, y: 1 }), Pos { x: 1, y: 1 }); } + #[test] + #[parallel] + fn jump_left_chart() { + let mut sheet = Sheet::test(); + + create_3x3_chart(&mut sheet, Pos { x: 5, y: 1 }); + + assert_eq!(sheet.jump_left(Pos { x: 10, y: 2 }), Pos { x: 7, y: 2 }); + } + #[test] #[parallel] fn jump_up_empty() { @@ -399,6 +410,16 @@ mod tests { assert_eq!(sheet.jump_up(Pos { x: 3, y: 1 }), Pos { x: 3, y: 1 }); } + #[test] + #[parallel] + fn jump_up_chart() { + let mut sheet = Sheet::test(); + + create_3x3_chart(&mut sheet, Pos { x: 5, y: 1 }); + + assert_eq!(sheet.jump_up(Pos { x: 6, y: 4 }), Pos { x: 6, y: 3 }); + } + #[test] #[parallel] fn jump_down_empty() { diff --git a/quadratic-core/src/wasm_bindings/controller/bounds.rs b/quadratic-core/src/wasm_bindings/controller/bounds.rs index 172d255b87..3bda9930e4 100644 --- a/quadratic-core/src/wasm_bindings/controller/bounds.rs +++ b/quadratic-core/src/wasm_bindings/controller/bounds.rs @@ -1,3 +1,5 @@ +use sheet::jump_cursor::JumpDirection; + use super::*; #[derive(Serialize, Deserialize, Debug)] @@ -69,37 +71,22 @@ impl GridController { } } - /// finds nearest column with or without content - #[wasm_bindgen(js_name = "findNextColumn")] - pub fn js_find_next_column( + #[wasm_bindgen(js_name = "jumpCursor")] + pub fn js_jump_cursor( &self, sheet_id: String, - column_start: i32, - row: i32, - reverse: bool, - with_content: bool, - ) -> Option { - // todo: this should have Result return type and handle no sheet found (which should not happen) - let sheet = self.try_sheet_from_string_id(sheet_id)?; - sheet - .find_next_column(column_start as i64, row as i64, reverse, with_content) - .map(|x| x as i32) - } - - /// finds nearest row with or without content - #[wasm_bindgen(js_name = "findNextRow")] - pub fn js_find_next_row( - &self, - sheet_id: String, - row_start: i32, - column: i32, - reverse: bool, - with_content: bool, - ) -> Option { - // todo: this should have Result return type and handle no sheet found (which should not happen) - let sheet = self.try_sheet_from_string_id(sheet_id)?; - sheet - .find_next_row(row_start as i64, column as i64, reverse, with_content) - .map(|y| y as i32) + pos: String, + direction: String, + ) -> Result { + let sheet = self + .try_sheet_from_string_id(sheet_id) + .ok_or_else(|| JsValue::from_str("Sheet not found"))?; + let pos: Pos = serde_json::from_str(&pos) + .map_err(|e| JsValue::from_str(&format!("Invalid current position: {}", e)))?; + let direction: JumpDirection = serde_json::from_str(&direction) + .map_err(|e| JsValue::from_str(&format!("Invalid direction: {}", e)))?; + let next = sheet.jump_cursor(pos, direction); + serde_json::to_string(&next) + .map_err(|e| JsValue::from_str(&format!("Failed to serialize next position: {}", e))) } } From ba1a8531bd3010a0e8f4768cbe67b40ba4785ddf Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 8 Nov 2024 06:34:04 -0800 Subject: [PATCH 229/373] add show all columns to table menu --- .../src/app/actions/dataTableSpec.ts | 3 -- .../contextMenus/TableColumnContextMenu.tsx | 29 ++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 7d86fd5246..d5ab6722f4 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -203,7 +203,6 @@ export const dataTableSpec: DataTableSpec = { setTimeout(() => { const contextMenu = { type: ContextMenuType.TableColumn, rename: true, table, selectedColumn }; events.emit('contextMenu', contextMenu); - console.log('emitting...'); }); } } @@ -287,8 +286,6 @@ export const dataTableSpec: DataTableSpec = { column.display = true; }); - console.log('show all columns', columns); - quadraticCore.dataTableMeta( sheets.sheet.id, table.x, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx index ff49c2f3ee..bc604e6989 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -1,10 +1,13 @@ //! This shows the table column's header context menu. import { Action } from '@/app/actions/actions'; -import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; +import { getColumns } from '@/app/actions/dataTableSpec'; +import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { ContextMenuBase } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuBase'; import { ContextMenuItemAction } from '@/app/gridGL/HTMLGrid/contextMenus/ContextMenuItem'; import { TableMenu } from '@/app/gridGL/HTMLGrid/contextMenus/TableMenu'; +import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { TableIcon } from '@/shared/components/Icons'; import { DropdownMenuSeparator, @@ -13,8 +16,29 @@ import { DropdownMenuSubTrigger, } from '@/shared/shadcn/ui/dropdown-menu'; import { DropdownMenuItem } from '@radix-ui/react-dropdown-menu'; +import { useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; export const TableColumnContextMenu = () => { + const { table } = useRecoilValue(contextMenuAtom); + + const hiddenColumns = getColumns()?.filter((c) => !c.display); + + const hasHiddenColumns = useMemo(() => { + if (!hiddenColumns) return false; + + return hiddenColumns?.length > 0; + }, [hiddenColumns]); + + const isImageOrHtmlCell = useMemo(() => { + if (!table) return false; + return ( + htmlCellsHandler.isHtmlCell(table.x, table.y) || pixiApp.cellsSheet().cellsImages.isImageCell(table.x, table.y) + ); + }, [table]); + + const spillError = table?.spill_error; + return ( {({ contextMenu }) => ( @@ -25,6 +49,9 @@ export const TableColumnContextMenu = () => { + {!isImageOrHtmlCell && hasHiddenColumns && !spillError && ( + + )} From 892c444b21f7c6bfdc756faf82464ecf33a788a8 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 8 Nov 2024 07:38:16 -0800 Subject: [PATCH 230/373] fix color stuff --- .../HTMLGrid/contextMenus/PixiRename.tsx | 12 +++++---- .../contextMenus/TableColumnHeaderRename.tsx | 7 ++++-- .../HTMLGrid/contextMenus/TableRename.tsx | 2 +- .../gridGL/cells/tables/TableColumnHeaders.ts | 2 +- .../app/gridGL/interaction/pointer/Pointer.ts | 1 + .../interaction/pointer/PointerTable.ts | 25 +++++++++++++++---- .../src/app/helpers/convertColor.ts | 21 ++++++++++++++++ quadratic-client/src/shared/shadcn/styles.css | 5 +++- 8 files changed, 60 insertions(+), 15 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx index 1bb15a65d7..a189acefdd 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/PixiRename.tsx @@ -19,10 +19,12 @@ interface Props { // if true, the input will be the same scale as the app; otherwise it will // scale with the viewport noScale?: boolean; + + hasBorder?: number; } export const PixiRename = (props: Props) => { - const { position, defaultValue, className, styles, onClose, onSave, noScale } = props; + const { position, defaultValue, className, styles, onClose, onSave, noScale, hasBorder } = props; // ensure we can wait a tick for the rename to close to avoid a conflict // between Escape and Blur @@ -126,10 +128,10 @@ export const PixiRename = (props: Props) => { ref={ref} className={cn('pointer-events-auto absolute rounded-none border-none outline-none', className)} style={{ - left: position.x, - top: position.y, - width: position.width, - height: position.height, + left: position.x + (hasBorder ? hasBorder / 2 : 0), + top: position.y + (hasBorder ? hasBorder / 2 : 0), + width: position.width - (hasBorder ? hasBorder : 0), + height: position.height - (hasBorder ? hasBorder : 0), transform: noScale ? `scale(${1 / pixiApp.viewport.scaled})` : undefined, ...styles, }} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx index 0e27c1b0ec..ef7b84bb4e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -1,8 +1,10 @@ import { getDisplayColumns } from '@/app/actions/dataTableSpec'; import { contextMenuAtom, ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { events } from '@/app/events/events'; +import { COLUMN_HEADER_BACKGROUND_LUMINOSITY } from '@/app/gridGL/cells/tables/TableColumnHeaders'; import { PixiRename } from '@/app/gridGL/HTMLGrid/contextMenus/PixiRename'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { cssVariableWithLuminosity } from '@/app/helpers/convertColor'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { FONT_SIZE } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellLabel'; import { useMemo } from 'react'; @@ -45,13 +47,14 @@ export const TableColumnHeaderRename = () => { return ( { if (contextMenu.table && contextMenu.selectedColumn !== undefined && pixiApp.cellsSheets.current) { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx index daec07ee23..bfb9ee681c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableRename.tsx @@ -30,7 +30,7 @@ export const TableRename = () => { { if (contextMenu.table && pixiApp.cellsSheets.current) { diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 0cb12922a0..2b04fd19df 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -13,7 +13,7 @@ import { sharedEvents } from '@/shared/sharedEvents'; import { Container, Graphics, Point, Rectangle } from 'pixi.js'; // used to make the column header background a bit darker than the primary color -const COLUMN_HEADER_BACKGROUND_LUMINOSITY = 1.75; +export const COLUMN_HEADER_BACKGROUND_LUMINOSITY = 1.75; export class TableColumnHeaders extends Container { private table: Table; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts index c878e9f3e9..d2e9127264 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/Pointer.ts @@ -156,6 +156,7 @@ export class Pointer { this.pointerHtmlCells.pointerUp(e) || this.pointerImages.pointerUp() || this.pointerCellMoving.pointerUp() || + this.pointerTable.pointerUp() || this.pointerHeading.pointerUp() || this.pointerAutoComplete.pointerUp() || this.pointerDown.pointerUp(event); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts index cc4033d8b9..60ab05ace0 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerTable.ts @@ -29,6 +29,8 @@ export class PointerTable { table: tableDown.table, rename: true, }); + window.clearTimeout(this.doubleClickTimeout); + this.doubleClickTimeout = undefined; } else { this.doubleClickTimeout = window.setTimeout(() => { this.doubleClickTimeout = undefined; @@ -125,11 +127,16 @@ export class PointerTable { this.doubleClickTimeout = undefined; } if (this.tableNameDown) { - pixiApp.pointer.pointerCellMoving.tableMove( - this.tableNameDown.column, - this.tableNameDown.row, - this.tableNameDown.point - ); + if ( + this.tableNameDown.column !== this.tableNameDown.point.x || + this.tableNameDown.row !== this.tableNameDown.point.y + ) { + pixiApp.pointer.pointerCellMoving.tableMove( + this.tableNameDown.column, + this.tableNameDown.row, + this.tableNameDown.point + ); + } this.tableNameDown = undefined; return true; } @@ -137,4 +144,12 @@ export class PointerTable { this.cursor = pixiApp.cellsSheet().tables.tableCursor; return result; } + + pointerUp(): boolean { + if (this.tableNameDown) { + this.tableNameDown = undefined; + return true; + } + return false; + } } diff --git a/quadratic-client/src/app/helpers/convertColor.ts b/quadratic-client/src/app/helpers/convertColor.ts index 392f5d8fc0..ff436fe6f5 100644 --- a/quadratic-client/src/app/helpers/convertColor.ts +++ b/quadratic-client/src/app/helpers/convertColor.ts @@ -97,3 +97,24 @@ export function getCSSVariableTint(cssVariableName: string, options?: { luminosi const out = parsed.rgbNumber(); return out; } + +/** + * Given the name of a CSS variable that maps to an HSL string, return the tint + * we can use in pixi. + * @param cssVariableName - CSS var without the `--` prefix + * @param luminosity - If provided, will mulitply the luminosity by this number + */ +export function cssVariableWithLuminosity(cssVariableName: string, luminosity: number): string { + if (cssVariableName.startsWith('--')) { + console.warn( + '`getCSSVariableTint` expects a CSS variable name without the `--` prefix. Are you sure you meant: `%s`', + cssVariableName + ); + } + + const hslColorString = getComputedStyle(document.documentElement).getPropertyValue(`--${cssVariableName}`).trim(); + const numbers = hslColorString.split(' ').map(parseFloat); + numbers[2] *= luminosity; + const parsed = Color.hsl(...numbers); + return parsed.rgb().toString(); +} diff --git a/quadratic-client/src/shared/shadcn/styles.css b/quadratic-client/src/shared/shadcn/styles.css index 0381c019a0..691321cfaa 100644 --- a/quadratic-client/src/shared/shadcn/styles.css +++ b/quadratic-client/src/shared/shadcn/styles.css @@ -43,7 +43,6 @@ /* Sheet Table Colors */ --table-column-header-foreground: 0 0% 0%; - --table-column-header-background: 223 81% 94%; --table-alternating-background: 220 75% 98%; } @@ -88,6 +87,10 @@ background-color: hsl(var(--primary) / 0.1); } +.reverse-selection::selection { + background-color: hsl(var(--primary-foreground) / 0.4); +} + /** * Themes */ From 33230e3496d40652c362a086b39de8254d628a60 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 8 Nov 2024 07:40:07 -0800 Subject: [PATCH 231/373] minor tweaks --- .../gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx | 2 +- quadratic-client/src/shared/shadcn/styles.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx index ef7b84bb4e..aabf9d2e4c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -50,7 +50,7 @@ export const TableColumnHeaderRename = () => { hasBorder={2} defaultValue={originalHeaderName} position={position} - className="origin-bottom-left border-none p-0 text-sm font-bold text-primary-foreground outline-none" + className="darker-selection origin-bottom-left border-none p-0 text-sm font-bold text-primary-foreground outline-none" styles={{ fontSize: FONT_SIZE, color: 'var(--primary-foreground)', diff --git a/quadratic-client/src/shared/shadcn/styles.css b/quadratic-client/src/shared/shadcn/styles.css index 691321cfaa..306b66afe8 100644 --- a/quadratic-client/src/shared/shadcn/styles.css +++ b/quadratic-client/src/shared/shadcn/styles.css @@ -91,6 +91,10 @@ background-color: hsl(var(--primary-foreground) / 0.4); } +.darker-selection::selection { + background-color: hsl(var(--primary) / 0.4); +} + /** * Themes */ From 3ba71d64bee9a5530cf1d3525ab9bf0d0f69cc72 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 8 Nov 2024 08:55:24 -0800 Subject: [PATCH 232/373] html chart has a default size --- .../app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 1 + .../src/controller/execution/run_code/mod.rs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index b370866a42..12baafb649 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -11,6 +11,7 @@ import { HtmlCellResizing } from './HtmlCellResizing'; // number of screen pixels to trigger the resize cursor const tolerance = 5; +// this should be kept in sync with run_code/mod.rs const DEFAULT_HTML_WIDTH = 600; const DEFAULT_HTML_HEIGHT = 460; diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 9266d27c94..7c4c650c74 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -12,6 +12,10 @@ pub mod run_formula; pub mod run_javascript; pub mod run_python; +// this should be kept in sync with HtmlCell.ts +const DEFAULT_HTML_WIDTH: f32 = 600.0; +const DEFAULT_HTML_HEIGHT: f32 = 460.0; + impl GridController { /// finalize changes to a code_run pub(crate) fn finalize_code_run( @@ -38,13 +42,13 @@ impl GridController { ); if let Some(new_data_table) = new_data_table.as_mut() { - if let Some((pixel_width, pixel_height)) = new_data_table.chart_pixel_output { - let chart_output = - sheet - .offsets - .calculate_grid_size(pos, pixel_width, pixel_height); - new_data_table.chart_output = Some(chart_output); - } + let (pixel_width, pixel_height) = new_data_table + .chart_pixel_output + .unwrap_or((DEFAULT_HTML_WIDTH, DEFAULT_HTML_HEIGHT)); + let chart_output = sheet + .offsets + .calculate_grid_size(pos, pixel_width, pixel_height); + new_data_table.chart_output = Some(chart_output); } let old_data_table = if let Some(new_data_table) = &new_data_table { From 1cec49b325f26d70f15e52905d6052d63df954d1 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 8 Nov 2024 08:59:07 -0800 Subject: [PATCH 233/373] fix bug with gridlines after table deletion --- quadratic-client/src/app/grid/sheet/GridOverflowLines.ts | 1 + quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 2 +- .../src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts b/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts index aa4bdf304f..4f9552ccc3 100644 --- a/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts +++ b/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts @@ -39,6 +39,7 @@ export class GridOverflowLines { updateImageHtml(column: number, row: number, width?: number, height?: number) { if (width === undefined || height === undefined) { this.overflowImageHtml.delete(`${column},${row}`); + pixiApp.gridLines.dirty = true; return; } const start = this.sheet.offsets.getCellOffsets(column, row); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index 12baafb649..7ab8a7c76e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -100,7 +100,7 @@ export class HtmlCell { destroy() { this.div.remove(); - this.sheet.gridOverflowLines.updateImageHtml(this.x, this.y, 0, 0); + this.sheet.gridOverflowLines.updateImageHtml(this.x, this.y); } get x(): number { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts index 0221c201a9..05107ccdca 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler.ts @@ -86,6 +86,7 @@ class HTMLCellsHandler { // remove old cells old.forEach((cell) => { + cell.destroy(); parent.removeChild(cell.div); this.cells.delete(cell); }); From d90d9ec1e5bb1767e7837e4486e0a9b81cd4ced2 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 8 Nov 2024 09:07:08 -0800 Subject: [PATCH 234/373] ensure that chart_output is only set for charts --- .../src/controller/execution/run_code/mod.rs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 7c4c650c74..13b3df64fe 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -41,14 +41,20 @@ impl GridController { .unwrap_or(sheet.data_tables.len()), ); - if let Some(new_data_table) = new_data_table.as_mut() { - let (pixel_width, pixel_height) = new_data_table - .chart_pixel_output - .unwrap_or((DEFAULT_HTML_WIDTH, DEFAULT_HTML_HEIGHT)); - let chart_output = sheet - .offsets - .calculate_grid_size(pos, pixel_width, pixel_height); - new_data_table.chart_output = Some(chart_output); + if new_data_table + .as_ref() + .is_some_and(|dt| dt.is_html_or_image()) + { + if let Some(new_data_table) = new_data_table.as_mut() { + let (pixel_width, pixel_height) = new_data_table + .chart_pixel_output + .unwrap_or((DEFAULT_HTML_WIDTH, DEFAULT_HTML_HEIGHT)); + let chart_output = + sheet + .offsets + .calculate_grid_size(pos, pixel_width, pixel_height); + new_data_table.chart_output = Some(chart_output); + } } let old_data_table = if let Some(new_data_table) = &new_data_table { From cdb2c8e7a7b1876049d3a4880f0513d239faf8ed Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 8 Nov 2024 09:21:50 -0800 Subject: [PATCH 235/373] minor tweaks --- quadratic-client/src/app/actions/dataTableSpec.ts | 6 +++--- .../HTMLGrid/contextMenus/TableColumnHeaderRename.tsx | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index d5ab6722f4..ecd0d3d5ac 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -5,7 +5,7 @@ import { createSelection } from '@/app/grid/sheet/selection'; import { doubleClickCell } from '@/app/gridGL/interaction/pointer/doubleClickCell'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; -import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { JsDataTableColumn, JsRenderCodeCell } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { DeleteIcon, @@ -46,12 +46,12 @@ export const getTable = (): JsRenderCodeCell | undefined => { return pixiAppSettings.contextMenu?.table ?? pixiApp.cellsSheet().cursorOnDataTable(); }; -export const getColumns = (): { name: string; display: boolean; valueIndex: number }[] | undefined => { +export const getColumns = (): JsDataTableColumn[] | undefined => { const table = getTable(); return table?.columns; }; -export const getDisplayColumns = (): { name: string; display: boolean; valueIndex: number }[] | undefined => { +export const getDisplayColumns = (): JsDataTableColumn[] | undefined => { const table = getTable(); return table?.columns.filter((c) => c.display); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx index aabf9d2e4c..35f5ff1c47 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnHeaderRename.tsx @@ -58,11 +58,10 @@ export const TableColumnHeaderRename = () => { }} onSave={(value: string) => { if (contextMenu.table && contextMenu.selectedColumn !== undefined && pixiApp.cellsSheets.current) { - const columns = JSON.parse(JSON.stringify(getDisplayColumns())); + const columns = getDisplayColumns(); if (columns) { columns[contextMenu.selectedColumn].name = value; - console.log('rename columns', columns); quadraticCore.dataTableMeta( pixiApp.cellsSheets.current?.sheetId, From 719880e3f3d5f4088357de198e9a1fc0ed7f0f63 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Fri, 8 Nov 2024 10:10:10 -0800 Subject: [PATCH 236/373] fix sort button --- .../src/app/gridGL/cells/tables/TableColumnHeader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index e2728d96e1..9f34f4f988 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -104,7 +104,7 @@ export class TableColumnHeader extends Container { } private updateSortButton(width: number, height: number, sort?: DataTableSort) { - this.sortButtonStart = this.columnHeaderBounds.right - SORT_BUTTON_RADIUS - SORT_BUTTON_PADDING; + this.sortButtonStart = this.columnHeaderBounds.right - SORT_BUTTON_RADIUS * 2 - SORT_BUTTON_PADDING * 2; if (!this.sortButton) { throw new Error('Expected sortButton to be defined in updateSortButton'); } From d11370540f1284c7b05eb779f512728aae8ea878 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 8 Nov 2024 16:07:15 -0700 Subject: [PATCH 237/373] Make all the name unique + refactors --- .../src/app/actions/dataTableSpec.ts | 5 +- .../execute_operation/execute_data_table.rs | 1 + .../src/controller/execution/run_code/mod.rs | 9 + quadratic-core/src/grid/data_table/column.rs | 167 ++++++++---------- quadratic-core/src/grid/data_table/mod.rs | 11 +- quadratic-core/src/grid/sheet/data_table.rs | 7 +- quadratic-core/src/util.rs | 9 +- quadratic-core/src/values/mod.rs | 12 ++ 8 files changed, 114 insertions(+), 107 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index ecd0d3d5ac..4018fac74f 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -53,7 +53,8 @@ export const getColumns = (): JsDataTableColumn[] | undefined => { export const getDisplayColumns = (): JsDataTableColumn[] | undefined => { const table = getTable(); - return table?.columns.filter((c) => c.display); + + return table?.columns.filter((c) => c.display).map((c) => ({ ...c })); }; const isHeadingShowing = (): boolean => { @@ -251,7 +252,7 @@ export const dataTableSpec: DataTableSpec = { Icon: HideIcon, run: () => { const table = getTable(); - const columns = JSON.parse(JSON.stringify(getDisplayColumns())); + const columns = getDisplayColumns(); const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; if (table && columns && selectedColumn !== undefined && columns[selectedColumn]) { diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index aa112c950b..2ccd965a67 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -368,6 +368,7 @@ impl GridController { let old_columns = columns.as_ref().and_then(|columns| { let old_columns = data_table.columns.to_owned(); data_table.columns = Some(columns.to_owned()); + data_table.normalize_column_names(); old_columns }); diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 13b3df64fe..db5fecbd95 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -26,6 +26,15 @@ impl GridController { index: Option, ) { let sheet_id = sheet_pos.sheet_id; + + // enforce unique data table names + if let Some(new_data_table) = &mut new_data_table { + let unique_name = self + .grid() + .unique_data_table_name(&new_data_table.name, false); + new_data_table.update_table_name(&unique_name); + } + let Some(sheet) = self.try_sheet_mut(sheet_id) else { // sheet may have been deleted return; diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index 9aea715a11..655ff93c06 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -1,11 +1,11 @@ //! DataTable columns -use crate::grid::js_types::JsDataTableColumn; -use crate::{CellValue, Value}; -use anyhow::{anyhow, Ok}; use serde::{Deserialize, Serialize}; use super::DataTable; +use crate::grid::js_types::JsDataTableColumn; +use crate::util::unique_name; +use crate::{CellValue, Value}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct DataTableColumn { @@ -40,6 +40,8 @@ impl DataTable { }), _ => None, }; + + self.normalize_column_names(); } pub fn toggle_first_row_as_header(&mut self, first_row_as_header: bool) { @@ -51,81 +53,23 @@ impl DataTable { } } - /// Apply default column headings to the DataTable. + /// Create default column headings for the DataTable. /// For example, the column headings will be "Column 1", "Column 2", etc. - pub fn apply_default_header(&mut self) { - self.columns = match self.value { - Value::Array(ref mut array) => Some( - (1..=array.width()) - .map(|i| DataTableColumn::new(format!("Column {i}"), true, i - 1)) - .collect::>(), - ), - _ => None, - }; - } - - /// Ensure that the index is within the bounds of the columns. - /// If there are no columns, apply default headers first if `apply_default_header` is true. - fn check_index(&mut self, index: usize, apply_default_header: bool) -> anyhow::Result<()> { - match self.columns { - Some(ref mut columns) => { - let column_len = columns.len(); - - if index >= column_len { - return Err(anyhow!("Column {index} out of bounds: {column_len}")); - } - } - // there are no columns, so we need to apply default headers first - None => { - apply_default_header.then(|| self.apply_default_header()); - } - }; - - Ok(()) - } - - /// Replace a column header at the given index in place. - pub fn set_header_at( - &mut self, - index: usize, - name: String, - display: bool, - ) -> anyhow::Result<()> { - self.check_index(index, true)?; - // let all_names = &self - // .columns - // .as_ref() - // .unwrap() - // .iter() - // .map(|column| column.name.to_string().to_owned().as_str()) - // .collect_vec(); - // let name = unique_name(&name, all_names); - - if let Some(column) = self - .columns - .as_mut() - .and_then(|columns| columns.get_mut(index)) - { - column.name = CellValue::Text(name); - column.display = display; + pub fn default_header(&self, width: Option) -> Vec { + let width = width.unwrap_or(self.value.size().w.get()); + + match self.value { + Value::Array(_) => (1..=width) + .map(|i| DataTableColumn::new(format!("Column {i}"), true, i - 1)) + .collect::>(), + _ => vec![], } - - Ok(()) } - /// Set the display of a column header at the given index. - pub fn set_header_display_at(&mut self, index: usize, display: bool) -> anyhow::Result<()> { - self.check_index(index, true)?; - - if let Some(column) = self - .columns - .as_mut() - .and_then(|columns| columns.get_mut(index)) - { - column.display = display; - } - - Ok(()) + /// Apply default column headings to the DataTable. + /// For example, the column headings will be "Column 1", "Column 2", etc. + pub fn apply_default_header(&mut self) { + self.columns = Some(self.default_header(None)); } pub fn adjust_for_header(&self, index: usize) -> usize { @@ -139,19 +83,33 @@ impl DataTable { /// Prepares the columns to be sent to the client. If no columns are set, it /// will create default columns. pub fn send_columns(&self) -> Vec { - match self.columns.as_ref() { - Some(columns) => columns - .iter() - .map(|column| JsDataTableColumn::from(column.to_owned())) - .collect(), - // TODO(ddimaria): refacor this to use the default columns + let columns = match self.columns.as_ref() { + Some(columns) => columns, None => { - let size = self.output_size(); - (0..size.w.get()) - .map(|i| DataTableColumn::new(format!("Column {}", i + 1), true, i).into()) - .collect::>() + let width = self.value.size().w.get(); + &self.default_header(Some(width)) } - } + }; + + columns + .iter() + .map(|column| JsDataTableColumn::from(column.to_owned())) + .collect() + } + + /// Set the display of a column header at the given index. + pub fn normalize_column_names(&mut self) { + let mut all_names = vec![]; + + self.columns + .as_mut() + .unwrap() + .iter_mut() + .for_each(|column| { + let name = unique_name(&column.name.to_string(), &all_names, false); + column.name = CellValue::Text(name.to_owned()); + all_names.push(name); + }); } } @@ -204,17 +162,38 @@ pub mod test { data_table.value.clone().into_array().unwrap(), expected_values ); + } - // test setting header at index - data_table.set_header_at(0, "new".into(), true).unwrap(); - assert_eq!( - data_table.columns.as_ref().unwrap()[0].name, - CellValue::Text("new".into()) - ); + #[test] + #[parallel] + fn test_normalize_column_names() { + let mut data_table = new_data_table().1; + + let to_cols = |columns: Vec<&str>| { + columns + .iter() + .enumerate() + .map(|(i, c)| DataTableColumn::new(c.to_string(), true, i as u32)) + .collect::>() + }; - // test setting header display at index - data_table.set_header_display_at(0, false).unwrap(); - assert!(!data_table.columns.as_ref().unwrap()[0].display); + let assert_cols = + |data_table: &mut DataTable, columns: Vec<&str>, expected_columns: Vec<&str>| { + data_table.columns = Some(to_cols(columns)); + data_table.normalize_column_names(); + let data_table_cols = data_table.columns.clone().unwrap(); + expected_columns.iter().enumerate().for_each(|(i, c)| { + assert_eq!(data_table_cols[i].name.to_string(), c.to_string()); + }); + }; + + let columns = vec!["name", "name", "name", "name"]; + let expected_columns = vec!["name", "name1", "name2", "name3"]; + assert_cols(&mut data_table, columns, expected_columns); + + let columns = vec!["name1", "name1", "name2", "name2"]; + let expected_columns = vec!["name1", "name2", "name3", "name4"]; + assert_cols(&mut data_table, columns, expected_columns); } #[test] diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 688fba5c0e..5fc41e2547 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -36,7 +36,12 @@ impl Grid { let all_names = &self .sheets() .iter() - .flat_map(|sheet| sheet.data_tables.values().map(|table| table.name.as_str())) + .flat_map(|sheet| { + sheet + .data_tables + .values() + .map(|table| table.name.to_owned()) + }) .collect_vec(); unique_name(name, all_names, require_number) @@ -53,7 +58,9 @@ impl Grid { .try_sheet_mut(sheet_pos.sheet_id) .ok_or_else(|| anyhow!("Sheet {} not found", sheet_pos.sheet_id))?; - sheet.update_table_name(sheet_pos.into(), &unique_name)?; + sheet + .data_table_mut(sheet_pos.into())? + .update_table_name(&unique_name); Ok(()) } diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index f48c653d0a..24f8fca48a 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -7,6 +7,7 @@ impl Sheet { /// Sets or deletes a data table. /// /// Returns the old value if it was set. + #[cfg(test)] pub fn set_data_table(&mut self, pos: Pos, data_table: Option) -> Option { if let Some(data_table) = data_table { self.data_tables.insert(pos, data_table) @@ -15,12 +16,6 @@ impl Sheet { } } - pub fn update_table_name(&mut self, pos: Pos, name: &str) -> Result<()> { - self.data_table_mut(pos)?.update_table_name(name); - - Ok(()) - } - /// Returns a DataTable at a Pos pub fn data_table(&self, pos: Pos) -> Option<&DataTable> { self.data_tables.get(&pos) diff --git a/quadratic-core/src/util.rs b/quadratic-core/src/util.rs index 23a4675728..d0a081fdc0 100644 --- a/quadratic-core/src/util.rs +++ b/quadratic-core/src/util.rs @@ -213,13 +213,13 @@ pub fn unused_name(prefix: &str, already_used: &[&str]) -> String { /// Returns a unique name by appending numbers to the base name if the name is not unique. /// Starts at 1, and checks if the name is unique, then 2, etc. /// If `require_number` is true, the name will always have an appended number. -pub fn unique_name(name: &str, all_names: &[&str], require_number: bool) -> String { +pub fn unique_name(name: &str, all_names: &[String], require_number: bool) -> String { let base = MATCH_NUMBERS.replace(name, ""); let contains_number = base != name; let should_short_circuit = !require_number || contains_number; // short circuit if the name is unique - if should_short_circuit && !all_names.contains(&name) { + if should_short_circuit && !all_names.contains(&name.to_owned()) { return name.to_string(); } @@ -232,7 +232,10 @@ pub fn unique_name(name: &str, all_names: &[&str], require_number: bool) -> Stri let new_name_alt = format!("{} {}", base, num); let new_names = [new_name.as_str(), new_name_alt.as_str()]; - if !all_names.iter().any(|item| new_names.contains(item)) { + if !all_names + .iter() + .any(|item| new_names.contains(&item.as_str())) + { name = new_name; } diff --git a/quadratic-core/src/values/mod.rs b/quadratic-core/src/values/mod.rs index c9617bf59c..effa6a25c9 100644 --- a/quadratic-core/src/values/mod.rs +++ b/quadratic-core/src/values/mod.rs @@ -168,6 +168,18 @@ impl Value { }) } + /// Returns the size of the value. + pub fn size(&self) -> ArraySize { + match self { + Value::Single(_) => ArraySize::_1X1, + Value::Array(array) => array.size(), + Value::Tuple(t) => t + .first() + .unwrap_or(&Array::new_empty(ArraySize::_1X1)) + .size(), + } + } + /// Returns the contained error, or panics the value is not just a single /// error. #[cfg(test)] From a52a1b7df7444535641e13de637c77c229033205 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Mon, 11 Nov 2024 05:05:21 -0800 Subject: [PATCH 238/373] add description text for charts in code editor header --- .../ui/menus/CodeEditor/CodeEditorHeader.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx index 54b70cc2fb..1c7d9d5c68 100644 --- a/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx +++ b/quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorHeader.tsx @@ -3,6 +3,8 @@ import { codeEditorCodeCellAtom, codeEditorUnsavedChangesAtom } from '@/app/atom import { editorInteractionStatePermissionsAtom } from '@/app/atoms/editorInteractionStateAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { codeCellIsAConnection, getCodeCell, getConnectionUuid, getLanguage } from '@/app/helpers/codeCellLanguage'; import { KeyboardSymbols } from '@/app/helpers/keyboardSymbols'; import { LanguageIcon } from '@/app/ui/components/LanguageIcon'; @@ -153,6 +155,21 @@ export const CodeEditorHeader = ({ editorInst }: CodeEditorHeaderProps) => { }; }, [codeCellState.pos.x, codeCellState.pos.y, codeCellState.sheetId]); + const description = useMemo(() => { + if (codeCell) { + if (htmlCellsHandler.isHtmlCell(codeCellState.pos.x, codeCellState.pos.y)) { + return 'Python chart at'; + } else if ( + pixiApp.cellsSheets.getById(codeCellState.sheetId)?.cellsImages.isImageCell(codeCellState.pos.x, codeCellState.pos.y)) { + return 'JS chart at'; + } else { + return 'Cell at'; + } + } else { + return ''; + } + }, [codeCell, codeCellState.pos.x, codeCellState.pos.y, codeCellState.sheetId]); + return (
{
- Cell ({codeCellState.pos.x}, {codeCellState.pos.y}) + {description} ({codeCellState.pos.x}, {codeCellState.pos.y}) {currentCodeEditorCellIsNotInActiveSheet && ( - {currentSheetNameOfActiveCodeEditorCell} )} From 82e42a40929c16195a1df493f135d78b7217c3e1 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 11 Nov 2024 12:39:59 -0700 Subject: [PATCH 239/373] Fix data tables copy/paste issue, add panic hook --- .../web-workers/quadraticCore/worker/core.ts | 6 ++- .../src/controller/operations/clipboard.rs | 47 ++++++++++++++++++- quadratic-core/src/grid/sheet.rs | 5 +- quadratic-core/src/util.rs | 11 +++++ .../src/wasm_bindings/controller/mod.rs | 3 ++ 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 1050613452..16d03a5ea6 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -854,7 +854,11 @@ class Core { this.clientQueue.push(() => { if (!this.gridController) throw new Error('Expected gridController to be defined'); try { - const result = this.gridController.jumpCursor(sheetId, posToPos(current.x, current.y), JSON.stringify(direction)); + const result = this.gridController.jumpCursor( + sheetId, + posToPos(current.x, current.y), + JSON.stringify(direction) + ); const pos = JSON.parse(result); resolve({ x: Number(pos.x), y: Number(pos.y) }); } catch (error: any) { diff --git a/quadratic-core/src/controller/operations/clipboard.rs b/quadratic-core/src/controller/operations/clipboard.rs index f9b34986f6..145b882d6a 100644 --- a/quadratic-core/src/controller/operations/clipboard.rs +++ b/quadratic-core/src/controller/operations/clipboard.rs @@ -32,7 +32,7 @@ pub enum PasteSpecial { /// /// For example, this is used to copy and paste a column /// on top of another column, or a sheet on top of another sheet. -#[derive(Default, Debug, Serialize, Deserialize)] +#[derive(Default, Debug, Serialize, Deserialize, Clone, Copy)] pub struct ClipboardOrigin { pub x: i64, pub y: i64, @@ -402,6 +402,8 @@ impl GridController { .map_err(|e| error(e.to_string(), "Serialization error"))?; drop(decoded); + let mut has_data_table = false; + // loop through the clipboard and replace cell references in formulas for (x, col) in clipboard.cells.columns.iter_mut().enumerate() { for (&y, cell) in col.iter_mut() { @@ -417,11 +419,18 @@ impl GridController { ); } } + CellValue::Import(_) => { + has_data_table = true; + } _ => { /* noop */ } }; } } + if has_data_table { + clipboard.cells = clipboard.values.clone(); + } + Ok(self.set_clipboard_cells(selection, clipboard, special)) } } @@ -439,6 +448,7 @@ mod test { use super::{PasteSpecial, *}; use crate::controller::active_transactions::transaction_name::TransactionName; + use crate::controller::user_actions::import::tests::simple_csv; use crate::grid::formats::format_update::FormatUpdate; use crate::grid::sheet::validations::validation_rules::ValidationRule; use crate::grid::SheetId; @@ -753,4 +763,39 @@ mod test { Some(CellValue::Number(BigDecimal::from(6))) ); } + + #[test] + #[parallel] + fn paste_clipboard_with_data_table() { + let (mut gc, sheet_id, _, _) = simple_csv(); + + let (_, html) = gc + .sheet(sheet_id) + .copy_to_clipboard(&Selection { + sheet_id, + x: 0, + y: 0, + rects: Some(vec![Rect::new(0, 0, 3, 10)]), + ..Default::default() + }) + .unwrap(); + + gc.paste_from_clipboard( + Selection { + sheet_id, + x: 20, + y: 20, + ..Default::default() + }, + None, + Some(html), + PasteSpecial::None, + None, + ); + + assert_eq!( + gc.sheet(sheet_id).cell_value((20, 20).into()), + Some(CellValue::Text("city".into())) + ); + } } diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index 0d4fb40db1..d8a7a331eb 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -261,12 +261,15 @@ impl Sheet { }) } - /// Returns the cell_value at the Pos in column.values. This does not check or return results within data_tables. + /// Returns the cell_value at the Pos in column.values. This does not check + /// or return results within data_tables. pub fn cell_value(&self, pos: Pos) -> Option { let column = self.get_column(pos.x)?; column.values.get(&pos.y).cloned() } + /// Returns the ref of thecell_value at the Pos in column.values. This does + /// not check or return results within data_tables. pub fn cell_value_ref(&self, pos: Pos) -> Option<&CellValue> { let column = self.get_column(pos.x)?; column.values.get(&pos.y) diff --git a/quadratic-core/src/util.rs b/quadratic-core/src/util.rs index d0a081fdc0..8c50b87a23 100644 --- a/quadratic-core/src/util.rs +++ b/quadratic-core/src/util.rs @@ -306,6 +306,17 @@ pub fn case_fold(s: &str) -> String { s.to_uppercase() // TODO: want proper Unicode case folding } +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} + #[cfg(test)] pub(crate) fn assert_f64_approx_eq(expected: f64, actual: &str) { const EPSILON: f64 = 0.0001; diff --git a/quadratic-core/src/wasm_bindings/controller/mod.rs b/quadratic-core/src/wasm_bindings/controller/mod.rs index 0857951aa3..b311788d09 100644 --- a/quadratic-core/src/wasm_bindings/controller/mod.rs +++ b/quadratic-core/src/wasm_bindings/controller/mod.rs @@ -3,6 +3,7 @@ use crate::grid::js_types::*; use crate::wasm_bindings::controller::sheet_info::SheetInfo; use js_sys::{ArrayBuffer, Uint8Array}; use std::str::FromStr; +use util::set_panic_hook; pub mod auto_complete; pub mod borders; @@ -34,6 +35,8 @@ impl GridController { last_sequence_num: u32, initialize: bool, ) -> Result { + set_panic_hook(); + match file::import(file).map_err(|e| e.to_string()) { Ok(file) => { let mut grid = GridController::from_grid(file, last_sequence_num as u64); From fe48d69561e095cd4b7811c266bf05d4398793d6 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 11 Nov 2024 16:50:57 -0700 Subject: [PATCH 240/373] Move data table partially working, enforce new nameing standards --- .../interaction/pointer/PointerCellMoving.ts | 8 ++++++++ quadratic-core/src/controller/dependencies.rs | 2 ++ .../controller/execution/control_transaction.rs | 8 ++++---- .../execute_operation/execute_move_cells.rs | 3 +++ .../src/controller/execution/run_code/mod.rs | 16 ++++++++-------- quadratic-core/src/grid/sheet/clipboard.rs | 10 ++++++---- 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index 8e8811b05a..c56b9c7934 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -1,3 +1,4 @@ +import { getTable } from '@/app/actions/dataTableSpec'; import { PanMode } from '@/app/atoms/gridPanModeAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; @@ -233,7 +234,14 @@ export class PointerCellMoving { this.movingCells && (this.startCell.x !== this.movingCells.toColumn || this.startCell.y !== this.movingCells.toRow) ) { + const table = getTable(); const rectangle = sheets.sheet.cursor.getLargestMultiCursorRectangle(); + + if (table) { + rectangle.width = table.w; + rectangle.height = table.h; + } + quadraticCore.moveCells( rectToSheetRect( new Rectangle(rectangle.x, rectangle.y, rectangle.width - 1, rectangle.height - 1), diff --git a/quadratic-core/src/controller/dependencies.rs b/quadratic-core/src/controller/dependencies.rs index 39e9c4c23b..99368d05ec 100644 --- a/quadratic-core/src/controller/dependencies.rs +++ b/quadratic-core/src/controller/dependencies.rs @@ -21,6 +21,8 @@ impl GridController { }); }); + dbgjs!(format!("dependent_cells: {:?}", dependent_cells)); + if dependent_cells.is_empty() { None } else { diff --git a/quadratic-core/src/controller/execution/control_transaction.rs b/quadratic-core/src/controller/execution/control_transaction.rs index e3484857f4..db62b7f657 100644 --- a/quadratic-core/src/controller/execution/control_transaction.rs +++ b/quadratic-core/src/controller/execution/control_transaction.rs @@ -310,10 +310,10 @@ impl GridController { let name = match code.language { CodeCellLanguage::Connection { kind, .. } => match kind { - ConnectionKind::Postgres => "Postgres 1", - ConnectionKind::Mysql => "MySQL 1", - ConnectionKind::Mssql => "MSSQL 1", - ConnectionKind::Snowflake => "Snowflake 1", + ConnectionKind::Postgres => "Postgres1", + ConnectionKind::Mysql => "MySQL1", + ConnectionKind::Mssql => "MSSQL1", + ConnectionKind::Snowflake => "Snowflake1", }, // this should not happen _ => "Connection 1", diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs b/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs index eb8856a13a..30bcd5a42d 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_move_cells.rs @@ -20,8 +20,10 @@ impl GridController { // approach. let mut operations = VecDeque::new(); let selection = Selection::rect(source.into(), source.sheet_id); + if let Ok((cut_ops, _, html)) = self.cut_to_clipboard_operations(&selection) { operations.extend(cut_ops); + if let Ok(paste_ops) = self.paste_html_operations( &Selection::sheet_rect(dest.into()), html, @@ -29,6 +31,7 @@ impl GridController { ) { operations.extend(paste_ops); } + operations.extend(transaction.operations.drain(..)); transaction.operations = operations; } diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index db5fecbd95..383d80cf9d 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -281,10 +281,10 @@ impl GridController { }, }; let table_name = match code_cell_value.language { - CodeCellLanguage::Formula => "Formula 1", - CodeCellLanguage::Javascript => "JavaScript 1", - CodeCellLanguage::Python => "Python 1", - _ => "Table 1", + CodeCellLanguage::Formula => "Formula1", + CodeCellLanguage::Javascript => "JavaScript1", + CodeCellLanguage::Python => "Python1", + _ => "Table1", }; let new_data_table = DataTable::new( DataTableKind::CodeRun(new_code_run), @@ -310,10 +310,10 @@ impl GridController { language: CodeCellLanguage, ) -> DataTable { let table_name = match language { - CodeCellLanguage::Formula => "Formula 1", - CodeCellLanguage::Javascript => "JavaScript 1", - CodeCellLanguage::Python => "Python 1", - _ => "Table 1", + CodeCellLanguage::Formula => "Formula1", + CodeCellLanguage::Javascript => "JavaScript1", + CodeCellLanguage::Python => "Python1", + _ => "Table1", }; let Some(sheet) = self.try_sheet_mut(start.sheet_id) else { // todo: this is probably not the best place to handle this diff --git a/quadratic-core/src/grid/sheet/clipboard.rs b/quadratic-core/src/grid/sheet/clipboard.rs index 881d61aa43..a00455c152 100644 --- a/quadratic-core/src/grid/sheet/clipboard.rs +++ b/quadratic-core/src/grid/sheet/clipboard.rs @@ -180,8 +180,8 @@ impl Sheet { // allow copying of code_run values (unless CellValue::Code is also in the clipboard) self.iter_code_output_in_rect(bounds) - .filter(|(_, code_cell)| !code_cell.spill_error) - .for_each(|(output_rect, code_cell)| { + .filter(|(_, data_table)| !data_table.spill_error) + .for_each(|(output_rect, data_table)| { // only change the cells if the CellValue::Code is not in the selection box let code_pos = Pos { x: output_rect.min.x, @@ -214,17 +214,19 @@ impl Sheet { // add the code_run output to clipboard.values for y in y_start..=y_end { for x in x_start..=x_end { - if let Some(value) = code_cell + if let Some(value) = data_table .cell_value_at((x - code_pos.x) as u32, (y - code_pos.y) as u32) { let pos = Pos { x: x - bounds.min.x, y: y - bounds.min.y, }; + if selection.contains_pos(Pos { x, y }) { if include_in_cells { cells.set(pos.x as u32, pos.y as u32, value.clone()); } + values.set(pos.x as u32, pos.y as u32, value); } } @@ -252,10 +254,10 @@ impl Sheet { clipboard_origin.column = sheet_bounds.map(|b| b.min.x); } } + let sheet_formats = self.sheet_formats(selection, &clipboard_origin); let validations = self.validations.to_clipboard(selection, &clipboard_origin); let borders = self.borders.to_clipboard(selection); - let clipboard = Clipboard { cells, formats, From b5e96e45de5dc3d3c566aedec816b032ad01ce10 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Tue, 12 Nov 2024 04:39:03 -0800 Subject: [PATCH 241/373] thumbnails don't include selected table names --- .../src/app/gridGL/cells/CellsSheet.ts | 1 + .../src/app/gridGL/cells/tables/Tables.ts | 34 +++++++++++++++++++ .../src/app/gridGL/pixiApp/thumbnail.ts | 7 ++-- quadratic-client/src/routes/file.$uuid.tsx | 2 +- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts index f454af26ff..f396ca4fd9 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts @@ -73,6 +73,7 @@ export class CellsSheet extends Container { toggleOutlines(off?: boolean) { this.cellsMarkers.visible = off ?? true; + this.tables.toggleOutlines(); } showLabel(x: number, y: number, show: boolean) { diff --git a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts index 57a6803124..68dfd01fbd 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/Tables.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/Tables.ts @@ -33,6 +33,13 @@ export class Tables extends Container
{ // tracks which tables are html or image cells private htmlOrImage: Set; + private saveToggleOutlines?: { + active?: Table; + hover?: Table; + context?: Table; + action?: Table; + }; + tableCursor: string | undefined; constructor(cellsSheet: CellsSheet) { @@ -428,6 +435,33 @@ export class Tables extends Container
{ } } + // Toggles the outlines of the table (used during thumbnail generation) + toggleOutlines() { + if (this.saveToggleOutlines) { + this.activeTable = this.saveToggleOutlines.active; + this.activeTable?.showActive(true); + this.hoverTable = this.saveToggleOutlines.hover; + this.hoverTable?.showActive(false); + this.contextMenuTable = this.saveToggleOutlines.context; + this.contextMenuTable?.showActive(false); + this.actionDataTable = this.saveToggleOutlines.action; + this.actionDataTable?.showActive(false); + pixiApp.setViewportDirty(); + this.saveToggleOutlines = undefined; + } else { + this.saveToggleOutlines = { + active: this.activeTable, + hover: this.hoverTable, + context: this.contextMenuTable, + action: this.actionDataTable, + }; + this.activeTable?.hideActive(); + this.hoverTable?.hideActive(); + this.contextMenuTable?.hideActive(); + this.actionDataTable?.hideActive(); + } + } + resizeTable(x: number, y: number, width: number, height: number) { const table = this.children.find((table) => table.codeCell.x === x && table.codeCell.y === y); if (table) { diff --git a/quadratic-client/src/app/gridGL/pixiApp/thumbnail.ts b/quadratic-client/src/app/gridGL/pixiApp/thumbnail.ts index 62bf7abbb0..f9590fcf3f 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/thumbnail.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/thumbnail.ts @@ -18,15 +18,16 @@ class Thumbnail { private renderer?: Renderer; constructor() { - events.on('generateThumbnail', this.generateThumbnail); + events.on('generateThumbnail', this.setThumbnailDirty); } - generateThumbnail = () => { + setThumbnailDirty = () => { + console.log('setThumbnailDirty'); this.thumbnailDirty = true; }; destroy() { - events.off('generateThumbnail', this.generateThumbnail); + events.off('generateThumbnail', this.setThumbnailDirty); if (this.renderer) { this.renderer.destroy(false); } diff --git a/quadratic-client/src/routes/file.$uuid.tsx b/quadratic-client/src/routes/file.$uuid.tsx index 7920d75548..662e436f8f 100644 --- a/quadratic-client/src/routes/file.$uuid.tsx +++ b/quadratic-client/src/routes/file.$uuid.tsx @@ -81,7 +81,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs): Promise Date: Tue, 12 Nov 2024 16:46:51 -0700 Subject: [PATCH 242/373] Complete data move/cut/copy for various scenarios --- quadratic-core/src/controller/dependencies.rs | 2 - .../src/controller/execution/run_code/mod.rs | 9 ++++ .../src/controller/operations/clipboard.rs | 50 ++++++++++++------- .../src/controller/user_actions/data_table.rs | 1 + quadratic-core/src/grid/sheet/clipboard.rs | 11 ++-- 5 files changed, 47 insertions(+), 26 deletions(-) diff --git a/quadratic-core/src/controller/dependencies.rs b/quadratic-core/src/controller/dependencies.rs index 99368d05ec..39e9c4c23b 100644 --- a/quadratic-core/src/controller/dependencies.rs +++ b/quadratic-core/src/controller/dependencies.rs @@ -21,8 +21,6 @@ impl GridController { }); }); - dbgjs!(format!("dependent_cells: {:?}", dependent_cells)); - if dependent_cells.is_empty() { None } else { diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 383d80cf9d..10f14e8c83 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -414,6 +414,15 @@ impl GridController { js_code_result.chart_pixel_output, ); + // update the name if the data_table is an image or html + if data_table.is_html() || data_table.is_image() { + data_table.name = match language { + CodeCellLanguage::Javascript => "JSChart1".to_string(), + CodeCellLanguage::Python => "PythonChart1".to_string(), + _ => table_name.to_string(), + }; + } + // set alternating colors to false if chart_pixel_output is set. if js_code_result.chart_pixel_output.is_some() { data_table.alternating_colors = false; diff --git a/quadratic-core/src/controller/operations/clipboard.rs b/quadratic-core/src/controller/operations/clipboard.rs index 145b882d6a..60f7943f68 100644 --- a/quadratic-core/src/controller/operations/clipboard.rs +++ b/quadratic-core/src/controller/operations/clipboard.rs @@ -13,7 +13,7 @@ use crate::grid::formats::format::Format; use crate::grid::formats::Formats; use crate::grid::sheet::borders::BorderStyleCellUpdates; use crate::grid::sheet::validations::validation::Validation; -use crate::grid::CodeCellLanguage; +use crate::grid::{CodeCellLanguage, DataTableKind}; use crate::selection::Selection; use crate::{CellValue, Pos, SheetPos, SheetRect}; @@ -109,7 +109,9 @@ impl GridController { .flat_map(|(x, col)| { col.iter() .filter_map(|(y, cell)| match cell { - CellValue::Code(_) => Some((x as u32, *y as u32)), + CellValue::Code(_) | CellValue::Import(_) => { + Some((x as u32, *y as u32)) + } _ => None, }) .collect::>() @@ -252,6 +254,7 @@ impl GridController { clipboard.values, special, ); + if let Some(values) = values { ops.push(Operation::SetCellValues { sheet_pos: start_pos.to_sheet_pos(selection.sheet_id), @@ -259,14 +262,32 @@ impl GridController { }); } - code.iter().for_each(|(x, y)| { - let sheet_pos = SheetPos { - x: start_pos.x + *x as i64, - y: start_pos.y + *y as i64, - sheet_id: selection.sheet_id, - }; - ops.push(Operation::ComputeCode { sheet_pos }); - }); + if let Some(sheet) = self.try_sheet(selection.sheet_id) { + code.iter().for_each(|(x, y)| { + let sheet_pos = SheetPos { + x: start_pos.x + *x as i64, + y: start_pos.y + *y as i64, + sheet_id: selection.sheet_id, + }; + + let source_pos = Pos { + x: clipboard.origin.x + *x as i64, + y: clipboard.origin.y + *y as i64, + }; + + if let Some(data_table) = sheet.data_table(source_pos) { + if matches!(data_table.kind, DataTableKind::Import(_)) { + ops.push(Operation::SetCodeRun { + sheet_pos, + code_run: Some(data_table.to_owned()), + index: 0, + }); + } + } + + ops.push(Operation::ComputeCode { sheet_pos }); + }); + } } PasteSpecial::Values => { let (values, _) = GridController::cell_values_from_clipboard_cells( @@ -402,8 +423,6 @@ impl GridController { .map_err(|e| error(e.to_string(), "Serialization error"))?; drop(decoded); - let mut has_data_table = false; - // loop through the clipboard and replace cell references in formulas for (x, col) in clipboard.cells.columns.iter_mut().enumerate() { for (&y, cell) in col.iter_mut() { @@ -419,18 +438,11 @@ impl GridController { ); } } - CellValue::Import(_) => { - has_data_table = true; - } _ => { /* noop */ } }; } } - if has_data_table { - clipboard.cells = clipboard.values.clone(); - } - Ok(self.set_clipboard_cells(selection, clipboard, special)) } } diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index d065c33f57..49b8494b61 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -7,6 +7,7 @@ use crate::{ use anyhow::{anyhow, Result}; impl GridController { + /// Returns all data tables within the given sheet position. pub fn data_tables_within(&self, sheet_pos: SheetPos) -> Result> { let sheet = self .try_sheet(sheet_pos.sheet_id) diff --git a/quadratic-core/src/grid/sheet/clipboard.rs b/quadratic-core/src/grid/sheet/clipboard.rs index a00455c152..d6b670badb 100644 --- a/quadratic-core/src/grid/sheet/clipboard.rs +++ b/quadratic-core/src/grid/sheet/clipboard.rs @@ -183,7 +183,7 @@ impl Sheet { .filter(|(_, data_table)| !data_table.spill_error) .for_each(|(output_rect, data_table)| { // only change the cells if the CellValue::Code is not in the selection box - let code_pos = Pos { + let data_table_pos = Pos { x: output_rect.min.x, y: output_rect.min.y, }; @@ -209,14 +209,15 @@ impl Sheet { }; // add the CellValue to cells if the code is not included in the clipboard - let include_in_cells = !bounds.contains(code_pos); + let include_in_cells = !bounds.contains(data_table_pos); // add the code_run output to clipboard.values for y in y_start..=y_end { for x in x_start..=x_end { - if let Some(value) = data_table - .cell_value_at((x - code_pos.x) as u32, (y - code_pos.y) as u32) - { + if let Some(value) = data_table.cell_value_at( + (x - data_table_pos.x) as u32, + (y - data_table_pos.y) as u32, + ) { let pos = Pos { x: x - bounds.min.x, y: y - bounds.min.y, From 019d596b68ce138a148b0440229f05b3676f328c Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 13 Nov 2024 10:00:06 -0700 Subject: [PATCH 243/373] Rename columns to column header --- quadratic-core/src/bin/export_types.rs | 6 +- .../execute_operation/execute_data_table.rs | 2 +- .../src/controller/operations/data_table.rs | 4 +- .../src/controller/operations/operation.rs | 4 +- .../src/controller/user_actions/data_table.rs | 4 +- quadratic-core/src/grid/data_table/column.rs | 56 +++++++++---------- quadratic-core/src/grid/data_table/mod.rs | 5 +- quadratic-core/src/grid/data_table/row.rs | 0 .../src/grid/file/serialize/data_table.rs | 4 +- quadratic-core/src/grid/js_types.rs | 18 +++--- quadratic-core/src/grid/sheet/rendering.rs | 4 +- .../wasm_bindings/controller/data_table.rs | 2 +- 12 files changed, 54 insertions(+), 55 deletions(-) create mode 100644 quadratic-core/src/grid/data_table/row.rs diff --git a/quadratic-core/src/bin/export_types.rs b/quadratic-core/src/bin/export_types.rs index e0a899d689..47f4dbc64b 100644 --- a/quadratic-core/src/bin/export_types.rs +++ b/quadratic-core/src/bin/export_types.rs @@ -6,8 +6,8 @@ use formulas::{CellRef, CellRefCoord, RangeRef}; use grid::data_table::sort::{DataTableSort, SortDirection}; use grid::formats::format::Format; use grid::js_types::{ - CellFormatSummary, JsCellValue, JsClipboard, JsDataTableColumn, JsOffset, JsPos, JsRenderFill, - JsRowHeight, JsSheetFill, JsValidationWarning, + CellFormatSummary, JsCellValue, JsClipboard, JsDataTableColumnHeader, JsOffset, JsPos, + JsRenderFill, JsRowHeight, JsSheetFill, JsValidationWarning, }; use grid::sheet::borders::{BorderStyleCell, BorderStyleTimestamp}; use grid::sheet::jump_cursor::JumpDirection; @@ -96,7 +96,7 @@ fn main() { JsClipboard, JsCodeCell, JsCodeResult, - JsDataTableColumn, + JsDataTableColumnHeader, JsGetCellResponse, JsHtmlOutput, JsNumber, diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 2ccd965a67..008c8f7fb7 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -368,7 +368,7 @@ impl GridController { let old_columns = columns.as_ref().and_then(|columns| { let old_columns = data_table.columns.to_owned(); data_table.columns = Some(columns.to_owned()); - data_table.normalize_column_names(); + data_table.normalize_column_header_names(); old_columns }); diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index 8153df4558..ec8a96d64b 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -3,7 +3,7 @@ use crate::{ cellvalue::Import, controller::GridController, grid::{ - data_table::{column::DataTableColumn, sort::DataTableSort}, + data_table::{column::DataTableColumnHeader, sort::DataTableSort}, DataTableKind, }, CellValue, SheetPos, SheetRect, @@ -51,7 +51,7 @@ impl GridController { sheet_pos: SheetPos, name: Option, alternating_colors: Option, - columns: Option>, + columns: Option>, show_header: Option, _cursor: Option, ) -> Vec { diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 2ccdba1153..567e99f868 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use crate::{ cell_values::CellValues, grid::{ - data_table::{column::DataTableColumn, sort::DataTableSort}, + data_table::{column::DataTableColumnHeader, sort::DataTableSort}, file::sheet_schema::SheetSchema, formats::Formats, formatting::CellFmtArray, @@ -65,7 +65,7 @@ pub enum Operation { sheet_pos: SheetPos, name: Option, alternating_colors: Option, - columns: Option>, + columns: Option>, show_header: Option, }, SortDataTable { diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index 49b8494b61..86134506ae 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -1,6 +1,6 @@ use crate::{ controller::{active_transactions::transaction_name::TransactionName, GridController}, - grid::{data_table::column::DataTableColumn, sort::DataTableSort}, + grid::{data_table::column::DataTableColumnHeader, sort::DataTableSort}, Pos, SheetPos, SheetRect, }; @@ -53,7 +53,7 @@ impl GridController { sheet_pos: SheetPos, name: Option, alternating_colors: Option, - columns: Option>, + columns: Option>, show_header: Option, cursor: Option, ) { diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index 655ff93c06..15281c784d 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -3,20 +3,20 @@ use serde::{Deserialize, Serialize}; use super::DataTable; -use crate::grid::js_types::JsDataTableColumn; +use crate::grid::js_types::JsDataTableColumnHeader; use crate::util::unique_name; use crate::{CellValue, Value}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct DataTableColumn { +pub struct DataTableColumnHeader { pub name: CellValue, pub display: bool, pub value_index: u32, } -impl DataTableColumn { +impl DataTableColumnHeader { pub fn new(name: String, display: bool, value_index: u32) -> Self { - DataTableColumn { + DataTableColumnHeader { name: CellValue::Text(name), display, value_index, @@ -35,13 +35,13 @@ impl DataTable { array .iter() .enumerate() - .map(|(i, value)| DataTableColumn::new(value.to_string(), true, i as u32)) - .collect::>() + .map(|(i, value)| DataTableColumnHeader::new(value.to_string(), true, i as u32)) + .collect::>() }), _ => None, }; - self.normalize_column_names(); + self.normalize_column_header_names(); } pub fn toggle_first_row_as_header(&mut self, first_row_as_header: bool) { @@ -55,13 +55,13 @@ impl DataTable { /// Create default column headings for the DataTable. /// For example, the column headings will be "Column 1", "Column 2", etc. - pub fn default_header(&self, width: Option) -> Vec { + pub fn default_header(&self, width: Option) -> Vec { let width = width.unwrap_or(self.value.size().w.get()); match self.value { Value::Array(_) => (1..=width) - .map(|i| DataTableColumn::new(format!("Column {i}"), true, i - 1)) - .collect::>(), + .map(|i| DataTableColumnHeader::new(format!("Column {i}"), true, i - 1)) + .collect::>(), _ => vec![], } } @@ -82,7 +82,7 @@ impl DataTable { /// Prepares the columns to be sent to the client. If no columns are set, it /// will create default columns. - pub fn send_columns(&self) -> Vec { + pub fn send_columns(&self) -> Vec { let columns = match self.columns.as_ref() { Some(columns) => columns, None => { @@ -93,23 +93,21 @@ impl DataTable { columns .iter() - .map(|column| JsDataTableColumn::from(column.to_owned())) + .map(|column| JsDataTableColumnHeader::from(column.to_owned())) .collect() } /// Set the display of a column header at the given index. - pub fn normalize_column_names(&mut self) { + pub fn normalize_column_header_names(&mut self) { let mut all_names = vec![]; - self.columns - .as_mut() - .unwrap() - .iter_mut() - .for_each(|column| { + if let Some(columns) = self.columns.as_mut() { + columns.iter_mut().for_each(|column| { let name = unique_name(&column.name.to_string(), &all_names, false); column.name = CellValue::Text(name.to_owned()); all_names.push(name); }); + } } } @@ -135,10 +133,10 @@ pub mod test { data_table.apply_default_header(); let expected_columns = vec![ - DataTableColumn::new("Column 1".into(), true, 0), - DataTableColumn::new("Column 2".into(), true, 1), - DataTableColumn::new("Column 3".into(), true, 2), - DataTableColumn::new("Column 4".into(), true, 3), + DataTableColumnHeader::new("Column 1".into(), true, 0), + DataTableColumnHeader::new("Column 2".into(), true, 1), + DataTableColumnHeader::new("Column 3".into(), true, 2), + DataTableColumnHeader::new("Column 4".into(), true, 3), ]; assert_eq!(data_table.columns, Some(expected_columns)); @@ -150,10 +148,10 @@ pub mod test { data_table.apply_first_row_as_header(); let expected_columns = vec![ - DataTableColumn::new("city".into(), true, 0), - DataTableColumn::new("region".into(), true, 1), - DataTableColumn::new("country".into(), true, 2), - DataTableColumn::new("population".into(), true, 3), + DataTableColumnHeader::new("city".into(), true, 0), + DataTableColumnHeader::new("region".into(), true, 1), + DataTableColumnHeader::new("country".into(), true, 2), + DataTableColumnHeader::new("population".into(), true, 3), ]; assert_eq!(data_table.columns, Some(expected_columns)); @@ -173,14 +171,14 @@ pub mod test { columns .iter() .enumerate() - .map(|(i, c)| DataTableColumn::new(c.to_string(), true, i as u32)) - .collect::>() + .map(|(i, c)| DataTableColumnHeader::new(c.to_string(), true, i as u32)) + .collect::>() }; let assert_cols = |data_table: &mut DataTable, columns: Vec<&str>, expected_columns: Vec<&str>| { data_table.columns = Some(to_cols(columns)); - data_table.normalize_column_names(); + data_table.normalize_column_header_names(); let data_table_cols = data_table.columns.clone().unwrap(); expected_columns.iter().enumerate().for_each(|(i, c)| { assert_eq!(data_table_cols[i].name.to_string(), c.to_string()); diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 5fc41e2547..4a62be978c 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -6,6 +6,7 @@ pub mod column; pub mod display_value; +pub mod row; pub mod sort; pub mod table_formats; use std::num::NonZeroU32; @@ -18,7 +19,7 @@ use crate::{ }; use anyhow::{anyhow, Ok, Result}; use chrono::{DateTime, Utc}; -use column::DataTableColumn; +use column::DataTableColumnHeader; use itertools::Itertools; use serde::{Deserialize, Serialize}; use sort::DataTableSort; @@ -83,7 +84,7 @@ pub struct DataTable { pub name: String, pub header_is_first_row: bool, pub show_header: bool, - pub columns: Option>, + pub columns: Option>, pub sort: Option>, pub display_buffer: Option>, pub value: Value, diff --git a/quadratic-core/src/grid/data_table/row.rs b/quadratic-core/src/grid/data_table/row.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index d019960eb6..6f7c958f02 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -9,7 +9,7 @@ use crate::{ grid::{ block::SameValue, data_table::{ - column::DataTableColumn, + column::DataTableColumnHeader, sort::{DataTableSort, SortDirection}, }, formats::format::Format, @@ -223,7 +223,7 @@ pub(crate) fn import_data_table_builder( _ => format!("Column {}", index + 1), }; - DataTableColumn::new(column_name, column.display, column.value_index) + DataTableColumnHeader::new(column_name, column.display, column.value_index) }) .collect() }), diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index aa74ff92b7..93f1ccb726 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use uuid::Uuid; -use super::data_table::{column::DataTableColumn, sort::DataTableSort}; +use super::data_table::{column::DataTableColumnHeader, sort::DataTableSort}; use super::formats::format::Format; use super::formatting::{CellAlign, CellVerticalAlign, CellWrap}; use super::sheet::validations::validation::ValidationStyle; @@ -201,7 +201,7 @@ pub struct JsRenderCodeCell { pub state: JsRenderCodeCellState, pub spill_error: Option>, pub name: String, - pub columns: Vec, + pub columns: Vec, pub first_row_header: bool, pub show_header: bool, pub sort: Option>, @@ -248,15 +248,15 @@ pub struct JsValidationSheet { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] -pub struct JsDataTableColumn { +pub struct JsDataTableColumnHeader { pub name: String, pub display: bool, pub value_index: u32, } -impl From for JsDataTableColumn { - fn from(column: DataTableColumn) -> Self { - JsDataTableColumn { +impl From for JsDataTableColumnHeader { + fn from(column: DataTableColumnHeader) -> Self { + JsDataTableColumnHeader { name: column.name.to_string(), display: column.display, value_index: column.value_index, @@ -264,9 +264,9 @@ impl From for JsDataTableColumn { } } -impl From for DataTableColumn { - fn from(column: JsDataTableColumn) -> Self { - DataTableColumn { +impl From for DataTableColumnHeader { + fn from(column: JsDataTableColumnHeader) -> Self { + DataTableColumnHeader { name: column.name.into(), display: column.display, value_index: column.value_index, diff --git a/quadratic-core/src/grid/sheet/rendering.rs b/quadratic-core/src/grid/sheet/rendering.rs index d57b0a4875..b7e9d3e082 100644 --- a/quadratic-core/src/grid/sheet/rendering.rs +++ b/quadratic-core/src/grid/sheet/rendering.rs @@ -656,7 +656,7 @@ mod tests { grid::{ formats::{format::Format, format_update::FormatUpdate, Formats}, js_types::{ - JsDataTableColumn, JsHtmlOutput, JsNumber, JsRenderCell, JsRenderCellSpecial, + JsDataTableColumnHeader, JsHtmlOutput, JsNumber, JsRenderCell, JsRenderCellSpecial, JsRenderCodeCell, JsSheetFill, JsValidationWarning, }, sheet::validations::{ @@ -1128,7 +1128,7 @@ mod tests { state: crate::grid::js_types::JsRenderCodeCellState::Success, spill_error: None, name: "Table 1".to_string(), - columns: vec![JsDataTableColumn { + columns: vec![JsDataTableColumnHeader { name: "Column 1".into(), display: true, value_index: 0, diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index e71bc61316..c92573a046 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -108,7 +108,7 @@ impl GridController { let columns = columns_js .map(|c| { - serde_json::from_str::>(&c) + serde_json::from_str::>(&c) .map_err(|e| e.to_string()) .map(|c| c.into_iter().map(|c| c.into()).collect()) }) From 85710c6dc9b5809553a970a1e4a200798a95480c Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 13 Nov 2024 10:00:49 -0700 Subject: [PATCH 244/373] Rename column.rs to column_header.rs --- .../src/grid/data_table/{column.rs => column_header.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename quadratic-core/src/grid/data_table/{column.rs => column_header.rs} (100%) diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column_header.rs similarity index 100% rename from quadratic-core/src/grid/data_table/column.rs rename to quadratic-core/src/grid/data_table/column_header.rs From 7ba1ad7f344589bdcb7a09311e69f8244247667c Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 13 Nov 2024 10:08:47 -0700 Subject: [PATCH 245/373] Finish renaming --- .../execute_operation/execute_data_table.rs | 4 ++-- .../src/controller/operations/data_table.rs | 2 +- .../src/controller/operations/operation.rs | 2 +- .../src/controller/user_actions/data_table.rs | 2 +- quadratic-core/src/grid/data_table/column.rs | 1 + .../src/grid/data_table/column_header.rs | 18 +++++++++--------- .../src/grid/data_table/display_value.rs | 14 +++++++------- quadratic-core/src/grid/data_table/mod.rs | 7 ++++--- .../src/grid/file/serialize/data_table.rs | 6 +++--- quadratic-core/src/grid/js_types.rs | 2 +- 10 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 quadratic-core/src/grid/data_table/column.rs diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 008c8f7fb7..39d195e716 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -366,8 +366,8 @@ impl GridController { }); let old_columns = columns.as_ref().and_then(|columns| { - let old_columns = data_table.columns.to_owned(); - data_table.columns = Some(columns.to_owned()); + let old_columns = data_table.column_headers.to_owned(); + data_table.column_headers = Some(columns.to_owned()); data_table.normalize_column_header_names(); old_columns diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index ec8a96d64b..14fe0de246 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -3,7 +3,7 @@ use crate::{ cellvalue::Import, controller::GridController, grid::{ - data_table::{column::DataTableColumnHeader, sort::DataTableSort}, + data_table::{column_header::DataTableColumnHeader, sort::DataTableSort}, DataTableKind, }, CellValue, SheetPos, SheetRect, diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 567e99f868..3faadf1b5d 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use crate::{ cell_values::CellValues, grid::{ - data_table::{column::DataTableColumnHeader, sort::DataTableSort}, + data_table::{column_header::DataTableColumnHeader, sort::DataTableSort}, file::sheet_schema::SheetSchema, formats::Formats, formatting::CellFmtArray, diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index 86134506ae..0473ebff99 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -1,6 +1,6 @@ use crate::{ controller::{active_transactions::transaction_name::TransactionName, GridController}, - grid::{data_table::column::DataTableColumnHeader, sort::DataTableSort}, + grid::{data_table::column_header::DataTableColumnHeader, sort::DataTableSort}, Pos, SheetPos, SheetRect, }; diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/quadratic-core/src/grid/data_table/column.rs @@ -0,0 +1 @@ + diff --git a/quadratic-core/src/grid/data_table/column_header.rs b/quadratic-core/src/grid/data_table/column_header.rs index 15281c784d..0924008907 100644 --- a/quadratic-core/src/grid/data_table/column_header.rs +++ b/quadratic-core/src/grid/data_table/column_header.rs @@ -29,7 +29,7 @@ impl DataTable { pub fn apply_first_row_as_header(&mut self) { self.header_is_first_row = true; - self.columns = match self.value { + self.column_headers = match self.value { // Value::Array(ref mut array) => array.shift().ok().map(|array| { Value::Array(ref mut array) => array.get_row(0).ok().map(|array| { array @@ -69,7 +69,7 @@ impl DataTable { /// Apply default column headings to the DataTable. /// For example, the column headings will be "Column 1", "Column 2", etc. pub fn apply_default_header(&mut self) { - self.columns = Some(self.default_header(None)); + self.column_headers = Some(self.default_header(None)); } pub fn adjust_for_header(&self, index: usize) -> usize { @@ -83,7 +83,7 @@ impl DataTable { /// Prepares the columns to be sent to the client. If no columns are set, it /// will create default columns. pub fn send_columns(&self) -> Vec { - let columns = match self.columns.as_ref() { + let columns = match self.column_headers.as_ref() { Some(columns) => columns, None => { let width = self.value.size().w.get(); @@ -101,7 +101,7 @@ impl DataTable { pub fn normalize_column_header_names(&mut self) { let mut all_names = vec![]; - if let Some(columns) = self.columns.as_mut() { + if let Some(columns) = self.column_headers.as_mut() { columns.iter_mut().for_each(|column| { let name = unique_name(&column.name.to_string(), &all_names, false); column.name = CellValue::Text(name.to_owned()); @@ -138,7 +138,7 @@ pub mod test { DataTableColumnHeader::new("Column 3".into(), true, 2), DataTableColumnHeader::new("Column 4".into(), true, 3), ]; - assert_eq!(data_table.columns, Some(expected_columns)); + assert_eq!(data_table.column_headers, Some(expected_columns)); // test column headings taken from first row let value = Value::Array(values.clone()); @@ -153,7 +153,7 @@ pub mod test { DataTableColumnHeader::new("country".into(), true, 2), DataTableColumnHeader::new("population".into(), true, 3), ]; - assert_eq!(data_table.columns, Some(expected_columns)); + assert_eq!(data_table.column_headers, Some(expected_columns)); let expected_values = values.clone(); assert_eq!( @@ -177,9 +177,9 @@ pub mod test { let assert_cols = |data_table: &mut DataTable, columns: Vec<&str>, expected_columns: Vec<&str>| { - data_table.columns = Some(to_cols(columns)); + data_table.column_headers = Some(to_cols(columns)); data_table.normalize_column_header_names(); - let data_table_cols = data_table.columns.clone().unwrap(); + let data_table_cols = data_table.column_headers.clone().unwrap(); expected_columns.iter().enumerate().for_each(|(i, c)| { assert_eq!(data_table_cols[i].name.to_string(), c.to_string()); }); @@ -203,7 +203,7 @@ pub mod test { let t = DataTable { kind: DataTableKind::Import(Import::new("test.csv".to_string())), name: "Table 1".into(), - columns: None, + column_headers: None, sort: None, display_buffer: None, value: Value::Array(array), diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs index 07885940f9..2fbb54f0c0 100644 --- a/quadratic-core/src/grid/data_table/display_value.rs +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -83,7 +83,7 @@ impl DataTable { return Ok(&CellValue::Blank); } - let columns = self.columns.iter().flatten().collect::>(); + let columns = self.column_headers.iter().flatten().collect::>(); // increase the x position if the column before it is not displayed for i in 0..columns.len() { @@ -114,7 +114,7 @@ impl DataTable { } if pos.y == 0 && self.show_header { - if let Some(columns) = &self.columns { + if let Some(columns) = &self.column_headers { let display_columns = columns.iter().filter(|c| c.display).collect::>(); if let Some(column) = display_columns.get(pos.x as usize) { @@ -135,7 +135,7 @@ impl DataTable { /// Get the indices of the columns to show. pub fn columns_to_show(&self) -> Vec { - self.columns + self.column_headers .iter() .flatten() .enumerate() @@ -223,25 +223,25 @@ pub mod test { let (_, mut data_table) = new_data_table(); data_table.apply_first_row_as_header(); let width = data_table.output_size().w.get(); - let mut columns = data_table.columns.clone().unwrap(); + let mut columns = data_table.column_headers.clone().unwrap(); pretty_print_data_table(&data_table, None, None); // validate display_value() columns[0].display = false; - data_table.columns = Some(columns.clone()); + data_table.column_headers = Some(columns.clone()); let mut values = test_csv_values(); values[0].remove(0); assert_data_table_row(&data_table, 0, values[0].clone()); // reset values columns[0].display = true; - data_table.columns = Some(columns.clone()); + data_table.column_headers = Some(columns.clone()); let remove_column = |remove_at: usize, row: u32, data_table: &mut DataTable| { let mut column = columns.clone(); column[remove_at].display = false; - data_table.columns = Some(column); + data_table.column_headers = Some(column); let title = Some(format!("Remove column {}", remove_at)); pretty_print_data_table(&data_table, title.as_deref(), None); diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 4a62be978c..14b248605c 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -5,6 +5,7 @@ //! performed yet). pub mod column; +pub mod column_header; pub mod display_value; pub mod row; pub mod sort; @@ -19,7 +20,7 @@ use crate::{ }; use anyhow::{anyhow, Ok, Result}; use chrono::{DateTime, Utc}; -use column::DataTableColumnHeader; +use column_header::DataTableColumnHeader; use itertools::Itertools; use serde::{Deserialize, Serialize}; use sort::DataTableSort; @@ -84,7 +85,7 @@ pub struct DataTable { pub name: String, pub header_is_first_row: bool, pub show_header: bool, - pub columns: Option>, + pub column_headers: Option>, pub sort: Option>, pub display_buffer: Option>, pub value: Value, @@ -138,7 +139,7 @@ impl DataTable { name: name.into(), header_is_first_row, show_header, - columns: None, + column_headers: None, sort: None, display_buffer: None, value, diff --git a/quadratic-core/src/grid/file/serialize/data_table.rs b/quadratic-core/src/grid/file/serialize/data_table.rs index 6f7c958f02..ea334fe33b 100644 --- a/quadratic-core/src/grid/file/serialize/data_table.rs +++ b/quadratic-core/src/grid/file/serialize/data_table.rs @@ -9,7 +9,7 @@ use crate::{ grid::{ block::SameValue, data_table::{ - column::DataTableColumnHeader, + column_header::DataTableColumnHeader, sort::{DataTableSort, SortDirection}, }, formats::format::Format, @@ -213,7 +213,7 @@ pub(crate) fn import_data_table_builder( last_modified: data_table.last_modified.unwrap_or(Utc::now()), // this is required but fall back to now if failed spill_error: data_table.spill_error, value, - columns: data_table.columns.map(|columns| { + column_headers: data_table.columns.map(|columns| { columns .into_iter() .enumerate() @@ -429,7 +429,7 @@ pub(crate) fn export_data_tables( } }; - let columns = data_table.columns.map(|columns| { + let columns = data_table.column_headers.map(|columns| { columns .into_iter() .map(|column| current::DataTableColumnSchema { diff --git a/quadratic-core/src/grid/js_types.rs b/quadratic-core/src/grid/js_types.rs index 93f1ccb726..1f53bf3e0e 100644 --- a/quadratic-core/src/grid/js_types.rs +++ b/quadratic-core/src/grid/js_types.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use uuid::Uuid; -use super::data_table::{column::DataTableColumnHeader, sort::DataTableSort}; +use super::data_table::{column_header::DataTableColumnHeader, sort::DataTableSort}; use super::formats::format::Format; use super::formatting::{CellAlign, CellVerticalAlign, CellWrap}; use super::sheet::validations::validation::ValidationStyle; From 0f569917809e948ea5a4be334e4fbd9f0a6429ea Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Wed, 13 Nov 2024 16:33:50 -0700 Subject: [PATCH 246/373] tweak labels --- quadratic-client/src/app/actions/dataTableSpec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 4018fac74f..82a87a3276 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -91,7 +91,7 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.ToggleFirstRowAsHeaderTable]: { - label: 'First row as column headers', + label: 'Use 1st row as column headers', checkbox: isFirstRowHeader, run: () => { const table = getTable(); @@ -173,7 +173,7 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.ToggleTableAlternatingColors]: { - label: 'Toggle alternating colors', + label: 'Show alternating colors', checkbox: isAlternatingColorsShowing, run: () => { const table = getTable(); From b353f96bd6ece8f3dede218ef47667e8ef537ca6 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 14 Nov 2024 05:42:53 -0800 Subject: [PATCH 247/373] fix z-index of cursor and floating column headers --- quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index c2fa04c52d..81d1d51f43 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -159,7 +159,6 @@ export class PixiApp { this.cellsSheets = this.viewportContents.addChild(this.cellsSheets); this.gridLines = this.viewportContents.addChild(this.gridLines); - this.viewportContents.addChild(this.overHeadingsColumnsHeaders); // this is a hack to ensure that table column names appears over the column // headings, but under the row headings @@ -168,7 +167,6 @@ export class PixiApp { this.axesLines = this.viewportContents.addChild(new AxesLines()); this.boxCells = this.viewportContents.addChild(new BoxCells()); - this.cellImages = this.viewportContents.addChild(this.cellImages); this.multiplayerCursor = this.viewportContents.addChild(new UIMultiPlayerCursor()); this.cursor = this.viewportContents.addChild(new Cursor()); this.htmlPlaceholders = this.viewportContents.addChild(new HtmlPlaceholders()); @@ -177,6 +175,8 @@ export class PixiApp { this.cellMoving = this.viewportContents.addChild(new UICellMoving()); this.validations = this.viewportContents.addChild(this.validations); this.headings = this.viewportContents.addChild(gridHeadings); + this.viewportContents.addChild(this.overHeadingsColumnsHeaders); + this.cellImages = this.viewportContents.addChild(this.cellImages); this.viewportContents.addChild(this.overHeadingsTableNames); this.reset(); From 4528b4666c02dde9ba5e9fde31ee9d8fc31f861f Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 14 Nov 2024 05:53:26 -0800 Subject: [PATCH 248/373] fix spill error for data tables --- .../src/app/gridGL/cells/tables/TableColumnHeaders.ts | 2 +- quadratic-core/src/controller/execution/spills.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 2b04fd19df..7487504c70 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -122,7 +122,7 @@ export class TableColumnHeaders extends Container { // update appearance when there is an updated code cell update() { - if (this.table.codeCell.show_header) { + if (this.table.codeCell.show_header && !this.table.codeCell.spill_error) { this.visible = true; this.headerHeight = this.table.sheet.offsets.getRowHeight(this.table.codeCell.y); this.drawBackground(); diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index f9f61b9061..01a2b40ccb 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -32,6 +32,10 @@ impl GridController { index, }); code_pos = Some(*pos); + + // need to update the cells that are affected by the spill error + let sheet_rect = run.output_sheet_rect(sheet_pos, true); + transaction.add_dirty_hashes_from_sheet_rect(sheet_rect); } if let Some(code_pos) = code_pos { if let Some(data_table) = sheet.data_tables.get(&code_pos) { From 4602b498543e8f261f985a3ab772bba2a57d4259 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 14 Nov 2024 06:39:30 -0800 Subject: [PATCH 249/373] style stuff --- .../src/app/gridGL/cells/tables/TableColumnHeader.ts | 1 - quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 9f34f4f988..5705eb9ae0 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ //! Holds a column header within a table. import { Table } from '@/app/gridGL/cells/tables/Table'; diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts index e16cee974c..fc6b9dbb1b 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableOutline.ts @@ -33,8 +33,6 @@ export class TableOutline extends Graphics { // draw the table selected outline const width = this.active ? 2 : 1; - - const image = this.table.codeCell.state === 'Image'; const chart = this.table.codeCell.state === 'HTML'; if (!chart) { this.lineStyle({ color: getCSSVariableTint('primary'), width, alignment: 0 }); @@ -52,6 +50,7 @@ export class TableOutline extends Graphics { // draw outline around where the code cell would spill this.lineStyle({ color: getCSSVariableTint('primary'), width: 1, alignment: 0 }); + const image = this.table.codeCell.state === 'Image'; this.drawRect( 0, 0, From 4f0154880b8c80de688a8dceea9b834084021c49 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Thu, 14 Nov 2024 06:43:29 -0800 Subject: [PATCH 250/373] remove console.log --- quadratic-client/src/app/gridGL/pixiApp/thumbnail.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/pixiApp/thumbnail.ts b/quadratic-client/src/app/gridGL/pixiApp/thumbnail.ts index f9590fcf3f..9a564c8455 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/thumbnail.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/thumbnail.ts @@ -22,7 +22,6 @@ class Thumbnail { } setThumbnailDirty = () => { - console.log('setThumbnailDirty'); this.thumbnailDirty = true; }; From 074b8e2ed640c27d5e02f2bb926b89862dc7f3de Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 14 Nov 2024 15:15:41 -0700 Subject: [PATCH 251/373] make grid dropdown icons same as material ones --- .../public/images/dropdown-white.png | Bin 1236 -> 822 bytes quadratic-client/public/images/dropdown.png | Bin 2161 -> 732 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/quadratic-client/public/images/dropdown-white.png b/quadratic-client/public/images/dropdown-white.png index b3e660a0ebefd3452e20f7dbc9ef0d3193545173..bfe9641d4cba777db07bc48ccafa2df035e20d52 100644 GIT binary patch delta 783 zcmV+q1MvLR3AP51Ba^WK6@LL~X+uL$L}_zyY+-pIP%{7kc${^Ry-UMT6va=gQqVex z4jmjai9^)V3U+a5D-=aAR0XS3ntlmwd?YDSaT8Z5_#aroS#Yq3RB&-{5JU$N-JCip zxM+CZmsHv!-pA$rIGl62T)-Pp87uYzDCw4wOvI$M)Vkz*gNL3|s()s(sG=+i{{6e_ z^U?L*Pl#DfyLJ%SPh6MIE|+$m0#kqeUDcn-ni~Dz)Ip6I7T}SIm2Ha&-X$I}Xer{V z;JnMng3~UaJD!zfocNYl(h6#ZxJfLhJM?@9mx^VrwS(B+pVe2F#T@EU%wZEI7>ZC) zfdmENfBe&qKaMSOSASqoE{_5%%0x*K6SBnf(4k=xGI0n&>X5i|>wXWd`^jx>?-iJB zaLzB<+Cl?3ouQM}+uC>p7#aX>>P$6MN9v*W=5oOM2{4!fhVLk|VdlaicYA*f=zHP( zV+`;e0(O=2uOrUw8ik*MEByx-DQdhiDNjBC00DDJL_t(|0e|h$M|Q(75CqZ2$=y2n z*rVs9z??v^{sI}Rk^lez00000000000001JScwRK%^yMyZY4*eRN+=iarxO1^c8lvzf{NRxsjqD(R=Mw%QX5oMIgG19Cci735h#YnS* zB%-`EJ4RX*BoXDoq8Mp$kVKTb#W7ZuQR2F+8pSoEST&1lMX}Bxt`WsLlW04Nbw<%< z6zj~QtteItqKzn4OXA#7tQN&NqgXAAb49VPAkGoRx_^?WJBoEhQD+qE%A&3)7OIJ2 z;ZGbJ#lowDqFCs*a}*21c8X$Qyp5w+m~Nve7Uo+yiiPD?ieh0s3!_+A-^?f$wlgt` zh3$=uVqrf6Q7r7QBZ`INJVmi^yzeL$000000000000000005}lC*{McKtv;1+W-In N07*qoLO!ljWUyllBO}rU^DaPmzo)`_h!;$t85h|HhwMm3)m`arHGWQt%(hK z&YPK)3hL;q_D}PNmX=i|*h-zq)fxs*^ z?el>;E8Y!SwQ8UH*P3H-Lxs9V;R#jKbHdZYv+Fs{;$7iRH5G{op9>G@azWvHOOKZL z)#7A{zb)27K_A$h!b-A=qgAY?^o+2)99JftSIhUYUeXfJF3d~p74OC%h#xNsGfd%Q zlwrnY9%T>@Lw~ZOM!vh4KgXKnsxoU7&{-u(ik$RTAurYA5)-1wWuz%s{r}rnBQfGB zRd4Y^!;5LY_km*vezr~X>)14ZPQd;QSJ_Jb+7XO>h`wFXT!+xM2^ZHDExHGnThR5S zMf6BfvWwdi33xk$)-ZJ3Lep}oXXSlPA0f0MUv&ff2M^FM=i63(0fb;@^KIlphLIr` ze**YPL_t(|obA=UjowBOfZ>O3KyE>iA;3)tQaf%%fr?NaD#C#R1*+fHft(+qQjuG*ojT-|$zX zrS9IhZQI^G8u+8)kkV+Gtm0kPT*#LOe~V=Q_v*W>`DDGp5B%9+p*#1Cx7)V;_h{%< zAuk7fYPHpk`}EpnP$7g=(x56G)cr$J$@!{uP$`5|a-wP-R0<)LoTyp{JA{x*4zx=L zJA{x*4zx=LJB5%+cC=duJB5%+cC=du2ZWGHDmtWt142k86&=#SK_R4)2OZYIe?cLn zk_R2u!3iOxlHYVn2PcG(N`BKR9h?+GD*2_;IyfnWRPsxwbXqFU3Jh=2$xjSRR^7ga499-brO382?Oc25nN~Y*wf)JKaGDQcIgb-TEG#yM5LTDw^bTCl}A(c$kf5Ai{gj6zB z2a|;mO38E`Ocp{YCDU~fKnNj}gwR0%A%sv8LI**FFjYwy9Rv}=R3%|_5J(79l!Vej zAR$aq5=sZbgfLV|I2{BN!cZmQbP!MoLzINnK|mo4Q4&%IL50v&Nmv~O6+%}fVRaB# z2wjwf)BVe^g0$9RwFbQzhYbEK@1vO)2HM9PjBxTKK{Xp^1{z(Yg~# zmeIi?LO7))b@b|hlBIO8kPr?jNgci0p=3E7EGC3qN>azDDwHg#g9U|9r6hHX?tzkJ zb+D)q9x6#4vwBhTbQK+!sFd=tl=A)M`7nE*EX%Si%d#xXveo(t6!9gQRXeMW<^TWy M07*qoM6N<$g2tB-nE(I) diff --git a/quadratic-client/public/images/dropdown.png b/quadratic-client/public/images/dropdown.png index 7ee27c2484a82b4e7d4d11018f251605c5a8d29c..d2da3348f54f4d63e455db84f17e0741f2a52cbc 100644 GIT binary patch literal 732 zcmeAS@N?(olHy`uVBq!ia0vp^DImd3&=FElSRmlSE_ip2mFA9&ihI*TZYkY6{`9ZKoZ_7^ z2c!cewZAcGyqq@C=g5++S9U$AZf>6!yee(eibXnH|NicN{rTe8d;T4fzPrl=Zu-Ye zKc_dnY#vi^{omND^H0u_`g{M1W3QyxgNdC}bGte3smd8xjeGO2hEG2#^D6L6qOh7oS3qDR3+u!BKbQOL z`*e%K69V-5nRHIMcnS({I`a9#!bBmXLJr4Q6Lu88uH#?(`($qJd(j!W1?Ti{-Ev~K zn7eTD)!Vl!SR|wv@?QCvhPc04`0ZKIf!~}4Gnm@mYiv$@c1W?jx}N7n^}|2OJRfA4 z%BFr^uYB&URO|dk?Z52enpt}cHT~^?fiCIk;uuoF`1ayPUSzopr0GL)i ARsaA1 literal 2161 zcmb7_ZB$a(8pr8Huc4Njkohu2DVYbeeAg)(NmJB}EP?1WC6h%`23U?SIgKSMFr`IO zqN$)}6GD=B1yr*7I&rm2GS)VL@XhrjWu-yW$NN5Jw0)>~Od9fv zIrWkHeoaH7A;y05!Cui|mGMF8k5`|nZ)Jyl$o?bbdG?mz>a`w>zPa^fe&%S;mxn3v zR0~fvA-P2wrt!pkp7!MbcyisU;I|n;M+ffa{CA#DgU^T&Up5GD^MWrpk+dMZj#7tK3- z>l?Illj6{W)t9U=OEXH3Ut9lOzLQ|67twCh#dH4iVZwq<=HtX;2G6R-7@j|NULq%$ zYa$Y7YJRY>X#I3yF6GX~_SdH05&jP-hC@E8^q1Kq_rr?3C+C)%Dn`A?-M#A`O|(vo zB+86-+sl}&!~J8~OtppEUTR!XNlhrL?yhEXtrCYH@%Hz=kxd#h&e%&_rV@i1E+BH) zFB+$oMLC`rXrt1;0XphKaf%WZg5BZ#&D;+(cv9S z?RPV+Hwf%+`rW))P1_KkYgeATC*{&%1CC|Ve4on||AgklS;zkT8^N=`rLuZe z75jG5W`RF0`C5K$+_Q%31ZSZyp*;b6lG5dF8(UiQzZL^oSw%%fBk$6isF%Z9@1Jg> zo^BrL?Ce~-v2=TVK|w)vb+w*Mnhy;P&C1Bwy3UZ^>Xx&=zjbnwF`aa)qT|zTM*~3k`Xst8Z8V7buQY3sHB!Bx!|gru$8b zzQI$O(3~f&ZFit@4{7?JCGDeooW_uC^F5Rae=n9=wo7y|18n)QO)TAm5kfwlA1N-m zgM}6SKu{!Nc90e~?1SWkObQ+fm#Jv&K*8c3hJ5NvoEX)T4=dgQ3ynbt1+Ek7kkmu+ z!SjG&38M$)4Lmgc zl=mFe@wzG%Ru~)d4DVZ59TlJsXX;-0U?v66IJ!ekGlDv7@A{G7?!<`$YIhkA6ucAGg9aEhADNM4ph+!;c)DCAX$IikT`uIa!2o>Y8CX9G zz_D+Np;=dm;cM%I2Sxl=L)KiOmlZ|efK z2<<2@7u04~09iVtpH!m?p?^4&SjwfW4oW*MNl9aMFx$QOB%C~Y!F8Uyhtc-+ng~YY zQ2oH9zV4=B)wSy0t+LkERz9DvubUW7ZMF`oKS~pSLA6)oBNRj%Ss{#(Q10ZBh;q9F zxa;ANC)vsiol=Fm0claz7CPP&)EJNRmd<&D6nEfLl=w7`j)w7}>ecwOieg9E1dM5+ z{8C6-Dlh&DSF;ORLXKXKW{XQML9gtrxw@`LD0U$SN3R`4p48%z%4bZDKMfN`Emh&M z3OiR>9E=fA3V$H^%8TQ0d6Cdsax@lAlav%euU19K1<`w&>pR$qEvVb#Y-C*5*A;iyw4-q%ZU*a(|Ask_^b zG8J<4^pY`TtyV)+#;`aEv?Nz*zX~6q*lHz9fyE+0Ief0#2@*ww`VZY-#lkspw6-%|)UlK^4vf75odY>r zK*^pB1DcKQhJ}Y*p=uV#ZTjhtDi8TecSYE{Sf#c<*WFO(wmTMfi>JAn5MV3QVUjp$ zo;8_F Date: Thu, 14 Nov 2024 15:32:38 -0700 Subject: [PATCH 252/373] make dropdown icons consistent --- quadratic-client/src/app/actions/insertActionsSpec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/actions/insertActionsSpec.ts b/quadratic-client/src/app/actions/insertActionsSpec.ts index 450260f9e0..979b79be4b 100644 --- a/quadratic-client/src/app/actions/insertActionsSpec.ts +++ b/quadratic-client/src/app/actions/insertActionsSpec.ts @@ -6,11 +6,11 @@ import { insertCellRef } from '@/app/ui/menus/CodeEditor/insertCellRef'; import { SNIPPET_JS_API, SNIPPET_JS_CHART } from '@/app/ui/menus/CodeEditor/snippetsJS'; import { SNIPPET_PY_API, SNIPPET_PY_CHART } from '@/app/ui/menus/CodeEditor/snippetsPY'; import { - ArrowDropDownCircleIcon, + ArrowDropDownIcon, CheckBoxIcon, DataValidationsIcon, - SheetIcon, FormatDateTimeIcon, + SheetIcon, } from '@/shared/components/Icons'; import { quadraticCore } from '../web-workers/quadraticCore/quadraticCore'; @@ -204,7 +204,7 @@ export const insertActionsSpec: InsertActionSpec = { [Action.InsertDropdown]: { label: 'Dropdown', labelVerbose: 'Insert dropdown', - Icon: ArrowDropDownCircleIcon, + Icon: ArrowDropDownIcon, run: () => { if (!pixiAppSettings.setEditorInteractionState) return; pixiAppSettings.setEditorInteractionState((prev) => ({ From 461418080ed2a6d2ebe912f7fd6f568616e4fb10 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 14 Nov 2024 16:13:38 -0700 Subject: [PATCH 253/373] Handle insert/remove row and column in Rust --- Cargo.lock | 11 + .../src/app/quadratic-core-types/index.d.ts | 4 +- quadratic-core/Cargo.toml | 1 + quadratic-core/src/compression.rs | 27 ++- quadratic-core/src/grid/data_table/column.rs | 132 +++++++++++ .../src/grid/data_table/column_header.rs | 24 ++ quadratic-core/src/grid/data_table/mod.rs | 23 +- quadratic-core/src/grid/data_table/row.rs | 88 ++++++++ quadratic-core/src/values/array.rs | 211 ++++++++++++++++++ 9 files changed, 513 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 247417e364..6bf3521f41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3350,6 +3350,16 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memory-stats" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c73f5c649995a115e1a0220b35e4df0a1294500477f97a91d0660fb5abeb574a" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "mime" version = "0.3.17" @@ -4162,6 +4172,7 @@ dependencies = [ "js-sys", "lazy_static", "lexicon_fractional_index", + "memory-stats", "parquet 51.0.0", "pollster", "proptest", diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index fc23843467..1ca306d40b 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -29,7 +29,7 @@ export interface JsCellValue { value: string, kind: string, } export interface JsClipboard { plainText: string, html: string, } export interface JsCodeCell { x: bigint, y: bigint, code_string: string, language: CodeCellLanguage, std_out: string | null, std_err: string | null, evaluation_result: string | null, spill_error: Array | null, return_info: JsReturnInfo | null, cells_accessed: Array | null, } export interface JsCodeResult { transaction_id: string, success: boolean, std_out: string | null, std_err: string | null, line_number: number | null, output_value: Array | null, output_array: Array>> | null, output_display_type: string | null, cancel_compute: boolean | null, chart_pixel_output: [number, number] | null, } -export interface JsDataTableColumn { name: string, display: boolean, valueIndex: number, } +export interface JsDataTableColumnHeader { name: string, display: boolean, valueIndex: number, } export interface JsGetCellResponse { x: bigint, y: bigint, value: string, type_name: string, } export interface JsHtmlOutput { sheet_id: string, x: bigint, y: bigint, html: string | null, w: number | null, h: number | null, } export interface JsNumber { decimals: number | null, commas: boolean | null, format: NumericFormat | null, } @@ -37,7 +37,7 @@ export interface JsOffset { column: number | null, row: number | null, size: num export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; -export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } +export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success" | "HTML" | "Image"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } export interface JsRowHeight { row: bigint, height: number, } diff --git a/quadratic-core/Cargo.toml b/quadratic-core/Cargo.toml index 1598174ec5..005be7e24b 100644 --- a/quadratic-core/Cargo.toml +++ b/quadratic-core/Cargo.toml @@ -91,6 +91,7 @@ dateparser = "0.2.1" criterion = { version = "0.4", default-features = false } tokio-test = "0.4.3" serial_test = "3.0.0" +memory-stats = "1.2.0" [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] proptest = "1.2.0" diff --git a/quadratic-core/src/compression.rs b/quadratic-core/src/compression.rs index 1534ce2365..64e5452018 100644 --- a/quadratic-core/src/compression.rs +++ b/quadratic-core/src/compression.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Result}; +use bincode::Options; use flate2::{ write::{ZlibDecoder, ZlibEncoder}, Compression, @@ -7,6 +8,7 @@ use serde::de::DeserializeOwned; use std::io::prelude::*; const HEADER_DELIMITER: u8 = "*".as_bytes()[0]; +const BUFFER_SIZE: usize = 8192; // 8KB chunks pub enum CompressionFormat { None, @@ -59,11 +61,23 @@ where T: DeserializeOwned, { match serialization_format { - SerializationFormat::Bincode => Ok(bincode::deserialize(data)?), + SerializationFormat::Bincode => Ok(deserialize_bincode(data)?), SerializationFormat::Json => Ok(serde_json::from_slice(data)?), } } +pub fn deserialize_bincode(data: &[u8]) -> Result +where + T: DeserializeOwned, +{ + let config = bincode::DefaultOptions::new() + .with_fixint_encoding() + .with_limit(1024 * 1024) + .allow_trailing_bytes(); + + Ok(config.deserialize(data)?) +} + // COMPRESSION pub fn compress(compression_format: &CompressionFormat, data: Vec) -> Result> { @@ -75,7 +89,11 @@ pub fn compress(compression_format: &CompressionFormat, data: Vec) -> Result pub fn compress_zlib(data: Vec) -> Result> { let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); - encoder.write_all(data.as_slice())?; + + for chunk in data.chunks(BUFFER_SIZE) { + encoder.write_all(chunk)?; + } + Ok(encoder.finish()?) } @@ -89,7 +107,10 @@ pub fn decompress(compression_format: &CompressionFormat, data: &[u8]) -> Result pub fn decompress_zlib(data: &[u8]) -> Result> { let writer = Vec::new(); let mut decoder = ZlibDecoder::new(writer); - decoder.write_all(data)?; + + for chunk in data.chunks(BUFFER_SIZE) { + decoder.write_all(chunk)?; + } Ok(decoder.finish()?) } diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index 8b13789179..5c76a828e5 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -1 +1,133 @@ +use anyhow::{bail, Result}; +use super::{column_header::DataTableColumnHeader, DataTable}; +use crate::Value; + +impl DataTable { + /// Insert a new column at the given index. + pub fn insert_column(mut self, column_index: usize) -> Result { + let column_name = self.unique_column_header_name(None).to_string(); + + if let Value::Array(array) = self.value { + let new_array = array.insert_column(column_index, None)?; + self.value = Value::Array(new_array); + } else { + bail!("Expected an array"); + } + + self.display_buffer = None; + + if let Some(mut headers) = self.column_headers { + let new_header = DataTableColumnHeader::new(column_name, true, column_index as u32); + headers.push(new_header); + self.column_headers = Some(headers); + } + + Ok(self) + } + + /// Remove a column at the given index. + pub fn remove_column(mut self, column_index: usize) -> Result { + if let Value::Array(array) = self.value { + let new_array = array.remove_column(column_index)?; + self.value = Value::Array(new_array); + } else { + bail!("Expected an array"); + } + + self.display_buffer = None; + + if let Some(mut headers) = self.column_headers { + headers.remove(column_index); + self.column_headers = Some(headers); + } + + Ok(self) + } +} + +#[cfg(test)] +pub mod test { + use crate::{ + grid::test::{new_data_table, pretty_print_data_table}, + ArraySize, CellValue, + }; + use serial_test::parallel; + + #[test] + #[parallel] + fn test_data_table_insert_column() { + let (_, mut data_table) = new_data_table(); + data_table.apply_first_row_as_header(); + + pretty_print_data_table(&data_table, Some("Original Data Table"), None); + + data_table = data_table.insert_column(4).unwrap(); + pretty_print_data_table(&data_table, Some("Data Table with New Column"), None); + + // there should be a "Column" header + let header = data_table.get_header_by_name("Column"); + assert!(header.is_some()); + + // this should be a 5x4 array + let expected_size = ArraySize::new(5, 4).unwrap(); + assert_eq!(data_table.output_size(), expected_size); + + // there is no data at position (0, 4) + assert!(data_table.cell_value_at(0, 4).is_none()); + } + + #[test] + #[parallel] + fn test_data_table_remove_column() { + let (_, mut source_data_table) = new_data_table(); + source_data_table.apply_first_row_as_header(); + + pretty_print_data_table(&source_data_table, Some("Original Data Table"), None); + + let data_table = source_data_table.clone().remove_column(3).unwrap(); + pretty_print_data_table(&data_table, Some("Data Table without Population"), None); + + // there should be no "population" header + let header = data_table.get_header_by_name("population"); + assert!(header.is_none()); + + // this should be a 5x4 array + let expected_size = ArraySize::new(3, 4).unwrap(); + assert_eq!(data_table.output_size(), expected_size); + + let data_table = source_data_table.clone().remove_column(0).unwrap(); + pretty_print_data_table(&data_table, Some("Data Table without City"), None); + + // there should be no "city" header + let header = data_table.get_header_by_name("city"); + assert!(header.is_none()); + + // this should be a 5x4 array + let expected_size = ArraySize::new(3, 4).unwrap(); + assert_eq!(data_table.output_size(), expected_size); + + // there is no data at position (0, 0) + assert_eq!( + data_table.cell_value_at(0, 0).unwrap(), + CellValue::Text("region".into()) + ); + + let data_table = source_data_table.clone().remove_column(1).unwrap(); + pretty_print_data_table(&data_table, Some("Data Table without Region"), None); + + // there should be no "region" header + let header = data_table.get_header_by_name("region"); + assert!(header.is_none()); + + // this should be a 5x4 array + let expected_size = ArraySize::new(3, 4).unwrap(); + assert_eq!(data_table.output_size(), expected_size); + + // there is no data at position (0, 0) + assert_eq!( + data_table.cell_value_at(0, 0).unwrap(), + CellValue::Text("city".into()) + ); + } +} diff --git a/quadratic-core/src/grid/data_table/column_header.rs b/quadratic-core/src/grid/data_table/column_header.rs index 0924008907..01f25c5ffc 100644 --- a/quadratic-core/src/grid/data_table/column_header.rs +++ b/quadratic-core/src/grid/data_table/column_header.rs @@ -97,6 +97,21 @@ impl DataTable { .collect() } + pub fn unique_column_header_name(&self, name: Option<&str>) -> String { + let name = name.unwrap_or("Column"); + + if let Some(columns) = self.column_headers.as_ref() { + let all_names = columns + .iter() + .map(|c| c.name.to_string()) + .collect::>(); + + unique_name(name, &all_names, false) + } else { + name.to_string() + } + } + /// Set the display of a column header at the given index. pub fn normalize_column_header_names(&mut self) { let mut all_names = vec![]; @@ -109,6 +124,15 @@ impl DataTable { }); } } + + /// Get a column header by name. + pub fn get_header_by_name(&self, name: &str) -> Option<&DataTableColumnHeader> { + self.column_headers + .as_ref() + .unwrap() + .iter() + .find(|h| h.name.to_string() == name) + } } #[cfg(test)] diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 14b248605c..8d3a66e2a0 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -18,7 +18,7 @@ use crate::util::unique_name; use crate::{ Array, ArraySize, CellValue, Pos, Rect, RunError, RunErrorMsg, SheetPos, SheetRect, Value, }; -use anyhow::{anyhow, Ok, Result}; +use anyhow::{anyhow, bail, Ok, Result}; use chrono::{DateTime, Utc}; use column_header::DataTableColumnHeader; use itertools::Itertools; @@ -330,6 +330,13 @@ impl DataTable { } } + pub fn value_as_array<'a>(&'a self) -> Result<&'a Array> { + match &self.value { + Value::Array(array) => Ok(array), + _ => bail!("Expected an array"), + } + } + pub fn pretty_print_data_table( data_table: &DataTable, title: Option<&str>, @@ -347,11 +354,21 @@ impl DataTable { for (index, row) in array.rows().take(max).enumerate() { let row = row.iter().map(|s| s.to_string()).collect::>(); let display_index = vec![display_buffer[index].to_string()]; - let row = [display_index, row].concat(); - if index == 0 && data_table.header_is_first_row { + if index == 0 && data_table.column_headers.is_some() { + let headers = data_table + .column_headers + .as_ref() + .unwrap() + .iter() + .map(|h| h.name.to_string()) + .collect::>(); + builder.set_header([display_index, headers].concat()); + } else if index == 0 && data_table.header_is_first_row { + let row = [display_index, row].concat(); builder.set_header(row); } else { + let row = [display_index, row].concat(); builder.push_record(row); } } diff --git a/quadratic-core/src/grid/data_table/row.rs b/quadratic-core/src/grid/data_table/row.rs index e69de29bb2..ce00e9a655 100644 --- a/quadratic-core/src/grid/data_table/row.rs +++ b/quadratic-core/src/grid/data_table/row.rs @@ -0,0 +1,88 @@ +use anyhow::{bail, Result}; + +use super::DataTable; +use crate::Value; + +impl DataTable { + /// Insert a new row at the given index. + pub fn insert_row(mut self, row_index: usize) -> Result { + if let Value::Array(array) = self.value { + let new_array = array.insert_row(row_index, None)?; + self.value = Value::Array(new_array); + } else { + bail!("Expected an array"); + } + + self.display_buffer = None; + + Ok(self) + } + + /// Remove a row at the given index. + pub fn remove_row(mut self, row_index: usize) -> Result { + if let Value::Array(array) = self.value { + let new_array = array.remove_row(row_index)?; + self.value = Value::Array(new_array); + } else { + bail!("Expected an array"); + } + + self.display_buffer = None; + + Ok(self) + } +} + +#[cfg(test)] +pub mod test { + use crate::{ + grid::test::{new_data_table, pretty_print_data_table}, + ArraySize, CellValue, + }; + use serial_test::parallel; + + #[test] + #[parallel] + fn test_data_table_insert_row() { + let (_, mut data_table) = new_data_table(); + data_table.apply_first_row_as_header(); + + pretty_print_data_table(&data_table, Some("Original Data Table"), None); + + data_table = data_table.insert_row(4).unwrap(); + pretty_print_data_table(&data_table, Some("Data Table with New Row"), None); + + // this should be a 5x4 array + let expected_size = ArraySize::new(4, 5).unwrap(); + assert_eq!(data_table.output_size(), expected_size); + } + + #[test] + #[parallel] + fn test_data_table_remove_row() { + let (_, mut source_data_table) = new_data_table(); + source_data_table.apply_first_row_as_header(); + + pretty_print_data_table(&source_data_table, Some("Original Data Table"), None); + + let data_table = source_data_table.clone().remove_row(3).unwrap(); + pretty_print_data_table(&data_table, Some("Data Table without row 4"), None); + + // this should be a 4x3 array + let expected_size = ArraySize::new(4, 3).unwrap(); + assert_eq!(data_table.output_size(), expected_size); + + let data_table = source_data_table.clone().remove_row(1).unwrap(); + pretty_print_data_table(&data_table, Some("Data Table without row 1"), None); + + // this should be a 4x3 array + let expected_size = ArraySize::new(4, 3).unwrap(); + assert_eq!(data_table.output_size(), expected_size); + + // Southborough should no longer be at (0, 1) + assert_eq!( + data_table.cell_value_at(0, 1), + Some(CellValue::Text("Denver".to_string())) + ); + } +} diff --git a/quadratic-core/src/values/array.rs b/quadratic-core/src/values/array.rs index a70472d927..aad3498cfc 100644 --- a/quadratic-core/src/values/array.rs +++ b/quadratic-core/src/values/array.rs @@ -273,6 +273,142 @@ impl Array { None => bail!("Cannot shift a single row array"), } } + /// Insert a new column at the given index. + pub fn insert_column( + mut self, + insert_at_index: usize, + values: Option>, + ) -> Result { + let width = self.width(); + let new_width = width + 1; + let new_size = ArraySize::new_or_err(new_width, self.height())?; + let mut array = Array::new_empty(new_size); + + let mut col_index: u32 = 0; + let insert_at_end = insert_at_index as u32 == width; + + // reverse the values so that we can efficiently pop them off the end + let mut reversed = + values.and_then(|values| Some(values.into_iter().rev().collect::>())); + + // pop the next value from the insert array + let mut next_insert_value = || { + reversed + .as_mut() + .map_or(CellValue::Blank, |r| r.pop().unwrap_or(CellValue::Blank)) + }; + + for (i, value) in self.values.into_iter().enumerate() { + let col = i % width as usize; + let row = ((i / width as usize) as f32).floor() as usize; + let last = col as u32 == width - 1; + + if col == insert_at_index { + array.set(col_index, row as u32, next_insert_value())?; + col_index += 1; + } + + array.set(col_index, row as u32, value)?; + + if insert_at_end && last { + array.set(width, row as u32, next_insert_value())?; + } + + col_index = last.then(|| 0).unwrap_or(col_index + 1); + } + + self.size = new_size; + self.values = array.values; + + Ok(self) + } + /// Remove a column at the given index. + pub fn remove_column(mut self, remove_at_index: usize) -> Result { + let width = self.width(); + let new_width = width - 1; + let new_size = ArraySize::new_or_err(new_width, self.height())?; + let mut array = Array::new_empty(new_size); + let mut col_index: u32 = 0; + + // loop through the values and skip the remove_at_index column, + // adding the rest to the new array + for (i, value) in self.values.into_iter().enumerate() { + let col = i % width as usize; + let row = ((i / width as usize) as f32).floor() as usize; + let last = col as u32 == width - 1; + + if col == remove_at_index { + if last { + col_index = 0 + } + + continue; + } + + array.set(col_index, row as u32, value)?; + col_index = last.then(|| 0).unwrap_or(col_index + 1); + } + + self.size = new_size; + self.values = array.values; + + Ok(self) + } + /// Insert a new row at the given index. + pub fn insert_row( + mut self, + insert_at_index: usize, + values: Option>, + ) -> Result { + let width = self.width(); + let height = self.height(); + let new_height = height + 1; + let new_size = ArraySize::new_or_err(width, new_height)?; + let mut array = Array::new_empty(new_size); + let values = values.unwrap_or_default(); + + let mut row_index: u32 = 0; + let insert_at_end = insert_at_index as u32 == height; + + for (i, row) in self.rows().enumerate() { + let last = row_index as u32 == height - 1; + + if i == insert_at_index { + array.set_row(row_index as usize, &values)?; + row_index += 1; + } + + array.set_row(row_index as usize, row)?; + + if insert_at_end && last { + array.set_row(height as usize, &values)?; + } + + row_index = last.then(|| 0).unwrap_or(row_index + 1); + } + + self.size = new_size; + self.values = array.values; + + Ok(self) + } + /// Remove a row at the given index. + pub fn remove_row(mut self, remove_at_index: usize) -> Result { + let width = (self.width() as usize).min(self.values.len()); + let start = remove_at_index * width; + let end = start + width; + let height = NonZeroU32::new(self.height() - 1); + + match height { + Some(h) => { + self.values.drain(start..end); + self.size.h = h; + } + None => bail!("Cannot remove a row from a single row array"), + } + + Ok(self) + } /// Returns the only cell value in a 1x1 array, or an error if this is not a /// 1x1 array. @@ -313,6 +449,16 @@ impl Array { self.values[i] = value; Ok(()) } + pub fn set_row(&mut self, index: usize, values: &[CellValue]) -> Result<(), RunErrorMsg> { + let width = self.width() as usize; + let start = index * width; + + for (i, value) in values.iter().enumerate() { + self.values[start + i] = value.to_owned(); + } + + Ok(()) + } /// Returns a flat slice of cell values in the array. pub fn cell_values_slice(&self) -> &[CellValue] { &self.values @@ -494,4 +640,69 @@ mod test { (None, vec![]) ); } + + #[test] + fn test_insert_column() { + let array = array!["a", "b"; "c", "d"]; + let values = vec![CellValue::from("e"), CellValue::from("f")]; + let array = array.insert_column(0, Some(values)).unwrap(); + assert_eq!(array, array!["e", "a", "b"; "f", "c", "d"]); + + let array = array!["a", "b"; "c", "d"]; + let values = vec![CellValue::from("e"), CellValue::from("f")]; + let array = array.insert_column(1, Some(values)).unwrap(); + assert_eq!(array, array!["a", "e", "b"; "c", "f", "d"]); + + let array = array!["a", "b"; "c", "d"]; + let values = vec![CellValue::from("e"), CellValue::from("f")]; + let array = array.insert_column(2, Some(values)).unwrap(); + assert_eq!(array, array!["a", "b", "e"; "c", "d", "f"]); + + let array = array!["a", "b"; "c", "d"]; + let array = array.insert_column(2, None).unwrap(); + assert_eq!( + array, + array!["a", "b", CellValue::Blank; "c", "d", CellValue::Blank] + ); + } + + #[test] + fn test_remove_column() { + let array = array!["a", "b", "c"; "d", "e", "f"; ]; + let array = array.remove_column(0).unwrap(); + assert_eq!(array, array!["b", "c"; "e", "f";]); + + let array = array!["a", "b", "c"; "d", "e", "f"; ]; + let array = array.remove_column(1).unwrap(); + assert_eq!(array, array!["a", "c"; "d", "f";]); + + let array = array!["a", "b", "c"; "d", "e", "f"; ]; + let array = array.remove_column(2).unwrap(); + assert_eq!(array, array!["a", "b"; "d", "e";]); + } + + #[test] + fn test_insert_row() { + let array = array!["a", "b"; "c", "d"]; + let values = vec![CellValue::from("e"), CellValue::from("f")]; + let array = array.insert_row(0, Some(values)).unwrap(); + assert_eq!(array, array!["e", "f"; "a", "b"; "c", "d"]); + + let array = array!["a", "b"; "c", "d"]; + let values = vec![CellValue::from("e"), CellValue::from("f")]; + let array = array.insert_row(1, Some(values)).unwrap(); + assert_eq!(array, array!["a", "b"; "e", "f"; "c", "d"]); + + let array = array!["a", "b"; "c", "d"]; + let values = vec![CellValue::from("e"), CellValue::from("f")]; + let array = array.insert_row(2, Some(values)).unwrap(); + assert_eq!(array, array!["a", "b"; "c", "d"; "e", "f"]); + + let array = array!["a", "b"; "c", "d"]; + let array = array.insert_row(2, None).unwrap(); + assert_eq!( + array, + array!["a", "b"; "c", "d"; CellValue::Blank, CellValue::Blank] + ); + } } From aeabc7aaf2b3f1aa345d7808bd7bf5a037764aa2 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 14 Nov 2024 19:07:51 -0700 Subject: [PATCH 254/373] update icons --- .../public/images/formula-fx-icon.png | Bin 2817 -> 0 bytes .../public/images/icon-javascript.png | Bin 523 -> 867 bytes quadratic-client/public/images/icon-mssql.png | Bin 0 -> 1404 bytes quadratic-client/public/images/icon-mysql.png | Bin 580 -> 1008 bytes .../public/images/icon-postgres.png | Bin 1028 -> 1176 bytes .../public/images/icon-snowflake.png | Bin 0 -> 1078 bytes quadratic-client/public/images/mysql-icon.svg | 4 ---- .../contextMenus/TableColumnContextMenu.tsx | 4 +--- .../src/app/gridGL/cells/CellsMarkers.ts | 11 +++++------ quadratic-client/src/app/gridGL/loadAssets.ts | 5 +++-- 10 files changed, 9 insertions(+), 15 deletions(-) delete mode 100644 quadratic-client/public/images/formula-fx-icon.png create mode 100644 quadratic-client/public/images/icon-mssql.png create mode 100644 quadratic-client/public/images/icon-snowflake.png delete mode 100644 quadratic-client/public/images/mysql-icon.svg diff --git a/quadratic-client/public/images/formula-fx-icon.png b/quadratic-client/public/images/formula-fx-icon.png deleted file mode 100644 index 5d6bab19687256aba9f5de7c5f97ad73c37718a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2817 zcmY*bc|4Ts7k`IPS>|5LEiu>F63T0-VK6fhnXwfvS<5!$T4pe^48;&}uZ&c(47thJ zRW7dV`xafs;CTY?o)Z^q4|lD0~h80+a#Z zgChWFJ_3Ma#PpYD+N^^pyfx0v!~{@a;lltIL=fO)AqeXMASVE}8Vdo25V7yr9CGR# zhXVj&Jb**rIJPX$URai~bG~}c$B=&&A9H+Xt3KxZ7qio%|7px+xg$iZjXwbJ3b73W zq^2Kdso8k^VokC(G1hb;cq=)%5}ff$LEc1m7N8xZ$%5W^k`p}0+snsaGYBR1g`vs9 z>@X+=|3V>oqNJ=%F2gSn{P1vulCqMr6#6I}4%hZ`b<;H0yZ9}gwL?i=Cy|JnAQ%`J zs1&HCMDTM5Rgg#|sH_UAs-9sn&iDuWkeq_f`1nhI75T4@9^T)@&x1(vAo#%9x=zl7 z01`?{ie2dY`r0SS!|mTnKK|dvVhsqgPe2tVW$?Q;D^;5v)x6~AfoD}_>!Ve)zcBwV z_N|UK$S(dr&3v8oOO!P#`lvSe{o2q+MX$c&VSRiGPEY5T>s;L_p1CuYA~CbCTB>sR zr&d=+l=Q;F){)=WH?(&h>gsk;vth6CsoJHJfrAWkN!g|?xg>16&b>QT+W1NDb=0F$ zyDje3n?$WG-XMWRNG5k%at}j#vGMabtj8M*wo3rFI7;V=O*iL`a1#=GRkq|OdwO=S z!Bdu3GXwPPf0d zXDq&Bhm_yd8-SLYj3wvq|Tj1-cqZqO=PKmPikVHh=k?aOj!i>G}u9gTl&+ z>_pmTp(l9WsYB)`To5}~k(yr4A9yMm48xw4buN%;z0u-Hg<1j~pBHkFzui@wO$=?0 z)F>1PIBbwvFg(Itv3FLZ{KE|q2@!&qk5A89M6Hh{o}*)2gA+M_f8edbm=Q@i1dJ*b z3a+9+M!Mq(!apo|Vm%=0Yc zNrHtDrhKN`Q*#?3c}bU^2m3OT1mrJG->5I#p45VPL9Y5GM_4pSI%0emeHmdIJ{)2V zd9o5|B3X+He>QP!+Y&JwO#C{4)}WtK!`$Ed19OV!?JB2?p?XNPcue|GQyukF8_rK^6KV#1*mv6^ zw^n42o@(->puwX)Udgy*A`*<0t9P07eU@-Ph}vL$i58FLOiEJ`>om+V{HX&byJw3G`j`LzMPn)QaR7sH^<-{V(~k%TGtb zOI$lrOfc6@JJ9-PN)?1t?hZv_=o~jILyQFDmhME(7AzMz$Y~Ovbq>81U5l;fU-5Sh zx(B}{j^+0Co%bP}W5VxckaNjqWpyPCyt=iVWG1H%H$il$a1y+@bcCbs1XjQct{dmy z)K|mpEq7u`{cKV1T({`MwUbn6$wxY)W%4*Rxrv?+Ua43rD^5s%lxLgXr(>ERmMCi> zsiPfHoyVOv_h?5Fl?a}K!3@K-5@pqiR?|OC!K`#APNzK-rPavSj)oQ6*cobh2s99Q z?nTHHMfXL&ESG3~$gB&4`Ej&2+h3r$!n4||^4b$oee+H^J38*|o=BbaFj{CZof#{= zv;keZYM`*-LR!M(~98OP)7|y(W^QispBAwvapR=_eSP5d?wn-tHPeP1n zEj&5>mIIGO`R=THQb(HWC3KkM=7Yl;p*-H>>ZW(0eJLc3h;$r5tiXU1r+mb<;n!E< zVWZUj`^nIZU(d`6SEP5l$;=lOK4pkVn?q zQVasOq^ZWe*+W{kCSmK2%paGNry#qzsCH9j!m{C_brgKy0ii^;-<|6y}D#frjKaKPyV$yf;zI^w(B=U9mq#{O>O>@(%ojNZ#wr5=AG`lI* z4uToh@wruk*yTj+Tc>9~#*GISMU7kJ7^&SCy7PI6Iy%+!=wmQB9IAG0`mI;alEI`I z5xHtvEh2OHT4NfMYwaR+f`d;+Xpr(WR1o_Edb*njI*1H?pk2rp zf`Op6MMwJ{;F+!68iDt9O56Xva%XPO_snaVLRBMoNjca(E6i__-unv8S>ggSRhgBL z_j3!3#$7Fiq?z6wu?KLdkX+B>7pfy&gJ-=F|G1nqc;0<>PZf?-J0b%U@UZ0ksIy;f zV{)*q%bgvf@k1vfi8!B)Q$!}(sd2BZQBFmMiue@V6^d#Jl__gr+7IJp)t_vhn*`w# zkSlu7;KLdL>s~1FWv;4v6Gx4i#AFG*fK<^Pg;w)73-kf>K+1U5y_kskrv4O<3hTPJ z{A9D5zbPrkk)sWNA^oe$hgTScGd3}6q4TN16A8CyB>bL$N*#Zz3REdTI=Oa2(mT{E zt>pf(eWes4{I(txBeuvR`FcfYAQGwutJ=1QO)8wu(`qR%8aFTht@tp_ZIU-}KfZ&9 zMxJqVYpstKed2R6Yk_=yoA5`moEN?L27Rsi&V_~WV{fPx?LE%C!UxD7b~ts%wOE(5 zjw+@^SqSx9-jI8E;1%exo~HOdcmL8MwC_|gWq~)QId46pZGk8ScOL|G1jhb2cl)Wi z-#hr{l@sY}tv$Q5=&3-;D1{+TPp3NRN6FM*6&;g6=N}?o`dDF&wE5P^Z;_j%d@<>$u oz&31^Grz{uaCXj^u0;LJA-q}n4AHetXaCsY^iB1ObsZ`H0WYrWi~s-t diff --git a/quadratic-client/public/images/icon-javascript.png b/quadratic-client/public/images/icon-javascript.png index 3575a24a4ec2a3e293e7c2a19c31e0edce6bf9ef..8ec75ec283e23a3d1fe7312cc995a5cc438cd5e6 100644 GIT binary patch delta 394 zcmV;50d@Y11mgyfIDY|YX+uL$L}_zyY+-pIP%{7kc${^Ry-UMT6va=gQqVex4jmja zi9^)V3U+a5D-=aAR0XS3ntlmwd?YDSaT8Z5_#aroS#Yq3RB&-{5JU$N-JCipxM+CZ zmsHv!-pA$rIGl62T)-Pp87uYzDCw4wOvI$M)Vkz*gNL3|s()s(sG=+i{{6e_^U?L* zPl#DfyLJ%SPh6MIE|+$m0#kqeUDcn-ni~Dz)Ip6I7T}SIm2Ha&-X$I}Xer{V;JnMn zg3~UaJD!zfocNYl(h6#ZxJfLhJM?@9mx^VrwS(B+pVe2F#T@EU%wZEI7>ZC)fdmEN zfBe&qKaMSOSASqoE{_5%%0x*K6SBnf(4k=xGI0n&>X5i|>wXWd`^jx>?-iJBaLzB< z+Cl?3ouQM}+uC>p7#aX>>P$6MN9v*W=5oOM2{4!fhVLk|VdlaicYA*f=zHP(V+`;e o0(O=2uOrUw8ik*MEByx-DQdhiDNjBC00E#$L_t(|0kK`K0eYXe*Z=?k delta 39 vcmaFN*3B}(LYjfGILO_JVcj{ImkbOHEa{HEjtmSN`?>!lvTk0@xPuV@>Te7y diff --git a/quadratic-client/public/images/icon-mssql.png b/quadratic-client/public/images/icon-mssql.png new file mode 100644 index 0000000000000000000000000000000000000000..59400abc40357fd24545c4af38fa3419a299acc9 GIT binary patch literal 1404 zcmV-?1%vvDP)at5VQ9hz=bbGKoXf z(h7EQXe$&&FjNJrQ<{DWZG0ptQgIVkDfk~)!C7#yh*WTKa1cZX5#5|RDY$5O-j`I` zBHqX4{WzR+xm>^-P#G)s0x0R0kxay-wbZ)gdxM9bQ>tdNsG=+i{{6e_^U?L*Pl#Df zyLJ%SPh6MIE|+$m0#kqeUDcn-ni~Dz)Ip6I7T}SIm2Ha&-X$I}Xer{V;JnMng3~Ua zJD!zfocNYl(h6#ZxJfLhJM?@9mx^VrwS(B+pVe2F#T@EU%wZEI7>ZC)fdmENfBe&q zKaMSOS71;sj{+>pL`e}7vc&VypY?`La=`luFqi^{?NiPd)$u1Gz~=K~#7Fy;to~+dvRqXZRmeL9T-*F8jE*Xz>tiOCme7bJE?O?rFbI>4uz>FXqdt!%ft~2ERDQym&Ob>HG z_6g8z(8QH;Q2wfHCaWoi`AY)pq^Z4t94PqG7zgTmkfb?)$|U(1fLg@WlA>?}nZC@q z3kneiUz#%iWSlPu5YIt*&4{e{Tu~;cANB@l=)ymD^DBT;o^w4XcrP=eT)~7Iqz~dj z1^;}@v2;0VNV&>s&A^uKhl+n?_=7VwX(P#TFO*L?2M33bd{!RvAo$i40Cb!y|53h> z6c{T>0g!ofw<7vTLGvy7TyZU@){Gb4XWZW4=4M`-Z2*}cl{_#fF#B6`<|?`p!@X~s ziT+i#0!2WeHQy^9WKmR76M<_p%FW|W&7;hOTEhMk^9SW2_mND8Ye_x>a|GopWa*eX zfm?lE*?~H&Df=uZDOY9Oi+T9kqkFE81%!<8!?EwSl#D{TsEvc&CbId7* z$nn+u3uRBGPjZ@xP6L)_EI|g-R&0vvjPr%`MKQG)Ry^-Gm-$0t%CMDVUtC)QI(Y*O zieWA(zXWm3y`+}a$np?mHp+Ld5ew1>#imWLKOM!UEG(UAwtSD&MozhkW7*cJJs7J$ zVQ0Q_QC7JeG%<5J+04lhqZHUzT@*0plJN*$wAV>$+WwA{#CDZdYg@IV)$nt=E#<7k0mO4EenTLA_&no z-iaKP1nUBAKBLKky-SL^R`k+-P!p(6c8~wbC2KWmfaV&2*0E_bOeS#8nXb3G`@d+2 zni%t=;Bzhk7;@4j!Cbh0#; zqcocBpO_eLVsrP3E*E7xvYnjS=-?KXK4v~+_xK>M`=qn3?de~aE{M(oEBS%|0000< KMNUMnLSTZY8H&LG literal 0 HcmV?d00001 diff --git a/quadratic-client/public/images/icon-mysql.png b/quadratic-client/public/images/icon-mysql.png index 1e3c2cdecd20a3551a4c7811e6d5ed8b59805663..e0eaafc3d426809f6efcd05449b02cb64b6f59c2 100644 GIT binary patch delta 966 zcmV;%13CP}1n>utIDY|YX+uL$L}_zyY+-pIP%{7kc${^Ry-UMT6va=gQqVex4jmja zi9^)V3U+a5D-=aAR0XS3ntlmwd?YDSaT8Z5_#aroS#Yq3RB&-{5JU$N-JCipxM+CZ zmsHv!-pA$rIGl62T)-Pp87uYzDCw4wOvI$M)Vkz*gNL3|s()s(sG=+i{{6e_^U?L* zPl#DfyLJ%SPh6MIE|+$m0#kqeUDcn-ni~Dz)Ip6I7T}SIm2Ha&-X$I}Xer{V;JnMn zg3~UaJD!zfocNYl(h6#ZxJfLhJM?@9mx^VrwS(B+pVe2F#T@EU%wZEI7>ZC)fdmEN zfBe&qKaMSOSASqoE{_5%%0x*K6SBnf(4k=xGI0n&>X5i|>wXWd`^jx>?-iJBaLzB< z+Cl?3ouQM}+uC>p7#aX>>P$6MN9v*W=5oOM2{4!fhVLk|VdlaicYA*f=zHP(V+`;e z0(O=2uOrUw8ik*MEByx-DQdhiDNjBC00J#ZL_t(|0e{6<3vt6R3{{383}y&{;4vtJ z1wt6CL(np4hoBn*m%+v$W6&|slvVD@t`pmF+PynnQd^c^^o!F97>KCp(vZ|7N0P6q zs(#=z=msDHhA&Eqi~mLOK~rX5{2Ja(k&dh$pqy0rZB5zdu3j5Pt9Tawq}3gsuX*d(rL zM#9?5@XsYwqZ=BXv34xqUZ4bQ_>xbM30~iOq)bt6q1y&D%z$UZhETyAaOf^i2Qz`^ zc5+&Zlwl)-!Fob(XbC%(RbakZgcQ2XvYpWT^nXIr1$0z^RGq`eZ;2fj!va9JaZa|D&|;OEOPQpk=UY4ekg!&Z`wv(SZ{q$rbtcFn54*LXMYW%58`cx(!o9 zIVoc|iIx4gxL4wSNO0TC(o1$6uSr_yHpMP5W|4Qk3GZd1A!9}7T!ij%!(t7Eu(_0? zxaUp(-Z^-Egs@Ohh{1M>lB|L?x@9VlCqJ`42(sakkT1oRH>cqq!0R*dB~0jk@ebGG o1kWMW%kYUg`Nf*_zrYxN0oT|(^b07*qoM6N<$f*(iG1^@s6 delta 535 zcmV+y0_gql2gC%BIDY^Eb5ch_0Itp)=>Px#1ZP1_K>z@;j|==^1poj6*-1n}RCodH zSkZODFbq^WK}YZgWdx7F2F(a;0G$9fpq+qjkZ!;T838s>CAc(hN}5=X8$NpP=>f)8 z&S%LI5HmlEo%o7MtfV<11ZuI3*i!5OVmh{@IRJHh@Yw~#RDZF(*!Ir|%7a!=fO~)^x{s$ZEvghL~bYxu9``-H$pB9BHGLqZ}Bk zQZV0PO&(H6|ElM^?X4rqN(!;WeaH3-*@o1Sr_MlR+=e0{YZh4oJqT_`k-(kClV75^ z5NnJ4P}sC~$ba#-k`l4bL#5H#I@W1TMnI*?u#_Hp#x({$$c{HLZ&COru*0OlL>!q^ z>T|ZB1faC=Br0`pUEDFAB7ww6SE;LzReqGBbd~tpF|P1_$t`@OpQf!jQVPN%i%XL6 zYEh}#kP_Ror2u1Rm+}F%j=`9xNWf3YZ7Up4jN4vY?SDNOOOl`4j#K@XF>9OD-FGrh zk2ATV!?%)JQ!1d8r$hv5eZ^)$MnJW5Q55IMb=n1J6?hWU_fE`0ACxyigW(hv;c}-lrpFN)Ii31`o{o1 Z0hmAY;}D@!o@xL9002ovPDHLkV1m^H`6&PZ diff --git a/quadratic-client/public/images/icon-postgres.png b/quadratic-client/public/images/icon-postgres.png index 592dd13e6cfa5441eff9a1ae305b620a29914cd5..014f66a3bc317a77c569f3a79f6f58f07b5a997d 100644 GIT binary patch delta 1135 zcmV-#1d#iL2$%_wIDY|YX+uL$L}_zyY+-pIP%{7kc${^Ry-UMT6va=gQqVex4jmja zi9^)V3U+a5D-=aAR0XS3ntlmwd?YDSaT8Z5_#aroS#Yq3RB&-{5JU$N-JCipxM+CZ zmsHv!-pA$rIGl62T)-Pp87uYzDCw4wOvI$M)Vkz*gNL3|s()s(sG=+i{{6e_^U?L* zPl#DfyLJ%SPh6MIE|+$m0#kqeUDcn-ni~Dz)Ip6I7T}SIm2Ha&-X$I}Xer{V;JnMn zg3~UaJD!zfocNYl(h6#ZxJfLhJM?@9mx^VrwS(B+pVe2F#T@EU%wZEI7>ZC)fdmEN zfBe&qKaMSOSASqoE{_5%%0x*K6SBnf(4k=xGI0n&>X5i|>wXWd`^jx>?-iJBaLzB< z+Cl?3ouQM}+uC>p7#aX>>P$6MN9v*W=5oOM2{4!fhVLk|VdlaicYA*f=zHP(V+`;e z0(O=2uOrUw8ik*MEByx-DQdhiDNjBC00PxXL_t(|0e|IK+jZJN5FI%AH>n`%AnG7W z1tArn4nhYO6@V*{IsjLITmec2k}E*2U^9d6Mr)ycfH~wN_nfh$oyXptoqd=IjX)_C z@gp*t+R0>6Ic#H=8JZfFitStIANi3$-@-;>J+n`NP_DwTMBr<%^pMKU?1Z3(eXXv?f+be=vPh=f z$DG3OBo6ex*e}IK$Fk^SArw;>N|ptE46@SJyt*>#R^ypo2z?T2crN6=R;PGEkS3gp( zk@z6a(5{-&_*@Qg#Pgf15PohEIHhjHPY&M}H6uPd^!Ec9H&M#&E*YvQ43d@1_?CbV z)Q#E|Lr+n{Qse!F&R6Si`7@<(Ld}4T%>R<*t0#te#?2tr2%+Lch||=Nf7(7fbg{=@ z&VR^=x`KY!ZoD`8@SdXKh1`(3iEJI}lih)o`jx54m_950q6zt??aiTUM+$U^U+AO6 zcSL=kt;!b&#nDIevy&(pZgJbJ{ay z$6j?8xFPfnV6Va;eaP)1$Elx@4a$EF5r6$=(3Y$2<`@dOcFZ7hKXTWe@?1$_vl)=g z)n(Wsj(?ijXzmF%H?o=Fy-=53+KxeF3{tfvP-?M{LGTzz=rkBTpb$c1HIZ3tAE6hY zO#qn#{4`_`wY0M=U8FWHGO8=eHS{$`odpou!#9O}ioy6c)>EfqoDSM#dbY;sq-cW( zx)4G;U%%EHBX5?)NSkbIKk0gSZx&O!FMNma-3k5Q8S-{!`vt|sJqZO}m2XL?@#sVT z-MT3u$$%?M-aY8%Q1;`apnnDI*asXbxe7f*fj@9yqRfW_fouQ(002ovPDHLkV1hJO BECc`m delta 986 zcmV<0110>J34{ocIDY^Eb5ch_0Itp)=>Px#1ZP1_K>z@;j|==^1poj8nMp)JRCoc^ zSnYAzKoDJf^51k|xq>JaSgydN0?QQyS71_saRrD9z!d-$fGdzv0qWqg552Pv`E)`K z$;5AF=cN1Cd$-!%TM+rra3&{!F=n96V6X7(tl)d360-$Lm47Wn=akZikSa{IxpR~i zd@lLCrt({rA^YtnF`ri|zc^n=7-QG$n<=p@NJxo9+RVAx%pqd2<$FTKFWflhhL~UK zMqBC|Y4eq9F!sv%nES}S2d~9IMkR-JRE8=W;|Kaj_$(?$cnYNeZ|g>s%cH({haeIJatu8Ezq zXXKJeR2HWCR3YpanvVf{Og#M{nwct))W zE2%>LBZyQ|mFGr3_PrFQhcD6(1>2ueS+wLPcY!qvl7A>TKk?{{IQC{C+qso6u;^aJ zkVGQYXP8O*0Uo5b&Rh;ZnN6qzo4YI_oA3(h#zti<^#M|;tl+~dNh1l`3>`b;YmTpN zhijp0>9!EyUJ*F)ZD8F6=SSQadCv9I1@~PX5Mt^s9CCdSNqgkj=yk^}p^`)pwW|X{ z#5?nOV1G}0fWYlNpw5}|UhvtHS)fAj5n4%QzLoStd5~q_1I}-+BFwYo5vh;T2#6-3 zJ!9zySTqidClnC4U|+^QSa;RB(p0x_!nQ5_FFk!z6Gm?8+kIr5%0fcm;3k6 zfZ3(4Y22bNlJ>d?4xFjPs%I<#bQWmMdx|GoIfN4u3`JQX;cWhfbNO?Wjn;=ksvnG< z<;|%GT}qe`h`2ZX-vx6@5{UZ!4RDOEQ&U*T0#+zI{fH_=;Y|J&Onu|q1UYANO(>gV zb@lxjiTA)+HwdvoSu8b>Kzly=-{S(d!Xxe;F#rGn07*qo IM6N<$f_6XJAOHXW diff --git a/quadratic-client/public/images/icon-snowflake.png b/quadratic-client/public/images/icon-snowflake.png new file mode 100644 index 0000000000000000000000000000000000000000..57807d9c4ef0dcd52f636325f3f24d2c90563956 GIT binary patch literal 1078 zcmV-61j+k}P)at5VQ9hz=bbGKoXf z(h7EQXe$&&FjNJrQ<{DWZG0ptQgIVkDfk~)!C7#yh*WTKa1cZX5#5|RDY$5O-j`I` zBHqX4{WzR+xm>^-P#G)s0x0R0kxay-wbZ)gdxM9bQ>tdNsG=+i{{6e_^U?L*Pl#Df zyLJ%SPh6MIE|+$m0#kqeUDcn-ni~Dz)Ip6I7T}SIm2Ha&-X$I}Xer{V;JnMng3~Ua zJD!zfocNYl(h6#ZxJfLhJM?@9mx^VrwS(B+pVe2F#T@EU%wZEI7>ZC)fdmENfBe&q zKaMSOS71;sj{+>pL`e}7vc&VypY?`La=`luFqi^{?NiPd)$u0&_`3K~#7F?O4%O>o5#eIsCtlkZ!;TVS~O6!USZ4 zb^~C7lnDwGBpYM|XM>grgaisnjAT2Z{Bh6GQ5vh#wPo3A0se4dAYw*RlQ@zGWqe0+ z5}P&rHYms=PLYqsa&PPrX5tFw!=H6#JTv~mOauvWdf>u{`Me5zu_onYuN3feBAiY) z^0`hXM8;cExqQq>DtdQZvSNJ^&|?ilN?C6;`)k9`S$Ve=IYT;fk^<6W=qt2Ztr}8K zpkq!cXU6*{itz%egoIQ{YD4l2mqWuo#W&0%B&0f$f*$WkI#`a5ms3G)jhFcv0RI7m3;?xnfEvN+S8g%2=N0}W@*jpYc<4noHB>Q>Wpn- zDXY*b7z}T`kMjh*4U6A5wL|@=C^1Z|)Cjr2EMnz+#7H-Q;etszhiX(do)Fl{c%h6J zIGjtFL9F&-IgJos9W92h4lS*@7iMM!@Z-S@PcS?_}TPD5$&mQ==O40 zj*P@iV-g`w52^NtM0^zR*;DUe4k1}y{!OJ-*PIsCvfW!Lv{pzX(vaMeyh1wo3fYKG ziD43A2c0~#zLRdRftir5^FK_x&PL=Lt_>%=)EnM_$t`D`*n!j`<}=z;!-Wy^x#YKf w=Y2j0#@^t!fzu21pp5tbj?}R;=MPKp4mTsb6LEQ?!2kdN07*qoM6N<$f-~Uk3IG5A literal 0 HcmV?d00001 diff --git a/quadratic-client/public/images/mysql-icon.svg b/quadratic-client/public/images/mysql-icon.svg deleted file mode 100644 index 06898af95a..0000000000 --- a/quadratic-client/public/images/mysql-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx index bc604e6989..9d34a223b2 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableColumnContextMenu.tsx @@ -15,7 +15,6 @@ import { DropdownMenuSubContent, DropdownMenuSubTrigger, } from '@/shared/shadcn/ui/dropdown-menu'; -import { DropdownMenuItem } from '@radix-ui/react-dropdown-menu'; import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; @@ -56,10 +55,9 @@ export const TableColumnContextMenu = () => { - {contextMenu.table?.language === 'Import' ? 'Data' : 'Code'} Table + Table - Test diff --git a/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts b/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts index 85fca7c634..0312bea986 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsMarkers.ts @@ -1,4 +1,3 @@ -import { convertColorStringToTint } from '@/app/helpers/convertColor'; import { CodeCellLanguage, JsRenderCodeCell } from '@/app/quadratic-core-types'; import { Container, Point, Rectangle, Sprite, Texture } from 'pixi.js'; import { colors } from '../../theme/colors'; @@ -34,8 +33,8 @@ export const getLanguageSymbol = (language: CodeCellLanguage, isError: boolean): } else if (typeof language === 'object') { switch (language.Connection?.kind) { case 'MSSQL': - symbol.texture = Texture.from('/images/mssql-icon.svg'); - symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languageMssql); + symbol.texture = Texture.from('icon-mssql'); + symbol.tint = isError ? colors.cellColorError : 0xffffff; return symbol; case 'POSTGRES': @@ -44,13 +43,13 @@ export const getLanguageSymbol = (language: CodeCellLanguage, isError: boolean): return symbol; case 'MYSQL': - symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languageMysql); + symbol.tint = isError ? colors.cellColorError : 0xffffff; symbol.texture = Texture.from('icon-mysql'); return symbol; case 'SNOWFLAKE': - symbol.tint = isError ? colors.cellColorError : convertColorStringToTint(colors.languageSnowflake); - symbol.texture = Texture.from('/images/snowflake-icon.svg'); + symbol.tint = isError ? colors.cellColorError : 0xffffff; + symbol.texture = Texture.from('icon-snowflake'); return symbol; default: diff --git a/quadratic-client/src/app/gridGL/loadAssets.ts b/quadratic-client/src/app/gridGL/loadAssets.ts index f3b4d2835c..ae01a438a7 100644 --- a/quadratic-client/src/app/gridGL/loadAssets.ts +++ b/quadratic-client/src/app/gridGL/loadAssets.ts @@ -47,12 +47,13 @@ export function loadAssets(): Promise { addResourceOnce('icon-javascript', '/images/icon-javascript.png'); addResourceOnce('icon-postgres', '/images/icon-postgres.png'); addResourceOnce('icon-mysql', '/images/icon-mysql.png'); + addResourceOnce('icon-snowflake', '/images/icon-snowflake.png'); + addResourceOnce('icon-mssql', '/images/icon-mssql.png'); addResourceOnce('checkbox-icon', '/images/checkbox.png'); addResourceOnce('checkbox-checked-icon', '/images/checkbox-checked.png'); addResourceOnce('dropdown-icon', '/images/dropdown.png'); addResourceOnce('dropdown-white-icon', '/images/dropdown-white.png'); - addResourceOnce('mssql-icon', '/images/mssql-icon.svg'); - addResourceOnce('snowflake-icon', '/images/snowflake-icon.svg'); + addResourceOnce('arrow-up', '/images/arrow-up.svg'); addResourceOnce('arrow-down', '/images/arrow-down.svg'); From af8cbfd3fea312ed2b1d4ce7377c82e5fc430cac Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 14 Nov 2024 19:19:03 -0700 Subject: [PATCH 255/373] tweak labels --- quadratic-client/src/app/actions/dataTableSpec.ts | 7 ++++--- .../src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx | 8 ++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 82a87a3276..7d77e1f6a0 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -17,6 +17,7 @@ import { ShowIcon, SortIcon, TableConvertIcon, + TableIcon, UpArrowIcon, } from '@/shared/components/Icons'; import { Rectangle } from 'pixi.js'; @@ -74,7 +75,7 @@ const isAlternatingColorsShowing = (): boolean => { export const dataTableSpec: DataTableSpec = { [Action.FlattenTable]: { - label: 'Flatten to sheet', + label: 'Flatten to sheet data', Icon: FlattenTableIcon, run: () => { const table = getTable(); @@ -152,8 +153,8 @@ export const dataTableSpec: DataTableSpec = { }, }, [Action.CodeToDataTable]: { - label: 'Convert from code to data', - Icon: TableConvertIcon, + label: 'Flatten to table data', + Icon: TableIcon, run: () => { const table = getTable(); if (table) { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index cd5d0d2602..8c7a1ac478 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -6,7 +6,7 @@ import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandl import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { getCodeCell } from '@/app/helpers/codeCellLanguage'; import { JsRenderCodeCell } from '@/app/quadratic-core-types'; -import { LanguageIcon } from '@/app/ui/components/LanguageIcon'; +import { EditIcon } from '@/shared/components/Icons'; import { DropdownMenuItem, DropdownMenuSeparator } from '@/shared/shadcn/ui/dropdown-menu'; import { useMemo } from 'react'; @@ -47,12 +47,8 @@ export const TableMenu = (props: Props) => { {isCodeCell && ( <> defaultActionSpec[Action.EditTableCode].run()}> - } - text={`Edit ${cell.label}`} - /> + } text={`Edit code`} /> - )} From 005e91f4e830216536971082f7640677d1be78a4 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 15 Nov 2024 16:54:55 -0700 Subject: [PATCH 256/373] sort iconography --- quadratic-client/public/images/arrow-down.svg | 1 - quadratic-client/public/images/arrow-up.svg | 1 - .../public/images/sort-ascending.svg | 4 ++++ .../public/images/sort-descending.svg | 3 +++ .../src/app/actions/dataTableSpec.ts | 8 ++++---- .../contextMenus/tableSort/TableSort.tsx | 2 +- .../contextMenus/tableSort/TableSortEntry.tsx | 20 +++++++++---------- .../gridGL/cells/tables/TableColumnHeader.ts | 6 ++++-- quadratic-client/src/app/gridGL/loadAssets.ts | 4 ++-- .../src/shared/components/Icons.tsx | 13 ++++++++++++ 10 files changed, 40 insertions(+), 22 deletions(-) delete mode 100644 quadratic-client/public/images/arrow-down.svg delete mode 100644 quadratic-client/public/images/arrow-up.svg create mode 100644 quadratic-client/public/images/sort-ascending.svg create mode 100644 quadratic-client/public/images/sort-descending.svg diff --git a/quadratic-client/public/images/arrow-down.svg b/quadratic-client/public/images/arrow-down.svg deleted file mode 100644 index dcdb14ec5f..0000000000 --- a/quadratic-client/public/images/arrow-down.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/quadratic-client/public/images/arrow-up.svg b/quadratic-client/public/images/arrow-up.svg deleted file mode 100644 index 7929437eef..0000000000 --- a/quadratic-client/public/images/arrow-up.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/quadratic-client/public/images/sort-ascending.svg b/quadratic-client/public/images/sort-ascending.svg new file mode 100644 index 0000000000..bb6c6bcde8 --- /dev/null +++ b/quadratic-client/public/images/sort-ascending.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/quadratic-client/public/images/sort-descending.svg b/quadratic-client/public/images/sort-descending.svg new file mode 100644 index 0000000000..4d0aa2a1ef --- /dev/null +++ b/quadratic-client/public/images/sort-descending.svg @@ -0,0 +1,3 @@ + + + diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 7d77e1f6a0..21fee9e522 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -9,16 +9,16 @@ import { JsDataTableColumn, JsRenderCodeCell } from '@/app/quadratic-core-types' import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { DeleteIcon, - DownArrowIcon, EditIcon, FileRenameIcon, FlattenTableIcon, HideIcon, ShowIcon, + SortAscendingIcon, + SortDescendingIcon, SortIcon, TableConvertIcon, TableIcon, - UpArrowIcon, } from '@/shared/components/Icons'; import { Rectangle } from 'pixi.js'; import { sheets } from '../grid/controller/Sheets'; @@ -212,7 +212,7 @@ export const dataTableSpec: DataTableSpec = { }, [Action.SortTableColumnAscending]: { label: 'Sort column ascending', - Icon: UpArrowIcon, + Icon: SortAscendingIcon, run: () => { const table = getTable(); if (table) { @@ -231,7 +231,7 @@ export const dataTableSpec: DataTableSpec = { }, [Action.SortTableColumnDescending]: { label: 'Sort column descending', - Icon: DownArrowIcon, + Icon: SortDescendingIcon, run: () => { const table = getTable(); if (table) { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx index 791f56758a..86aaa506bd 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSort.tsx @@ -166,7 +166,7 @@ export const TableSort = () => { e.stopPropagation(); }} > -
Table Sort
+
Table sort
{sort.map((entry, index) => { const name = entry.column_index === -1 ? '' : contextMenu.table?.columns[entry.column_index]?.name ?? ''; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx index 13fe46c823..8e53ff5603 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/tableSort/TableSortEntry.tsx @@ -1,6 +1,12 @@ import { SortDirection } from '@/app/quadratic-core-types'; import { ValidationDropdown } from '@/app/ui/menus/Validations/Validation/ValidationUI/ValidationUI'; -import { DeleteIcon, DownArrowIcon, UpArrowIcon } from '@/shared/components/Icons'; +import { + DeleteIcon, + DownArrowIcon, + SortAscendingIcon, + SortDescendingIcon, + UpArrowIcon, +} from '@/shared/components/Icons'; import { Button } from '@/shared/shadcn/ui/button'; import { cn } from '@/shared/shadcn/utils'; import { useCallback, useState } from 'react'; @@ -62,11 +68,7 @@ export const TableSortEntry = (props: Props) => { { label: (
-
-
A
-
-
Z
-
+
Ascending
), @@ -75,11 +77,7 @@ export const TableSortEntry = (props: Props) => { { label: (
-
-
Z
-
-
A
-
+
Descending
), diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts index 5705eb9ae0..907d38e1bf 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeader.ts @@ -94,7 +94,9 @@ export class TableColumnHeader extends Container { this.sortButton.position.set(width - SORT_BUTTON_RADIUS - SORT_BUTTON_PADDING, height / 2); this.sortButton.visible = false; - const texture = sort ? Texture.from(sort.direction === 'Descending' ? 'arrow-up' : 'arrow-down') : Texture.EMPTY; + const texture = sort + ? Texture.from(sort.direction === 'Descending' ? 'sort-descending' : 'sort-ascending') + : Texture.EMPTY; this.sortIcon = this.addChild(new Sprite(texture)); this.sortIcon.anchor.set(0.5); this.sortIcon.position = this.sortButton.position; @@ -113,7 +115,7 @@ export class TableColumnHeader extends Container { } this.sortIcon.position = this.sortButton.position; this.sortIcon.texture = sort - ? Texture.from(sort.direction === 'Descending' ? 'arrow-up' : 'arrow-down') + ? Texture.from(sort.direction === 'Descending' ? 'sort-descending' : 'sort-ascending') : Texture.EMPTY; this.sortIcon.width = SORT_ICON_SIZE; this.sortIcon.scale.y = this.sortIcon.scale.x; diff --git a/quadratic-client/src/app/gridGL/loadAssets.ts b/quadratic-client/src/app/gridGL/loadAssets.ts index ae01a438a7..6b5f154d84 100644 --- a/quadratic-client/src/app/gridGL/loadAssets.ts +++ b/quadratic-client/src/app/gridGL/loadAssets.ts @@ -54,8 +54,8 @@ export function loadAssets(): Promise { addResourceOnce('dropdown-icon', '/images/dropdown.png'); addResourceOnce('dropdown-white-icon', '/images/dropdown-white.png'); - addResourceOnce('arrow-up', '/images/arrow-up.svg'); - addResourceOnce('arrow-down', '/images/arrow-down.svg'); + addResourceOnce('sort-ascending', '/images/sort-ascending.svg'); + addResourceOnce('sort-descending', '/images/sort-descending.svg'); // Wait until pixi fonts are loaded before resolving Loader.shared.load(() => { diff --git a/quadratic-client/src/shared/components/Icons.tsx b/quadratic-client/src/shared/components/Icons.tsx index 2fd4c57523..13785cb5ae 100644 --- a/quadratic-client/src/shared/components/Icons.tsx +++ b/quadratic-client/src/shared/components/Icons.tsx @@ -471,6 +471,19 @@ export const SortIcon: IconComponent = (props) => { return sort; }; +export const SortDescendingIcon: IconComponent = (props) => { + return sort; +}; + +export const SortAscendingIcon: IconComponent = (props) => { + const { className, ...rest } = props; + return ( + + sort + + ); +}; + export const DragIndicatorIcon: IconComponent = (props) => { return drag_indicator; }; From 7999bac1d881dcce0a1994319b8425e73ec2b5e0 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Sat, 16 Nov 2024 10:39:31 -0700 Subject: [PATCH 257/373] insert/delete row/column operations, bugs --- .../src/app/actions/dataTableSpec.ts | 6 +- .../gridGL/cells/tables/TableColumnHeaders.ts | 4 +- .../execute_operation/execute_data_table.rs | 140 ++++++++++++++++++ .../execution/execute_operation/mod.rs | 12 ++ .../src/controller/operations/operation.rs | 44 ++++++ quadratic-core/src/grid/data_table/column.rs | 35 +++-- .../src/grid/data_table/display_value.rs | 14 +- quadratic-core/src/grid/data_table/mod.rs | 10 +- quadratic-core/src/grid/data_table/row.rs | 28 ++-- quadratic-core/src/grid/sheet/data_table.rs | 11 ++ quadratic-core/src/values/array.rs | 83 ++++++----- 11 files changed, 303 insertions(+), 84 deletions(-) diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 82a87a3276..29b5a55ac4 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -5,7 +5,7 @@ import { createSelection } from '@/app/grid/sheet/selection'; import { doubleClickCell } from '@/app/gridGL/interaction/pointer/doubleClickCell'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; -import { JsDataTableColumn, JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { JsDataTableColumnHeader, JsRenderCodeCell } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { DeleteIcon, @@ -46,12 +46,12 @@ export const getTable = (): JsRenderCodeCell | undefined => { return pixiAppSettings.contextMenu?.table ?? pixiApp.cellsSheet().cursorOnDataTable(); }; -export const getColumns = (): JsDataTableColumn[] | undefined => { +export const getColumns = (): JsDataTableColumnHeader[] | undefined => { const table = getTable(); return table?.columns; }; -export const getDisplayColumns = (): JsDataTableColumn[] | undefined => { +export const getDisplayColumns = (): JsDataTableColumnHeader[] | undefined => { const table = getTable(); return table?.columns.filter((c) => c.display).map((c) => ({ ...c })); diff --git a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts index 7487504c70..9cc86f34c5 100644 --- a/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts +++ b/quadratic-client/src/app/gridGL/cells/tables/TableColumnHeaders.ts @@ -7,7 +7,7 @@ import { TablePointerDownResult } from '@/app/gridGL/cells/tables/Tables'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Coordinate } from '@/app/gridGL/types/size'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; -import { JsDataTableColumn, SortDirection } from '@/app/quadratic-core-types'; +import { JsDataTableColumnHeader, SortDirection } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { sharedEvents } from '@/shared/sharedEvents'; import { Container, Graphics, Point, Rectangle } from 'pixi.js'; @@ -52,7 +52,7 @@ export class TableColumnHeaders extends Container { } }; - private onSortPressed(column: JsDataTableColumn) { + private onSortPressed(column: JsDataTableColumnHeader) { const sortOrder: SortDirection | undefined = this.table.codeCell.sort?.find( (s) => s.column_index === column.valueIndex )?.direction; diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 39d195e716..879cfbf56c 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -449,6 +449,146 @@ impl GridController { bail!("Expected Operation::SortDataTable in execute_sort_data_table"); } + pub(super) fn execute_insert_data_table_column( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::InsertDataTableColumn { sheet_pos, index } = op.to_owned() { + let sheet_id = sheet_pos.sheet_id; + let sheet = self.try_sheet_mut_result(sheet_id)?; + let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; + let data_table = sheet.data_table_mut(data_table_pos)?; + data_table.insert_column(index as usize)?; + + let data_table_rect = data_table + .output_rect(sheet_pos.into(), true) + .to_sheet_rect(sheet_id); + + self.send_to_wasm(transaction, &data_table_rect)?; + transaction.add_code_cell(sheet_id, data_table_pos); + + let forward_operations = vec![op]; + let reverse_operations = vec![Operation::DeleteDataTableColumn { sheet_pos, index }]; + + self.data_table_operations( + transaction, + &data_table_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::InsertDataTableColumn in execute_insert_data_table_column"); + } + + pub(super) fn execute_delete_data_table_column( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::DeleteDataTableColumn { sheet_pos, index } = op.to_owned() { + let sheet_id = sheet_pos.sheet_id; + let sheet = self.try_sheet_mut_result(sheet_id)?; + let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; + let data_table = sheet.data_table_mut(data_table_pos)?; + data_table.delete_column(index as usize)?; + + let data_table_rect = data_table + .output_rect(sheet_pos.into(), true) + .to_sheet_rect(sheet_id); + + self.send_to_wasm(transaction, &data_table_rect)?; + transaction.add_code_cell(sheet_id, data_table_pos); + + let forward_operations = vec![op]; + let reverse_operations = vec![Operation::InsertDataTableColumn { sheet_pos, index }]; + + self.data_table_operations( + transaction, + &data_table_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::DeleteDataTableColumn in execute_delete_data_table_column"); + } + + pub(super) fn execute_insert_data_table_row( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::InsertDataTableRow { sheet_pos, index } = op.to_owned() { + let sheet_id = sheet_pos.sheet_id; + let sheet = self.try_sheet_mut_result(sheet_id)?; + let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; + let data_table = sheet.data_table_mut(data_table_pos)?; + data_table.insert_row(index as usize)?; + + let data_table_rect = data_table + .output_rect(sheet_pos.into(), true) + .to_sheet_rect(sheet_id); + + self.send_to_wasm(transaction, &data_table_rect)?; + transaction.add_code_cell(sheet_id, data_table_pos); + + let forward_operations = vec![op]; + let reverse_operations = vec![Operation::DeleteDataTableRow { sheet_pos, index }]; + + self.data_table_operations( + transaction, + &data_table_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::InsertDataTableRow in execute_insert_data_table_row"); + } + + pub(super) fn execute_delete_data_table_row( + &mut self, + transaction: &mut PendingTransaction, + op: Operation, + ) -> Result<()> { + if let Operation::DeleteDataTableRow { sheet_pos, index } = op.to_owned() { + let sheet_id = sheet_pos.sheet_id; + let sheet = self.try_sheet_mut_result(sheet_id)?; + let data_table_pos = sheet.first_data_table_within(sheet_pos.into())?; + let data_table = sheet.data_table_mut(data_table_pos)?; + data_table.delete_row(index as usize)?; + + let data_table_rect = data_table + .output_rect(sheet_pos.into(), true) + .to_sheet_rect(sheet_id); + + self.send_to_wasm(transaction, &data_table_rect)?; + transaction.add_code_cell(sheet_id, data_table_pos); + + let forward_operations = vec![op]; + let reverse_operations = vec![Operation::InsertDataTableRow { sheet_pos, index }]; + + self.data_table_operations( + transaction, + &data_table_rect, + forward_operations, + reverse_operations, + ); + + return Ok(()); + }; + + bail!("Expected Operation::DeleteDataTableRow in execute_delete_data_table_row"); + } + pub(super) fn execute_data_table_first_row_as_header( &mut self, transaction: &mut PendingTransaction, diff --git a/quadratic-core/src/controller/execution/execute_operation/mod.rs b/quadratic-core/src/controller/execution/execute_operation/mod.rs index 4b38e4d95c..4971c72327 100644 --- a/quadratic-core/src/controller/execution/execute_operation/mod.rs +++ b/quadratic-core/src/controller/execution/execute_operation/mod.rs @@ -51,6 +51,18 @@ impl GridController { Operation::SortDataTable { .. } => Self::handle_execution_operation_result( self.execute_sort_data_table(transaction, op), ), + Operation::InsertDataTableColumn { .. } => Self::handle_execution_operation_result( + self.execute_insert_data_table_column(transaction, op), + ), + Operation::DeleteDataTableColumn { .. } => Self::handle_execution_operation_result( + self.execute_delete_data_table_column(transaction, op), + ), + Operation::InsertDataTableRow { .. } => Self::handle_execution_operation_result( + self.execute_insert_data_table_row(transaction, op), + ), + Operation::DeleteDataTableRow { .. } => Self::handle_execution_operation_result( + self.execute_delete_data_table_row(transaction, op), + ), Operation::DataTableFirstRowAsHeader { .. } => { Self::handle_execution_operation_result( self.execute_data_table_first_row_as_header(transaction, op), diff --git a/quadratic-core/src/controller/operations/operation.rs b/quadratic-core/src/controller/operations/operation.rs index 3faadf1b5d..62a35c3d5e 100644 --- a/quadratic-core/src/controller/operations/operation.rs +++ b/quadratic-core/src/controller/operations/operation.rs @@ -76,6 +76,22 @@ pub enum Operation { sheet_pos: SheetPos, first_row_is_header: bool, }, + InsertDataTableColumn { + sheet_pos: SheetPos, + index: u32, + }, + DeleteDataTableColumn { + sheet_pos: SheetPos, + index: u32, + }, + InsertDataTableRow { + sheet_pos: SheetPos, + index: u32, + }, + DeleteDataTableRow { + sheet_pos: SheetPos, + index: u32, + }, ComputeCode { sheet_pos: SheetPos, }, @@ -275,6 +291,34 @@ impl fmt::Display for Operation { sheet_pos, first_row_is_header ) } + Operation::InsertDataTableColumn { sheet_pos, index } => { + write!( + fmt, + "InsertDataTableColumn {{ sheet_pos: {}, index: {} }}", + sheet_pos, index + ) + } + Operation::DeleteDataTableColumn { sheet_pos, index } => { + write!( + fmt, + "DeleteDataTableColumn {{ sheet_pos: {}, index: {} }}", + sheet_pos, index + ) + } + Operation::InsertDataTableRow { sheet_pos, index } => { + write!( + fmt, + "InsertDataTableRow {{ sheet_pos: {}, index: {} }}", + sheet_pos, index + ) + } + Operation::DeleteDataTableRow { sheet_pos, index } => { + write!( + fmt, + "DeleteDataTableRow {{ sheet_pos: {}, index: {} }}", + sheet_pos, index + ) + } Operation::SetCellFormats { .. } => write!(fmt, "SetCellFormats {{ todo }}",), Operation::SetCellFormatsSelection { selection, formats } => { write!( diff --git a/quadratic-core/src/grid/data_table/column.rs b/quadratic-core/src/grid/data_table/column.rs index 5c76a828e5..1d1a3c554a 100644 --- a/quadratic-core/src/grid/data_table/column.rs +++ b/quadratic-core/src/grid/data_table/column.rs @@ -5,44 +5,40 @@ use crate::Value; impl DataTable { /// Insert a new column at the given index. - pub fn insert_column(mut self, column_index: usize) -> Result { + pub fn insert_column(&mut self, column_index: usize) -> Result<()> { let column_name = self.unique_column_header_name(None).to_string(); - if let Value::Array(array) = self.value { - let new_array = array.insert_column(column_index, None)?; - self.value = Value::Array(new_array); + if let Value::Array(array) = &mut self.value { + array.insert_column(column_index, None)?; } else { bail!("Expected an array"); } self.display_buffer = None; - if let Some(mut headers) = self.column_headers { + if let Some(headers) = &mut self.column_headers { let new_header = DataTableColumnHeader::new(column_name, true, column_index as u32); headers.push(new_header); - self.column_headers = Some(headers); } - Ok(self) + Ok(()) } /// Remove a column at the given index. - pub fn remove_column(mut self, column_index: usize) -> Result { - if let Value::Array(array) = self.value { - let new_array = array.remove_column(column_index)?; - self.value = Value::Array(new_array); + pub fn delete_column(&mut self, column_index: usize) -> Result<()> { + if let Value::Array(array) = &mut self.value { + array.delete_column(column_index)?; } else { bail!("Expected an array"); } self.display_buffer = None; - if let Some(mut headers) = self.column_headers { + if let Some(headers) = &mut self.column_headers { headers.remove(column_index); - self.column_headers = Some(headers); } - Ok(self) + Ok(()) } } @@ -62,7 +58,7 @@ pub mod test { pretty_print_data_table(&data_table, Some("Original Data Table"), None); - data_table = data_table.insert_column(4).unwrap(); + data_table.insert_column(4).unwrap(); pretty_print_data_table(&data_table, Some("Data Table with New Column"), None); // there should be a "Column" header @@ -85,7 +81,8 @@ pub mod test { pretty_print_data_table(&source_data_table, Some("Original Data Table"), None); - let data_table = source_data_table.clone().remove_column(3).unwrap(); + let mut data_table = source_data_table.clone(); + data_table.delete_column(3).unwrap(); pretty_print_data_table(&data_table, Some("Data Table without Population"), None); // there should be no "population" header @@ -96,7 +93,8 @@ pub mod test { let expected_size = ArraySize::new(3, 4).unwrap(); assert_eq!(data_table.output_size(), expected_size); - let data_table = source_data_table.clone().remove_column(0).unwrap(); + let mut data_table = source_data_table.clone(); + data_table.delete_column(0).unwrap(); pretty_print_data_table(&data_table, Some("Data Table without City"), None); // there should be no "city" header @@ -113,7 +111,8 @@ pub mod test { CellValue::Text("region".into()) ); - let data_table = source_data_table.clone().remove_column(1).unwrap(); + let mut data_table = source_data_table.clone(); + data_table.delete_column(1).unwrap(); pretty_print_data_table(&data_table, Some("Data Table without Region"), None); // there should be no "region" header diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs index 2fbb54f0c0..c533810987 100644 --- a/quadratic-core/src/grid/data_table/display_value.rs +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -113,7 +113,7 @@ impl DataTable { return Ok(&CellValue::Blank); } - if pos.y == 0 && self.show_header { + if pos.y == 0 && self.show_header && !self.header_is_first_row { if let Some(columns) = &self.column_headers { let display_columns = columns.iter().filter(|c| c.display).collect::>(); @@ -123,9 +123,11 @@ impl DataTable { } } - if !self.header_is_first_row && self.show_header { - pos.y -= 1; - } + pos.y = match (self.header_is_first_row, self.show_header) { + (true, false) => pos.y + 1, + (false, true) => pos.y - 1, + _ => pos.y, + }; match self.display_buffer { Some(ref display_buffer) => self.display_value_from_buffer_at(display_buffer, pos), @@ -149,7 +151,9 @@ impl DataTable { row.to_vec() .into_iter() .enumerate() - .filter(|(i, _)| columns_to_show.contains(&i)) + .filter(|(i, _)| { + (*i == 0 && !self.header_is_first_row) || (*i != 0 && columns_to_show.contains(&i)) + }) .map(|(_, v)| v) .collect::>() } diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 8d3a66e2a0..062cad5ef8 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -273,12 +273,16 @@ impl DataTable { Value::Array(a) => { let mut size = a.size(); - if self.show_header && !self.header_is_first_row { - size.h = NonZeroU32::new(size.h.get() + 1).unwrap_or(ArraySize::_1X1.h); - } + let height = match (self.show_header, self.header_is_first_row) { + (true, false) => size.h.get() + 1, + (false, true) => size.h.get() - 1, + _ => size.h.get(), + }; + size.h = NonZeroU32::new(height).unwrap_or(ArraySize::_1X1.h); let width = self.columns_to_show().len(); size.w = NonZeroU32::new(width as u32).unwrap_or(ArraySize::_1X1.w); + size } Value::Single(_) | Value::Tuple(_) => ArraySize::_1X1, diff --git a/quadratic-core/src/grid/data_table/row.rs b/quadratic-core/src/grid/data_table/row.rs index ce00e9a655..322fde6056 100644 --- a/quadratic-core/src/grid/data_table/row.rs +++ b/quadratic-core/src/grid/data_table/row.rs @@ -5,31 +5,29 @@ use crate::Value; impl DataTable { /// Insert a new row at the given index. - pub fn insert_row(mut self, row_index: usize) -> Result { - if let Value::Array(array) = self.value { - let new_array = array.insert_row(row_index, None)?; - self.value = Value::Array(new_array); + pub fn insert_row(&mut self, row_index: usize) -> Result<()> { + if let Value::Array(array) = &mut self.value { + array.insert_row(row_index, None)?; } else { bail!("Expected an array"); } self.display_buffer = None; - Ok(self) + Ok(()) } /// Remove a row at the given index. - pub fn remove_row(mut self, row_index: usize) -> Result { - if let Value::Array(array) = self.value { - let new_array = array.remove_row(row_index)?; - self.value = Value::Array(new_array); + pub fn delete_row(&mut self, row_index: usize) -> Result<()> { + if let Value::Array(array) = &mut self.value { + array.delete_row(row_index)?; } else { bail!("Expected an array"); } self.display_buffer = None; - Ok(self) + Ok(()) } } @@ -49,7 +47,7 @@ pub mod test { pretty_print_data_table(&data_table, Some("Original Data Table"), None); - data_table = data_table.insert_row(4).unwrap(); + data_table.insert_row(4).unwrap(); pretty_print_data_table(&data_table, Some("Data Table with New Row"), None); // this should be a 5x4 array @@ -59,20 +57,22 @@ pub mod test { #[test] #[parallel] - fn test_data_table_remove_row() { + fn test_data_table_delete_row() { let (_, mut source_data_table) = new_data_table(); source_data_table.apply_first_row_as_header(); pretty_print_data_table(&source_data_table, Some("Original Data Table"), None); - let data_table = source_data_table.clone().remove_row(3).unwrap(); + let mut data_table = source_data_table.clone(); + data_table.delete_row(3).unwrap(); pretty_print_data_table(&data_table, Some("Data Table without row 4"), None); // this should be a 4x3 array let expected_size = ArraySize::new(4, 3).unwrap(); assert_eq!(data_table.output_size(), expected_size); - let data_table = source_data_table.clone().remove_row(1).unwrap(); + let mut data_table = source_data_table.clone(); + data_table.delete_row(1).unwrap(); pretty_print_data_table(&data_table, Some("Data Table without row 1"), None); // this should be a 4x3 array diff --git a/quadratic-core/src/grid/sheet/data_table.rs b/quadratic-core/src/grid/sheet/data_table.rs index 24f8fca48a..3647364fb6 100644 --- a/quadratic-core/src/grid/sheet/data_table.rs +++ b/quadratic-core/src/grid/sheet/data_table.rs @@ -2,6 +2,7 @@ use super::Sheet; use crate::{grid::data_table::DataTable, Pos}; use anyhow::{anyhow, bail, Result}; +use indexmap::map::{Entry, OccupiedEntry}; impl Sheet { /// Sets or deletes a data table. @@ -35,6 +36,16 @@ impl Sheet { .ok_or_else(|| anyhow!("Data table not found at {:?}", pos)) } + /// Returns a DataTable entry at a Pos for in-place manipulation + pub fn data_table_entry(&mut self, pos: Pos) -> Result> { + let entry = self.data_tables.entry(pos); + + match entry { + Entry::Occupied(entry) => Ok(entry), + Entry::Vacant(_) => bail!("Data table not found at {:?}", pos), + } + } + pub fn delete_data_table(&mut self, pos: Pos) -> Result { self.data_tables .swap_remove(&pos) diff --git a/quadratic-core/src/values/array.rs b/quadratic-core/src/values/array.rs index aad3498cfc..812d8ce610 100644 --- a/quadratic-core/src/values/array.rs +++ b/quadratic-core/src/values/array.rs @@ -275,13 +275,14 @@ impl Array { } /// Insert a new column at the given index. pub fn insert_column( - mut self, + &mut self, insert_at_index: usize, values: Option>, - ) -> Result { + ) -> Result<()> { let width = self.width(); let new_width = width + 1; let new_size = ArraySize::new_or_err(new_width, self.height())?; + self.size = new_size; let mut array = Array::new_empty(new_size); let mut col_index: u32 = 0; @@ -298,7 +299,7 @@ impl Array { .map_or(CellValue::Blank, |r| r.pop().unwrap_or(CellValue::Blank)) }; - for (i, value) in self.values.into_iter().enumerate() { + for (i, value) in self.values.iter().enumerate() { let col = i % width as usize; let row = ((i / width as usize) as f32).floor() as usize; let last = col as u32 == width - 1; @@ -308,7 +309,9 @@ impl Array { col_index += 1; } - array.set(col_index, row as u32, value)?; + // TODO(ddimaria): this clone is expensive, we should be able to modify + // the array in-place + array.set(col_index, row as u32, value.to_owned())?; if insert_at_end && last { array.set(width, row as u32, next_insert_value())?; @@ -320,10 +323,10 @@ impl Array { self.size = new_size; self.values = array.values; - Ok(self) + Ok(()) } - /// Remove a column at the given index. - pub fn remove_column(mut self, remove_at_index: usize) -> Result { + /// Delete a column at the given index. + pub fn delete_column(&mut self, remove_at_index: usize) -> Result<()> { let width = self.width(); let new_width = width - 1; let new_size = ArraySize::new_or_err(new_width, self.height())?; @@ -332,7 +335,7 @@ impl Array { // loop through the values and skip the remove_at_index column, // adding the rest to the new array - for (i, value) in self.values.into_iter().enumerate() { + for (i, value) in self.values.iter().enumerate() { let col = i % width as usize; let row = ((i / width as usize) as f32).floor() as usize; let last = col as u32 == width - 1; @@ -345,21 +348,23 @@ impl Array { continue; } - array.set(col_index, row as u32, value)?; + // TODO(ddimaria): this clone is expensive, we should be able to modify + // the array in-place + array.set(col_index, row as u32, value.to_owned())?; col_index = last.then(|| 0).unwrap_or(col_index + 1); } self.size = new_size; self.values = array.values; - Ok(self) + Ok(()) } /// Insert a new row at the given index. pub fn insert_row( - mut self, + &mut self, insert_at_index: usize, values: Option>, - ) -> Result { + ) -> Result<()> { let width = self.width(); let height = self.height(); let new_height = height + 1; @@ -390,10 +395,10 @@ impl Array { self.size = new_size; self.values = array.values; - Ok(self) + Ok(()) } - /// Remove a row at the given index. - pub fn remove_row(mut self, remove_at_index: usize) -> Result { + /// Delete a row at the given index. + pub fn delete_row(&mut self, remove_at_index: usize) -> Result<()> { let width = (self.width() as usize).min(self.values.len()); let start = remove_at_index * width; let end = start + width; @@ -407,7 +412,7 @@ impl Array { None => bail!("Cannot remove a row from a single row array"), } - Ok(self) + Ok(()) } /// Returns the only cell value in a 1x1 array, or an error if this is not a @@ -643,23 +648,23 @@ mod test { #[test] fn test_insert_column() { - let array = array!["a", "b"; "c", "d"]; + let mut array = array!["a", "b"; "c", "d"]; let values = vec![CellValue::from("e"), CellValue::from("f")]; - let array = array.insert_column(0, Some(values)).unwrap(); + array.insert_column(0, Some(values)).unwrap(); assert_eq!(array, array!["e", "a", "b"; "f", "c", "d"]); - let array = array!["a", "b"; "c", "d"]; + let mut array = array!["a", "b"; "c", "d"]; let values = vec![CellValue::from("e"), CellValue::from("f")]; - let array = array.insert_column(1, Some(values)).unwrap(); + array.insert_column(1, Some(values)).unwrap(); assert_eq!(array, array!["a", "e", "b"; "c", "f", "d"]); - let array = array!["a", "b"; "c", "d"]; + let mut array = array!["a", "b"; "c", "d"]; let values = vec![CellValue::from("e"), CellValue::from("f")]; - let array = array.insert_column(2, Some(values)).unwrap(); + array.insert_column(2, Some(values)).unwrap(); assert_eq!(array, array!["a", "b", "e"; "c", "d", "f"]); - let array = array!["a", "b"; "c", "d"]; - let array = array.insert_column(2, None).unwrap(); + let mut array = array!["a", "b"; "c", "d"]; + array.insert_column(2, None).unwrap(); assert_eq!( array, array!["a", "b", CellValue::Blank; "c", "d", CellValue::Blank] @@ -667,39 +672,39 @@ mod test { } #[test] - fn test_remove_column() { - let array = array!["a", "b", "c"; "d", "e", "f"; ]; - let array = array.remove_column(0).unwrap(); + fn test_delete_column() { + let mut array = array!["a", "b", "c"; "d", "e", "f"; ]; + array.delete_column(0).unwrap(); assert_eq!(array, array!["b", "c"; "e", "f";]); - let array = array!["a", "b", "c"; "d", "e", "f"; ]; - let array = array.remove_column(1).unwrap(); + let mut array = array!["a", "b", "c"; "d", "e", "f"; ]; + array.delete_column(1).unwrap(); assert_eq!(array, array!["a", "c"; "d", "f";]); - let array = array!["a", "b", "c"; "d", "e", "f"; ]; - let array = array.remove_column(2).unwrap(); + let mut array = array!["a", "b", "c"; "d", "e", "f"; ]; + array.delete_column(2).unwrap(); assert_eq!(array, array!["a", "b"; "d", "e";]); } #[test] fn test_insert_row() { - let array = array!["a", "b"; "c", "d"]; + let mut array = array!["a", "b"; "c", "d"]; let values = vec![CellValue::from("e"), CellValue::from("f")]; - let array = array.insert_row(0, Some(values)).unwrap(); + array.insert_row(0, Some(values)).unwrap(); assert_eq!(array, array!["e", "f"; "a", "b"; "c", "d"]); - let array = array!["a", "b"; "c", "d"]; + let mut array = array!["a", "b"; "c", "d"]; let values = vec![CellValue::from("e"), CellValue::from("f")]; - let array = array.insert_row(1, Some(values)).unwrap(); + array.insert_row(1, Some(values)).unwrap(); assert_eq!(array, array!["a", "b"; "e", "f"; "c", "d"]); - let array = array!["a", "b"; "c", "d"]; + let mut array = array!["a", "b"; "c", "d"]; let values = vec![CellValue::from("e"), CellValue::from("f")]; - let array = array.insert_row(2, Some(values)).unwrap(); + array.insert_row(2, Some(values)).unwrap(); assert_eq!(array, array!["a", "b"; "c", "d"; "e", "f"]); - let array = array!["a", "b"; "c", "d"]; - let array = array.insert_row(2, None).unwrap(); + let mut array = array!["a", "b"; "c", "d"]; + array.insert_row(2, None).unwrap(); assert_eq!( array, array!["a", "b"; "c", "d"; CellValue::Blank, CellValue::Blank] From 8a2170852ecbd0684ee2a10c80fbff4bab85b84a Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 18 Nov 2024 16:51:01 -0700 Subject: [PATCH 258/373] Allow reading/writing of CellValues to fix bugs and add features --- .../execute_operation/execute_data_table.rs | 17 +++--- .../src/controller/operations/cell_value.rs | 11 +++- quadratic-core/src/grid/sheet/code.rs | 48 +++++++++++++++- quadratic-core/src/rect.rs | 8 ++- quadratic-core/src/values/cell_values.rs | 55 ++++++++++++++++++- 5 files changed, 123 insertions(+), 16 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs index 879cfbf56c..50a260f309 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_data_table.rs @@ -112,14 +112,13 @@ impl GridController { let mut pos = Pos::from(sheet_pos); let sheet = self.try_sheet_mut_result(sheet_id)?; let data_table_pos = sheet.first_data_table_within(pos)?; + let data_table = sheet.data_table_result(data_table_pos)?; - // TODO(ddimaria): handle multiple values - if values.size() != 1 { - bail!("Only single values are supported for now"); + if data_table.readonly { + dbgjs!(format!("Data table {} is readonly", data_table.name)); + return Ok(()); } - let data_table = sheet.data_table_result(data_table_pos)?; - // if there is a display buffer, use it to find the source row index if data_table.display_buffer.is_some() { let index_to_find = pos.y - data_table_pos.y; @@ -131,11 +130,11 @@ impl GridController { pos.y -= 1; } - let value = values.safe_get(0, 0).cloned()?; - let old_value = sheet.get_code_cell_value(pos).unwrap_or(CellValue::Blank); + let rect = Rect::from_numbers(pos.x, pos.y, values.w as i64, values.h as i64); + let old_values = sheet.get_code_cell_values(rect); // send the new value - sheet.set_code_cell_value(pos, value.to_owned()); + sheet.set_code_cell_values(pos, values.to_owned()); let data_table_rect = SheetRect::from_numbers( sheet_pos.x, @@ -150,7 +149,7 @@ impl GridController { let forward_operations = vec![op]; let reverse_operations = vec![Operation::SetDataTableAt { sheet_pos, - values: old_value.into(), + values: old_values, }]; self.data_table_operations( diff --git a/quadratic-core/src/controller/operations/cell_value.rs b/quadratic-core/src/controller/operations/cell_value.rs index b5d4b28cf2..f1d8f79bcb 100644 --- a/quadratic-core/src/controller/operations/cell_value.rs +++ b/quadratic-core/src/controller/operations/cell_value.rs @@ -127,10 +127,17 @@ impl GridController { let rects = sheet.selection_rects_values(selection); for rect in rects { let cell_values = CellValues::new(rect.width(), rect.height()); + let sheet_pos = SheetPos::from((rect.min.x, rect.min.y, selection.sheet_id)); + + // TODO(ddimaria): remove this once we have a way to delete data tables ops.push(Operation::SetCellValues { - sheet_pos: (rect.min.x, rect.min.y, selection.sheet_id).into(), - values: cell_values, + sheet_pos, + values: cell_values.to_owned(), }); + ops.push(Operation::SetDataTableAt { + sheet_pos, + values: cell_values, + }) } }; ops diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index cceecf04f1..b01cdb489c 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -2,13 +2,14 @@ use std::ops::Range; use super::Sheet; use crate::{ + cell_values::CellValues, formulas::replace_internal_cell_references, grid::{ data_table::DataTable, js_types::{JsCodeCell, JsReturnInfo}, CodeCellLanguage, DataTableKind, }, - CellValue, Pos, Rect, + CellValue, Pos, Rect, Value, }; impl Sheet { @@ -93,6 +94,29 @@ impl Sheet { }) } + /// TODO(ddimaria): move to DataTable code + pub fn get_code_cell_values(&self, rect: Rect) -> CellValues { + self.iter_code_output_in_rect(rect) + .flat_map(|(new_rect, data_table)| match &data_table.value { + Value::Single(v) => vec![vec![v.to_owned()]], + Value::Array(a) => rect + .y_range() + .map(|y| { + rect.x_range() + .map(|x| { + let new_x = u32::try_from(x - new_rect.min.x).unwrap_or(0); + let new_y = u32::try_from(y - new_rect.min.y).unwrap_or(0); + a.get(new_x, new_y).unwrap().to_owned() + }) + .collect::>() + }) + .collect::>>(), + Value::Tuple(_) => vec![vec![]], + }) + .collect::>>() + .into() + } + /// Sets the CellValue for a DataTable at the Pos. /// Returns true if the value was set. /// TODO(ddimaria): move to DataTable code @@ -110,6 +134,28 @@ impl Sheet { .is_some() } + /// TODO(ddimaria): move to DataTable code + pub fn set_code_cell_values(&mut self, pos: Pos, values: CellValues) { + self.data_tables + .iter_mut() + .find(|(code_cell_pos, data_table)| { + data_table.output_rect(**code_cell_pos, false).contains(pos) + }) + .map(|(code_cell_pos, data_table)| { + let rect = Rect::from(&values); + + for y in rect.y_range() { + for x in rect.x_range() { + let new_x = u32::try_from(pos.x - code_cell_pos.x + x).unwrap_or(0); + let new_y = u32::try_from(pos.y - code_cell_pos.y + y).unwrap_or(0); + let value = values.get(x as u32, y as u32).unwrap_or(&CellValue::Blank); + + data_table.set_cell_value_at(new_x, new_y, value.to_owned()); + } + } + }); + } + pub fn iter_code_output_in_rect(&self, rect: Rect) -> impl Iterator { self.data_tables .iter() diff --git a/quadratic-core/src/rect.rs b/quadratic-core/src/rect.rs index 2fd98c237a..f737953a39 100644 --- a/quadratic-core/src/rect.rs +++ b/quadratic-core/src/rect.rs @@ -2,7 +2,7 @@ use std::{collections::HashSet, ops::Range}; use serde::{Deserialize, Serialize}; -use crate::{grid::SheetId, ArraySize, Pos, SheetRect}; +use crate::{cell_values::CellValues, grid::SheetId, ArraySize, Pos, SheetRect}; /// Rectangular region of cells. #[derive( @@ -275,6 +275,12 @@ impl From for Rect { } } +impl From<&CellValues> for Rect { + fn from(values: &CellValues) -> Self { + Rect::from_numbers(0, 0, values.w as i64, values.h as i64) + } +} + #[cfg(test)] mod test { use serial_test::parallel; diff --git a/quadratic-core/src/values/cell_values.rs b/quadratic-core/src/values/cell_values.rs index 16e98f211b..9752f58bba 100644 --- a/quadratic-core/src/values/cell_values.rs +++ b/quadratic-core/src/values/cell_values.rs @@ -1,7 +1,7 @@ //! CellValues is a 2D array of CellValue used for Operation::SetCellValues. //! The width and height may grow as needed. -use crate::{Array, ArraySize, CellValue}; +use crate::{Array, ArraySize, CellValue, Rect}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -62,6 +62,42 @@ impl CellValues { Ok(cell_value) } + pub fn get_owned<'a>(&'a mut self, x: u32, y: u32) -> anyhow::Result<&'a mut CellValue> { + if !(x < self.w && y < self.h) { + anyhow::bail!( + "CellValues::safe_get out of bounds: w={}, h={}, x={}, y={}", + self.w, + self.h, + x, + y + ); + } + + let column = self + .columns + .get_mut(x as usize) + .ok_or_else(|| anyhow::anyhow!("No column found at {x}"))?; + + column + .get_mut(&(y as u64)) + .ok_or_else(|| anyhow::anyhow!("No value found at ({x}, {y})")) + } + + pub fn get_rect(mut self, rect: Rect) -> Vec> { + let mut values = + vec![vec![CellValue::Blank; rect.width() as usize]; rect.height() as usize]; + + for y in rect.y_range() { + for x in rect.x_range() { + let new_x = u32::try_from(rect.min.x + x).unwrap_or(0); + let new_y = u32::try_from(rect.min.y + y).unwrap_or(0); + values[new_y as usize][new_x as usize] = + self.remove(new_x, new_y).unwrap_or(CellValue::Blank); + } + } + values + } + pub fn set(&mut self, x: u32, y: u32, value: CellValue) { if y >= self.h { self.h = y + 1; @@ -77,9 +113,9 @@ impl CellValues { self.columns[x as usize].insert(y as u64, value); } - pub fn remove(&mut self, x: u32, y: u32) { + pub fn remove(&mut self, x: u32, y: u32) -> Option { assert!(x < self.w && y < self.h, "CellValues::remove out of bounds"); - self.columns[x as usize].remove(&(y as u64)); + self.columns[x as usize].remove(&(y as u64)) } pub fn size(&self) -> u32 { @@ -117,6 +153,13 @@ impl CellValues { }) } + pub fn into_vec(self) -> Vec> { + let width = self.w as i64; + let height = self.h as i64; + + self.get_rect(Rect::new(0, 0, width, height)) + } + #[cfg(test)] /// Creates a CellValues from a CellValue, including CellValue::Blank (which is ignored in into) pub fn from_cell_value(value: CellValue) -> Self { @@ -203,6 +246,12 @@ impl From for CellValues { } } +impl From for Vec> { + fn from(cell_values: CellValues) -> Self { + cell_values.into_vec() + } +} + #[cfg(test)] mod test { use crate::wasm_bindings::js::clear_js_calls; From 3706ca435163e753279cf0da349f396d06a0ffde Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Mon, 18 Nov 2024 16:52:22 -0700 Subject: [PATCH 259/373] Default to blank values in get_code_cell_values() --- quadratic-core/src/grid/sheet/code.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index b01cdb489c..58c7a2d1c1 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -106,7 +106,7 @@ impl Sheet { .map(|x| { let new_x = u32::try_from(x - new_rect.min.x).unwrap_or(0); let new_y = u32::try_from(y - new_rect.min.y).unwrap_or(0); - a.get(new_x, new_y).unwrap().to_owned() + a.get(new_x, new_y).unwrap_or(&CellValue::Blank).to_owned() }) .collect::>() }) From 61bc6609888bf3c3f6b06c4c5f832403a70699af Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 19 Nov 2024 09:56:43 -0700 Subject: [PATCH 260/373] Correctly display multi-column code outputs --- quadratic-core/src/grid/data_table/display_value.rs | 3 ++- quadratic-core/src/grid/data_table/mod.rs | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs index c533810987..6274714db4 100644 --- a/quadratic-core/src/grid/data_table/display_value.rs +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -138,8 +138,9 @@ impl DataTable { /// Get the indices of the columns to show. pub fn columns_to_show(&self) -> Vec { self.column_headers + .to_owned() + .unwrap_or_else(|| self.default_header(None)) .iter() - .flatten() .enumerate() .filter(|(_, c)| c.display) .map(|(index, _)| index) diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 062cad5ef8..2005e00e32 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -26,6 +26,8 @@ use serde::{Deserialize, Serialize}; use sort::DataTableSort; use strum_macros::Display; use table_formats::TableFormats; + +#[cfg(test)] use tabled::{ builder::Builder, settings::{Color, Modify, Style}, @@ -341,6 +343,8 @@ impl DataTable { } } + /// Pretty print a data table for testing + #[cfg(test)] pub fn pretty_print_data_table( data_table: &DataTable, title: Option<&str>, From efb2ab74560dded657dda1cf28b5f1b2482a3964 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 19 Nov 2024 16:23:56 -0700 Subject: [PATCH 261/373] Paste values on top of a data table + some edge cases --- .../src/controller/operations/clipboard.rs | 44 ++++++++++++++++++- quadratic-core/src/grid/sheet/code.rs | 16 +++++++ quadratic-core/src/values/cell_values.rs | 17 ++++--- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/quadratic-core/src/controller/operations/clipboard.rs b/quadratic-core/src/controller/operations/clipboard.rs index 60f7943f68..73facc0382 100644 --- a/quadratic-core/src/controller/operations/clipboard.rs +++ b/quadratic-core/src/controller/operations/clipboard.rs @@ -15,7 +15,7 @@ use crate::grid::sheet::borders::BorderStyleCellUpdates; use crate::grid::sheet::validations::validation::Validation; use crate::grid::{CodeCellLanguage, DataTableKind}; use crate::selection::Selection; -use crate::{CellValue, Pos, SheetPos, SheetRect}; +use crate::{CellValue, Pos, Rect, SheetPos, SheetRect}; // todo: break up this file so tests are easier to write @@ -255,7 +255,47 @@ impl GridController { special, ); - if let Some(values) = values { + if let Some(mut values) = values { + if let Some(sheet) = self.try_sheet(selection.sheet_id) { + let rect = Rect::from_numbers( + start_pos.x, + start_pos.y, + clipboard.w as i64, + clipboard.h as i64, + ); + + // Determine if the paste is happening within a data table. + // If so, replace values in the data table with the + // intersection of the data table and the paste + sheet.iter_code_output_intersects_rect(rect).for_each( + |(_, intersection_rect, data_table)| { + // there is no pasting on top of code cell output + if !data_table.readonly { + let adjusted_rect = Rect::from_numbers( + start_pos.x - intersection_rect.min.x, + start_pos.y - intersection_rect.min.y, + intersection_rect.width() as i64, + intersection_rect.height() as i64, + ); + + // pull the values from `values`, replacing + // the values in `values` with CellValue::Blank + let cell_values = values.get_rect(adjusted_rect); + let sheet_pos = SheetPos { + x: intersection_rect.min.x, + y: intersection_rect.min.y, + sheet_id: selection.sheet_id, + }; + + ops.push(Operation::SetDataTableAt { + sheet_pos, + values: CellValues::from(cell_values), + }); + } + }, + ); + } + ops.push(Operation::SetCellValues { sheet_pos: start_pos.to_sheet_pos(selection.sheet_id), values, diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index 58c7a2d1c1..bb285fbe46 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -167,6 +167,22 @@ impl Sheet { }) } + pub fn iter_code_output_intersects_rect( + &self, + rect: Rect, + ) -> impl Iterator { + self.data_tables + .iter() + .filter_map(move |(pos, data_table)| { + let output_rect = data_table.output_rect(*pos, false); + output_rect + .intersection(&rect) + .and_then(|intersection_rect| { + Some((output_rect, intersection_rect, data_table)) + }) + }) + } + /// Returns whether a rect overlaps the output of a code cell. /// It will only check code_cells until it finds the data_table at code_pos (since later data_tables do not cause spills in earlier ones) pub fn has_code_cell_in_rect(&self, rect: &Rect, code_pos: Pos) -> bool { diff --git a/quadratic-core/src/values/cell_values.rs b/quadratic-core/src/values/cell_values.rs index 9752f58bba..aa03702c77 100644 --- a/quadratic-core/src/values/cell_values.rs +++ b/quadratic-core/src/values/cell_values.rs @@ -83,14 +83,14 @@ impl CellValues { .ok_or_else(|| anyhow::anyhow!("No value found at ({x}, {y})")) } - pub fn get_rect(mut self, rect: Rect) -> Vec> { + pub fn get_rect(&mut self, rect: Rect) -> Vec> { let mut values = vec![vec![CellValue::Blank; rect.width() as usize]; rect.height() as usize]; for y in rect.y_range() { for x in rect.x_range() { - let new_x = u32::try_from(rect.min.x + x).unwrap_or(0); - let new_y = u32::try_from(rect.min.y + y).unwrap_or(0); + let new_x = u32::try_from(x).unwrap_or(0); + let new_y = u32::try_from(y).unwrap_or(0); values[new_y as usize][new_x as usize] = self.remove(new_x, new_y).unwrap_or(CellValue::Blank); } @@ -114,7 +114,12 @@ impl CellValues { } pub fn remove(&mut self, x: u32, y: u32) -> Option { - assert!(x < self.w && y < self.h, "CellValues::remove out of bounds"); + assert!( + x < self.w && y < self.h, + "CellValues::remove out of bounds: x={x}, y={y}, w={}, h={}", + self.w, + self.h + ); self.columns[x as usize].remove(&(y as u64)) } @@ -153,7 +158,7 @@ impl CellValues { }) } - pub fn into_vec(self) -> Vec> { + pub fn into_vec(&mut self) -> Vec> { let width = self.w as i64; let height = self.h as i64; @@ -247,7 +252,7 @@ impl From for CellValues { } impl From for Vec> { - fn from(cell_values: CellValues) -> Self { + fn from(mut cell_values: CellValues) -> Self { cell_values.into_vec() } } From 7a4d147ef463bdc2a5fd5eb29bc80a63fda92a0d Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 21 Nov 2024 14:29:39 -0700 Subject: [PATCH 262/373] Handle to-the-left and from-the-top paste conditions for data tables --- .../src/controller/execution/spills.rs | 2 +- .../src/controller/operations/clipboard.rs | 156 +++++++++++++++--- .../src/controller/user_actions/import.rs | 5 +- .../src/grid/data_table/display_value.rs | 5 +- quadratic-core/src/grid/data_table/mod.rs | 1 + quadratic-core/src/grid/sheet/code.rs | 2 +- quadratic-core/src/test_util.rs | 8 +- quadratic-core/src/values/cell_values.rs | 12 +- 8 files changed, 160 insertions(+), 31 deletions(-) diff --git a/quadratic-core/src/controller/execution/spills.rs b/quadratic-core/src/controller/execution/spills.rs index 01a2b40ccb..bbd1de9b31 100644 --- a/quadratic-core/src/controller/execution/spills.rs +++ b/quadratic-core/src/controller/execution/spills.rs @@ -68,7 +68,7 @@ impl GridController { || sheet.has_code_cell_in_rect(&output, *pos) { // if spill error has not been set, then set it and start the more expensive checks for all later code_cells. - if !data_table.spill_error { + if data_table.readonly && !data_table.spill_error { return Some(true); } } else if data_table.spill_error { diff --git a/quadratic-core/src/controller/operations/clipboard.rs b/quadratic-core/src/controller/operations/clipboard.rs index 73facc0382..3c7e9e5afa 100644 --- a/quadratic-core/src/controller/operations/clipboard.rs +++ b/quadratic-core/src/controller/operations/clipboard.rs @@ -256,7 +256,7 @@ impl GridController { ); if let Some(mut values) = values { - if let Some(sheet) = self.try_sheet(selection.sheet_id) { + if let Some(sheet) = self.try_sheet_mut(selection.sheet_id) { let rect = Rect::from_numbers( start_pos.x, start_pos.y, @@ -268,16 +268,52 @@ impl GridController { // If so, replace values in the data table with the // intersection of the data table and the paste sheet.iter_code_output_intersects_rect(rect).for_each( - |(_, intersection_rect, data_table)| { + |(output_rect, intersection_rect, data_table)| { // there is no pasting on top of code cell output if !data_table.readonly { let adjusted_rect = Rect::from_numbers( - start_pos.x - intersection_rect.min.x, - start_pos.y - intersection_rect.min.y, + intersection_rect.min.x - start_pos.x, + intersection_rect.min.y - start_pos.y, intersection_rect.width() as i64, intersection_rect.height() as i64, ); + let contains_header = + intersection_rect.y_range().contains(&output_rect.min.y); + let headers = data_table.column_headers.to_owned(); + + if let (Some(mut headers), true) = (headers, contains_header) { + let y = output_rect.min.y - start_pos.y; + + for x in intersection_rect.x_range() { + let new_x = x - output_rect.min.x; + + if let Some(header) = headers.get_mut(new_x as usize) { + let header_rect = + Rect::from_numbers(x - start_pos.x, y, 1, 1); + let new_x = + u32::try_from(x - start_pos.x).unwrap_or(0); + let new_y = u32::try_from(y).unwrap_or(0); + + let cell_value = values + .remove(new_x, new_y) + .unwrap_or(CellValue::Blank); + + header.name = cell_value; + } + } + + let sheet_pos = + output_rect.min.to_sheet_pos(selection.sheet_id); + ops.push(Operation::DataTableMeta { + sheet_pos, + name: None, + alternating_colors: None, + columns: Some(headers.to_vec()), + show_header: None, + }); + } + // pull the values from `values`, replacing // the values in `values` with CellValue::Blank let cell_values = values.get_rect(adjusted_rect); @@ -500,10 +536,11 @@ mod test { use super::{PasteSpecial, *}; use crate::controller::active_transactions::transaction_name::TransactionName; - use crate::controller::user_actions::import::tests::simple_csv; + use crate::controller::user_actions::import::tests::{simple_csv, simple_csv_at}; use crate::grid::formats::format_update::FormatUpdate; use crate::grid::sheet::validations::validation_rules::ValidationRule; use crate::grid::SheetId; + use crate::test_util::{assert_cell_value_row, print_data_table, print_table}; use crate::Rect; #[test] @@ -820,6 +857,22 @@ mod test { #[parallel] fn paste_clipboard_with_data_table() { let (mut gc, sheet_id, _, _) = simple_csv(); + let rect = Rect::new(0, 0, 3, 10); + + let paste = |gc: &mut GridController, x, y, html| { + gc.paste_from_clipboard( + Selection { + sheet_id, + x, + y, + ..Default::default() + }, + None, + Some(html), + PasteSpecial::None, + None, + ) + }; let (_, html) = gc .sheet(sheet_id) @@ -827,27 +880,90 @@ mod test { sheet_id, x: 0, y: 0, - rects: Some(vec![Rect::new(0, 0, 3, 10)]), + rects: Some(vec![rect]), ..Default::default() }) .unwrap(); - gc.paste_from_clipboard( - Selection { + let expected_row1 = vec!["city", "region", "country", "population"]; + + // paste side by side + paste(&mut gc, 4, 0, html.clone()); + print_table(&gc, sheet_id, Rect::from_numbers(0, 0, 8, 10)); + assert_cell_value_row(&mut gc, sheet_id, 4, 0, 0, expected_row1); + } + + #[test] + #[parallel] + fn paste_clipboard_on_top_of_data_table() { + let (mut gc, sheet_id, _, _) = simple_csv_at(Pos { x: 2, y: 0 }); + let rect = Rect::from_numbers(10, 0, 2, 2); + let sheet = gc.sheet_mut(sheet_id); + + sheet.test_set_values(10, 0, 2, 2, vec!["1", "2", "3", "4"]); + let (_, html) = sheet + .copy_to_clipboard(&Selection { sheet_id, - x: 20, - y: 20, + x: 10, + y: 0, + rects: Some(vec![rect]), ..Default::default() - }, - None, - Some(html), - PasteSpecial::None, - None, - ); + }) + .unwrap(); - assert_eq!( - gc.sheet(sheet_id).cell_value((20, 20).into()), - Some(CellValue::Text("city".into())) - ); + let paste = |gc: &mut GridController, x, y, html| { + gc.paste_from_clipboard( + Selection { + sheet_id, + x, + y, + ..Default::default() + }, + None, + Some(html), + PasteSpecial::None, + None, + ) + }; + + let expected_row1 = vec!["1", "2"]; + let expected_row2 = vec!["3", "4"]; + + // paste overlap inner + paste(&mut gc, 4, 2, html.clone()); + print_table(&gc, sheet_id, Rect::from_numbers(0, 0, 8, 11)); + assert_cell_value_row(&mut gc, sheet_id, 4, 5, 2, expected_row1.clone()); + assert_cell_value_row(&mut gc, sheet_id, 4, 5, 3, expected_row2.clone()); + gc.undo(None); + + // paste overlap with right grid + paste(&mut gc, 5, 2, html.clone()); + print_table(&gc, sheet_id, Rect::from_numbers(0, 0, 8, 11)); + assert_cell_value_row(&mut gc, sheet_id, 5, 6, 2, expected_row1.clone()); + assert_cell_value_row(&mut gc, sheet_id, 5, 6, 3, expected_row2.clone()); + gc.undo(None); + + // paste overlap with bottom grid + paste(&mut gc, 4, 10, html.clone()); + print_table(&gc, sheet_id, Rect::from_numbers(0, 0, 8, 12)); + assert_cell_value_row(&mut gc, sheet_id, 4, 5, 10, expected_row1.clone()); + assert_cell_value_row(&mut gc, sheet_id, 4, 5, 11, expected_row2.clone()); + gc.undo(None); + + // paste overlap with bottom left grid + paste(&mut gc, 1, 10, html.clone()); + print_table(&gc, sheet_id, Rect::from_numbers(0, 0, 8, 12)); + // print_table(&gc, sheet_id, Rect::from_numbers(1, 10, 2, 2)); + assert_cell_value_row(&mut gc, sheet_id, 1, 2, 10, expected_row1.clone()); + assert_cell_value_row(&mut gc, sheet_id, 1, 2, 11, expected_row2.clone()); + gc.undo(None); + + // paste overlap with top left grid + paste(&mut gc, 3, 0, html.clone()); + print_data_table(&gc, sheet_id, Rect::from_numbers(2, 0, 4, 4)); + // print_table(&gc, sheet_id, Rect::from_numbers(2, 0, 4, 4)); + // assert_cell_value_row(&mut gc, sheet_id, 1, 2, 10, expected_row1.clone()); + // assert_cell_value_row(&mut gc, sheet_id, 1, 2, 11, expected_row2.clone()); + gc.undo(None); } } diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index 0451939339..c09efcd0f4 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -109,10 +109,13 @@ pub(crate) mod tests { // "../quadratic-rust-shared/data/parquet/flights_1m.parquet"; pub(crate) fn simple_csv() -> (GridController, SheetId, Pos, &'static str) { + simple_csv_at(Pos { x: 0, y: 0 }) + } + + pub(crate) fn simple_csv_at(pos: Pos) -> (GridController, SheetId, Pos, &'static str) { let csv_file = read_test_csv_file("simple.csv"); let mut gc = GridController::test(); let sheet_id = gc.grid.sheets()[0].id; - let pos = Pos { x: 0, y: 0 }; let file_name = "simple.csv"; gc.import_csv(sheet_id, csv_file.as_slice().to_vec(), file_name, pos, None) diff --git a/quadratic-core/src/grid/data_table/display_value.rs b/quadratic-core/src/grid/data_table/display_value.rs index 6274714db4..8920388853 100644 --- a/quadratic-core/src/grid/data_table/display_value.rs +++ b/quadratic-core/src/grid/data_table/display_value.rs @@ -153,7 +153,10 @@ impl DataTable { .into_iter() .enumerate() .filter(|(i, _)| { - (*i == 0 && !self.header_is_first_row) || (*i != 0 && columns_to_show.contains(&i)) + // TODO(ddimaria): removed the "*i != 0" check, delete the + // commented-out code if no bugs arise + // (*i == 0 && !self.header_is_first_row) || (*i != 0 && columns_to_show.contains(&i)) + (*i == 0 && !self.header_is_first_row) || (columns_to_show.contains(&i)) }) .map(|(_, v)| v) .collect::>() diff --git a/quadratic-core/src/grid/data_table/mod.rs b/quadratic-core/src/grid/data_table/mod.rs index 2005e00e32..1e445bb050 100644 --- a/quadratic-core/src/grid/data_table/mod.rs +++ b/quadratic-core/src/grid/data_table/mod.rs @@ -369,6 +369,7 @@ impl DataTable { .as_ref() .unwrap() .iter() + .filter(|h| h.display) .map(|h| h.name.to_string()) .collect::>(); builder.set_header([display_index, headers].concat()); diff --git a/quadratic-core/src/grid/sheet/code.rs b/quadratic-core/src/grid/sheet/code.rs index bb285fbe46..4468e5e26d 100644 --- a/quadratic-core/src/grid/sheet/code.rs +++ b/quadratic-core/src/grid/sheet/code.rs @@ -168,7 +168,7 @@ impl Sheet { } pub fn iter_code_output_intersects_rect( - &self, + &mut self, rect: Rect, ) -> impl Iterator { self.data_tables diff --git a/quadratic-core/src/test_util.rs b/quadratic-core/src/test_util.rs index 4992391915..f05e779b0e 100644 --- a/quadratic-core/src/test_util.rs +++ b/quadratic-core/src/test_util.rs @@ -252,9 +252,11 @@ pub fn print_table(grid_controller: &GridController, sheet_id: SheetId, rect: Re #[track_caller] #[cfg(test)] pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rect: Rect) { - if let Some(sheet) = grid_controller.try_sheet(sheet_id) { - let data_table = sheet.data_table(rect.min).unwrap(); + let sheet = grid_controller + .try_sheet(sheet_id) + .expect("Sheet not found"); + if let Some(data_table) = sheet.data_table(rect.min) { let max = rect.max.y - rect.min.y + 1; crate::grid::data_table::test::pretty_print_data_table( data_table, @@ -262,7 +264,7 @@ pub fn print_data_table(grid_controller: &GridController, sheet_id: SheetId, rec Some(max as usize), ); } else { - println!("Sheet not found"); + println!("Data table not found at {:?}", rect.min); } } diff --git a/quadratic-core/src/values/cell_values.rs b/quadratic-core/src/values/cell_values.rs index aa03702c77..ab207e69a5 100644 --- a/quadratic-core/src/values/cell_values.rs +++ b/quadratic-core/src/values/cell_values.rs @@ -84,15 +84,19 @@ impl CellValues { } pub fn get_rect(&mut self, rect: Rect) -> Vec> { + // let size = rect.size(); + // let mut values = CellValues::new(size.w.get(), size.h.get()); let mut values = vec![vec![CellValue::Blank; rect.width() as usize]; rect.height() as usize]; - for y in rect.y_range() { - for x in rect.x_range() { + for (y_index, y) in rect.y_range().enumerate() { + for (x_index, x) in rect.x_range().enumerate() { let new_x = u32::try_from(x).unwrap_or(0); let new_y = u32::try_from(y).unwrap_or(0); - values[new_y as usize][new_x as usize] = - self.remove(new_x, new_y).unwrap_or(CellValue::Blank); + + values[y_index as usize][x_index as usize] = self + .remove(new_x as u32, new_y as u32) + .unwrap_or(CellValue::Blank); } } values From 1d26e68c58b5321c7a9e14cf63c2545d0b680395 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 21 Nov 2024 15:33:20 -0700 Subject: [PATCH 263/373] Remove data table when pasting over the source cell --- .../src/controller/operations/clipboard.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/quadratic-core/src/controller/operations/clipboard.rs b/quadratic-core/src/controller/operations/clipboard.rs index 3c7e9e5afa..9ecbf87005 100644 --- a/quadratic-core/src/controller/operations/clipboard.rs +++ b/quadratic-core/src/controller/operations/clipboard.rs @@ -269,8 +269,11 @@ impl GridController { // intersection of the data table and the paste sheet.iter_code_output_intersects_rect(rect).for_each( |(output_rect, intersection_rect, data_table)| { + let contains_source_cell = + intersection_rect.contains(output_rect.min); + // there is no pasting on top of code cell output - if !data_table.readonly { + if !data_table.readonly && !contains_source_cell { let adjusted_rect = Rect::from_numbers( intersection_rect.min.x - start_pos.x, intersection_rect.min.y - start_pos.y, @@ -289,14 +292,12 @@ impl GridController { let new_x = x - output_rect.min.x; if let Some(header) = headers.get_mut(new_x as usize) { - let header_rect = - Rect::from_numbers(x - start_pos.x, y, 1, 1); - let new_x = + let safe_x = u32::try_from(x - start_pos.x).unwrap_or(0); - let new_y = u32::try_from(y).unwrap_or(0); + let safe_y = u32::try_from(y).unwrap_or(0); let cell_value = values - .remove(new_x, new_y) + .remove(safe_x, safe_y) .unwrap_or(CellValue::Blank); header.name = cell_value; From 3c1cbf6789dd4024cd0e589369d237246a4868f9 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Thu, 21 Nov 2024 16:47:13 -0700 Subject: [PATCH 264/373] Fix most of the broken tests --- .../src/controller/execution/run_code/mod.rs | 2 +- .../execution/run_code/run_formula.rs | 4 +- .../src/controller/operations/cell_value.rs | 53 +++++++++++-------- quadratic-core/src/grid/sheet.rs | 1 + 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 10f14e8c83..4ff0573064 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -517,7 +517,7 @@ mod test { }; let new_data_table = DataTable::new( DataTableKind::CodeRun(new_code_run), - "Table 1", + "Table 2", Value::Single(CellValue::Text("replace me".to_string())), false, false, diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index d7551cded6..fec0c5d912 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -262,7 +262,7 @@ mod test { result, DataTable::new( DataTableKind::CodeRun(code_run), - "JavaScript 1", + "JavaScript1", Value::Single(CellValue::Number(12.into())), false, false, @@ -332,7 +332,7 @@ mod test { result, DataTable::new( DataTableKind::CodeRun(code_run), - "JavaScript 1", + "JavaScript1", Value::Array(array), false, false, diff --git a/quadratic-core/src/controller/operations/cell_value.rs b/quadratic-core/src/controller/operations/cell_value.rs index f1d8f79bcb..305562d415 100644 --- a/quadratic-core/src/controller/operations/cell_value.rs +++ b/quadratic-core/src/controller/operations/cell_value.rs @@ -348,19 +348,26 @@ mod test { "5 + 5".to_string(), None, ); + let selection = Selection::rect(Rect::from_numbers(1, 2, 2, 1), sheet_id); let operations = gc.delete_cells_operations(&selection); - assert_eq!(operations.len(), 1); + let sheet_pos = SheetPos { + x: 1, + y: 2, + sheet_id, + }; + let values = CellValues::new(2, 1); + + assert_eq!(operations.len(), 2); assert_eq!( operations, - vec![Operation::SetCellValues { - sheet_pos: SheetPos { - x: 1, - y: 2, - sheet_id + vec![ + Operation::SetCellValues { + sheet_pos, + values: values.clone() }, - values: CellValues::new(2, 1) - }] + Operation::SetDataTableAt { sheet_pos, values }, + ] ); } @@ -389,26 +396,28 @@ mod test { ); let selection = Selection::columns(&[1, 2], sheet_id); let operations = gc.delete_cells_operations(&selection); - assert_eq!(operations.len(), 2); + let values = CellValues::new(1, 1); + + assert_eq!(operations.len(), 4); assert_eq!( operations, vec![ Operation::SetCellValues { - sheet_pos: SheetPos { - x: 1, - y: 2, - sheet_id - }, - values: CellValues::new(1, 1) + sheet_pos: SheetPos::new(sheet_id, 1, 2), + values: values.clone() + }, + Operation::SetDataTableAt { + sheet_pos: SheetPos::new(sheet_id, 1, 2), + values: values.clone() }, Operation::SetCellValues { - sheet_pos: SheetPos { - x: 2, - y: 2, - sheet_id - }, - values: CellValues::new(1, 1) - } + sheet_pos: SheetPos::new(sheet_id, 2, 2), + values: values.clone() + }, + Operation::SetDataTableAt { + sheet_pos: SheetPos::new(sheet_id, 2, 2), + values: values.clone() + }, ] ); } diff --git a/quadratic-core/src/grid/sheet.rs b/quadratic-core/src/grid/sheet.rs index d8a7a331eb..adefd569cb 100644 --- a/quadratic-core/src/grid/sheet.rs +++ b/quadratic-core/src/grid/sheet.rs @@ -1186,6 +1186,7 @@ mod test { let mut dt = dt.clone(); dt.chart_output = Some((5, 5)); sheet.data_tables.insert(pos, dt); + println!("{:?}", sheet.data_tables); assert!(sheet.has_content(pos)); assert!(sheet.has_content(Pos { x: 1, y: 1 })); assert!(sheet.has_content(Pos { x: 5, y: 1 })); From 9cb35926e78c3df269153fb8775f4eca101178c1 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 22 Nov 2024 16:00:12 -0700 Subject: [PATCH 265/373] Add/remove a column from the UI --- quadratic-client/src/app/actions/actions.ts | 4 ++ .../src/app/actions/dataTableSpec.ts | 49 +++++++++++++++++++ .../HTMLGrid/contextMenus/TableMenu.tsx | 2 + .../src/app/quadratic-core-types/index.d.ts | 2 +- .../quadraticCore/coreClientMessages.ts | 13 +++++ .../quadraticCore/quadraticCore.ts | 23 +++++++++ .../web-workers/quadraticCore/worker/core.ts | 22 +++++++++ .../quadraticCore/worker/coreClient.ts | 13 +++++ .../active_transactions/transaction_name.rs | 1 + .../src/controller/operations/data_table.rs | 42 ++++++++++++++++ .../src/controller/user_actions/data_table.rs | 21 ++++++++ .../wasm_bindings/controller/data_table.rs | 26 ++++++++++ 12 files changed, 217 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 0a0f302467..5425f87480 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -152,6 +152,10 @@ export enum Action { RenameTableColumn = 'rename_table_column', SortTableColumnAscending = 'sort_table_column_ascending', SortTableColumnDescending = 'sort_table_column_descending', + InsertTableColumn = 'insert_table_column', + RemoveTableColumn = 'remove_table_column', + InsertTableRow = 'insert_table_row', + RemoveTableRow = 'remove_table_row', HideTableColumn = 'hide_table_column', ShowAllColumns = 'show_all_columns', EditTableCode = 'edit_table_code', diff --git a/quadratic-client/src/app/actions/dataTableSpec.ts b/quadratic-client/src/app/actions/dataTableSpec.ts index 90873b1ea9..1fa3feee0d 100644 --- a/quadratic-client/src/app/actions/dataTableSpec.ts +++ b/quadratic-client/src/app/actions/dataTableSpec.ts @@ -8,6 +8,7 @@ import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { JsDataTableColumnHeader, JsRenderCodeCell } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { + AddIcon, DeleteIcon, EditIcon, FileRenameIcon, @@ -38,6 +39,10 @@ type DataTableSpec = Pick< | Action.RenameTableColumn | Action.SortTableColumnAscending | Action.SortTableColumnDescending + | Action.InsertTableColumn + | Action.RemoveTableColumn + | Action.InsertTableRow + | Action.RemoveTableRow | Action.HideTableColumn | Action.ShowAllColumns | Action.EditTableCode @@ -248,6 +253,50 @@ export const dataTableSpec: DataTableSpec = { } }, }, + [Action.InsertTableColumn]: { + label: 'Insert column', + Icon: AddIcon, + run: () => { + const table = getTable(); + + if (table) { + let nextColumn = table.columns.length; + + quadraticCore.dataTableMutations( + sheets.sheet.id, + table.x, + table.y, + nextColumn, + undefined, + undefined, + undefined, + sheets.getCursorPosition() + ); + } + }, + }, + [Action.RemoveTableColumn]: { + label: 'Remove column', + Icon: DeleteIcon, + run: () => { + const table = getTable(); + const columns = getDisplayColumns(); + const selectedColumn = pixiAppSettings.contextMenu?.selectedColumn; + + if (table && columns && selectedColumn !== undefined && columns[selectedColumn]) { + quadraticCore.dataTableMutations( + sheets.sheet.id, + table.x, + table.y, + undefined, + selectedColumn, + undefined, + undefined, + sheets.getCursorPosition() + ); + } + }, + }, [Action.HideTableColumn]: { label: 'Hide column', Icon: HideIcon, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx index 8c7a1ac478..350588a818 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/contextMenus/TableMenu.tsx @@ -65,6 +65,8 @@ export const TableMenu = (props: Props) => { {!isImageOrHtmlCell && !spillError && isCodeCell && } {!isImageOrHtmlCell && !spillError && } {} + {!isImageOrHtmlCell && !spillError && } + {!isImageOrHtmlCell && !spillError && } ); }; diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 1ca306d40b..db76e9bc85 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -70,7 +70,7 @@ export interface Span { start: number, end: number, } export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; export type TextMatch = { "Exactly": TextCase } | { "Contains": TextCase } | { "NotContains": TextCase } | { "TextLength": { min: number | null, max: number | null, } }; -export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "SwitchDataTableKind" | "GridToDataTable" | "DataTableMeta" | "DataTableFirstRowAsHeader" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; +export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "SwitchDataTableKind" | "GridToDataTable" | "DataTableMeta" | "DataTableMutations" | "DataTableFirstRowAsHeader" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; export interface TransientResize { row: bigint | null, column: bigint | null, old_size: number, new_size: number, } export interface Validation { id: string, selection: Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } export interface ValidationDateTime { ignore_blank: boolean, require_date: boolean, require_time: boolean, prohibit_date: boolean, prohibit_time: boolean, ranges: Array, } diff --git a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts index 769fe0c220..848ebf6ebe 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/coreClientMessages.ts @@ -1053,6 +1053,18 @@ export interface ClientCoreDataTableMeta { cursor: string; } +export interface ClientCoreDataTableMutations { + type: 'clientCoreDataTableMutations'; + sheetId: string; + x: number; + y: number; + column_to_add?: number; + column_to_remove?: number; + row_to_add?: number; + row_to_remove?: number; + cursor?: string; +} + export interface ClientCoreSortDataTable { type: 'clientCoreSortDataTable'; sheetId: string; @@ -1154,6 +1166,7 @@ export type ClientCoreMessage = | ClientCoreCodeDataTableToDataTable | ClientCoreGridToDataTable | ClientCoreDataTableMeta + | ClientCoreDataTableMutations | ClientCoreSortDataTable | ClientCoreDataTableFirstRowAsHeader; diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index 33d14665a4..e2056e068b 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -1201,6 +1201,29 @@ class QuadraticCore { }); } + dataTableMutations( + sheetId: string, + x: number, + y: number, + column_to_add?: number, + column_to_remove?: number, + row_to_add?: number, + row_to_remove?: number, + cursor?: string + ) { + this.send({ + type: 'clientCoreDataTableMutations', + sheetId, + x, + y, + column_to_add, + column_to_remove, + row_to_add, + row_to_remove, + cursor: cursor || '', + }); + } + sortDataTable( sheetId: string, x: number, diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 16d03a5ea6..ae08c3809e 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -1145,6 +1145,28 @@ class Core { ); } + dataTableMutations( + sheetId: string, + x: number, + y: number, + column_to_add?: number, + column_to_remove?: number, + row_to_add?: number, + row_to_remove?: number, + cursor?: string + ) { + if (!this.gridController) throw new Error('Expected gridController to be defined'); + this.gridController.dataTableMutations( + sheetId, + posToPos(x, y), + column_to_add, + column_to_remove, + row_to_add, + row_to_remove, + cursor + ); + } + sortDataTable( sheetId: string, x: number, diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index 4c3f11378b..72611dda32 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -598,6 +598,19 @@ class CoreClient { ); return; + case 'clientCoreDataTableMutations': + core.dataTableMutations( + e.data.sheetId, + e.data.x, + e.data.y, + e.data.column_to_add, + e.data.column_to_remove, + e.data.row_to_add, + e.data.row_to_remove, + e.data.cursor + ); + return; + case 'clientCoreSortDataTable': core.sortDataTable(e.data.sheetId, e.data.x, e.data.y, e.data.sort, e.data.cursor); return; diff --git a/quadratic-core/src/controller/active_transactions/transaction_name.rs b/quadratic-core/src/controller/active_transactions/transaction_name.rs index 7afb70bcad..3c7334a76f 100644 --- a/quadratic-core/src/controller/active_transactions/transaction_name.rs +++ b/quadratic-core/src/controller/active_transactions/transaction_name.rs @@ -20,6 +20,7 @@ pub enum TransactionName { SwitchDataTableKind, GridToDataTable, DataTableMeta, + DataTableMutations, DataTableFirstRowAsHeader, Import, diff --git a/quadratic-core/src/controller/operations/data_table.rs b/quadratic-core/src/controller/operations/data_table.rs index 14fe0de246..2221727bb8 100644 --- a/quadratic-core/src/controller/operations/data_table.rs +++ b/quadratic-core/src/controller/operations/data_table.rs @@ -64,6 +64,48 @@ impl GridController { }] } + pub fn data_table_mutations_operations( + &self, + sheet_pos: SheetPos, + column_to_add: Option, + column_to_remove: Option, + row_to_add: Option, + row_to_remove: Option, + _cursor: Option, + ) -> Vec { + let mut ops = vec![]; + + if let Some(column_to_add) = column_to_add { + ops.push(Operation::InsertDataTableColumn { + sheet_pos, + index: column_to_add, + }); + } + + if let Some(column_to_remove) = column_to_remove { + ops.push(Operation::DeleteDataTableColumn { + sheet_pos, + index: column_to_remove, + }); + } + + if let Some(row_to_add) = row_to_add { + ops.push(Operation::InsertDataTableRow { + sheet_pos, + index: row_to_add, + }); + } + + if let Some(row_to_remove) = row_to_remove { + ops.push(Operation::DeleteDataTableRow { + sheet_pos, + index: row_to_remove, + }); + } + + ops + } + pub fn sort_data_table_operations( &self, sheet_pos: SheetPos, diff --git a/quadratic-core/src/controller/user_actions/data_table.rs b/quadratic-core/src/controller/user_actions/data_table.rs index 0473ebff99..fedaa10c95 100644 --- a/quadratic-core/src/controller/user_actions/data_table.rs +++ b/quadratic-core/src/controller/user_actions/data_table.rs @@ -68,6 +68,27 @@ impl GridController { self.start_user_transaction(ops, cursor, TransactionName::DataTableMeta); } + pub fn data_table_mutations( + &mut self, + sheet_pos: SheetPos, + column_to_add: Option, + column_to_remove: Option, + row_to_add: Option, + row_to_remove: Option, + cursor: Option, + ) { + let ops = self.data_table_mutations_operations( + sheet_pos, + column_to_add, + column_to_remove, + row_to_add, + row_to_remove, + cursor.to_owned(), + ); + + self.start_user_transaction(ops, cursor, TransactionName::DataTableMutations); + } + pub fn sort_data_table( &mut self, sheet_pos: SheetPos, diff --git a/quadratic-core/src/wasm_bindings/controller/data_table.rs b/quadratic-core/src/wasm_bindings/controller/data_table.rs index c92573a046..f937861bdd 100644 --- a/quadratic-core/src/wasm_bindings/controller/data_table.rs +++ b/quadratic-core/src/wasm_bindings/controller/data_table.rs @@ -125,4 +125,30 @@ impl GridController { Ok(()) } + + #[wasm_bindgen(js_name = "dataTableMutations")] + pub fn js_data_table_mutations( + &mut self, + sheet_id: String, + pos: String, + column_to_add: Option, + column_to_remove: Option, + row_to_add: Option, + row_to_remove: Option, + cursor: Option, + ) -> Result<(), JsValue> { + let pos = serde_json::from_str::(&pos).map_err(|e| e.to_string())?; + let sheet_id = SheetId::from_str(&sheet_id).map_err(|e| e.to_string())?; + + self.data_table_mutations( + pos.to_sheet_pos(sheet_id), + column_to_add, + column_to_remove, + row_to_add, + row_to_remove, + cursor, + ); + + Ok(()) + } } From 666c932ae211644deda0f0809e386d83d85f46f5 Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Fri, 20 Dec 2024 11:04:56 -0700 Subject: [PATCH 266/373] 1st pass at mergeing --- .../workflows/production-publish-images.yml | 60 + .gitignore | 6 + .vscode/settings.json | 21 + Cargo.lock | 1806 +++-- Cargo.toml | 2 +- DEVELOPMENT.md | 4 +- VERSION | 2 +- client.Dockerfile | 53 +- dev/control.js | 3 +- dev/control.ts | 3 +- dev/help.js | 4 +- dev/help.ts | 4 +- dev/ui.js | 2 +- dev/ui.ts | 2 +- docker-compose.base.yml | 1 - docker-compose.yml | 86 +- docker/ory-auth/config/identity.schema.json | 47 + docker/ory-auth/config/kratos.yml | 133 + docker/postgres/scripts/init.sh | 18 + .../data/cache/machine.json | 1 + .../data/cache/server.test.pem | 169 + .../data/cache/server.test.pem.crt | 141 + .../data/cache/server.test.pem.key | 28 + .../service-catalog-3_7_1_dev1-1_35_5.pickle | Bin 0 -> 635600 bytes package-lock.json | 6197 ++++++++++++++--- package.json | 3 +- quadratic-api/.env.docker | 18 +- quadratic-api/.env.example | 34 +- quadratic-api/.env.test | 26 +- quadratic-api/Dockerfile | 1 + quadratic-api/jest.setup.js | 27 +- quadratic-api/package.json | 7 +- quadratic-api/prisma/schema.prisma | 2 +- quadratic-api/src/app.ts | 2 + quadratic-api/src/auth/auth.ts | 53 + .../src/{auth0/profile.ts => auth/auth0.ts} | 50 +- quadratic-api/src/auth/ory.ts | 85 + quadratic-api/src/aws/s3.ts | 52 - quadratic-api/src/env-vars.ts | 30 +- quadratic-api/src/internal/addUserToTeam.ts | 4 + .../src/internal/removeUserFromTeam.ts | 4 + quadratic-api/src/licenseClient.ts | 80 + quadratic-api/src/middleware/user.ts | 5 +- .../src/middleware/validateAccessToken.ts | 17 +- quadratic-api/src/routes/ai/aiRateLimiter.ts | 13 + quadratic-api/src/routes/ai/anthropic.ts | 70 +- quadratic-api/src/routes/ai/bedrock.ts | 191 + quadratic-api/src/routes/ai/openai.ts | 59 +- quadratic-api/src/routes/v0/education.POST.ts | 4 +- quadratic-api/src/routes/v0/examples.POST.ts | 14 +- .../src/routes/v0/files.$uuid.GET.ts | 20 +- .../src/routes/v0/files.$uuid.invites.POST.ts | 6 +- .../src/routes/v0/files.$uuid.sharing.GET.ts | 4 +- .../routes/v0/files.$uuid.thumbnail.POST.ts | 23 +- quadratic-api/src/routes/v0/files.GET.ts | 4 +- quadratic-api/src/routes/v0/files.POST.ts | 8 +- .../src/routes/v0/teams.$uuid.GET.ts | 50 +- .../src/routes/v0/teams.$uuid.invites.POST.ts | 8 +- quadratic-api/src/server.ts | 3 +- quadratic-api/src/storage/fileSystem.ts | 114 + quadratic-api/src/storage/s3.ts | 89 + quadratic-api/src/storage/storage.ts | 57 + quadratic-api/src/utils/createFile.ts | 6 +- quadratic-api/src/utils/crypto.test.ts | 9 +- quadratic-api/src/utils/crypto.ts | 7 + quadratic-client/.env.docker | 5 + quadratic-client/.env.example | 11 +- quadratic-client/.env.test | 9 + quadratic-client/Dockerfile | 83 +- quadratic-client/index.html | 4 +- quadratic-client/package.json | 23 +- quadratic-client/public/.well-known/jwks.json | 26 + quadratic-client/src/app/actions.ts | 25 +- quadratic-client/src/app/actions/actions.ts | 8 +- .../src/app/actions/columnRowSpec.ts | 41 +- .../src/app/actions/editActionsSpec.ts | 52 +- .../src/app/actions/formatActionsSpec.ts | 4 +- .../src/app/actions/insertActionsSpec.ts | 33 +- .../src/app/actions/selectionActionsSpec.ts | 65 +- .../src/app/actions/viewActionsSpec.ts | 26 +- .../app/ai/components/SelectAIModelMenu.tsx | 67 + .../src/app/ai/docs/ConnectionDocs.ts | 79 + .../src/app/ai/docs/FormulaDocs.ts | 1093 +++ .../src/app/ai/docs/JavascriptDocs.ts | 389 ++ .../src/app/ai/docs/PythonDocs.ts | 663 ++ .../src/app/ai/docs/QuadraticDocs.ts | 8 + .../src/app/ai/hooks/useAIModel.tsx | 23 + .../src/app/ai/hooks/useAIRequestToAPI.tsx | 528 ++ .../ai/hooks/useCodeCellContextMessages.tsx | 98 + .../hooks/useCurrentSheetContextMessages.tsx | 77 + .../src/app/ai/hooks/useGetChatName.tsx | 60 + .../hooks/useOtherSheetsContextMessages.tsx | 83 + .../ai/hooks/useQuadraticContextMessages.tsx | 45 + .../ai/hooks/useSelectionContextMessages.tsx | 65 + .../src/app/ai/hooks/useToolUseMessages.tsx | 38 + .../ai/hooks/useVisibleContextMessages.tsx | 106 + .../src/app/ai/offline/aiAnalystChats.test.ts | 173 + .../src/app/ai/offline/aiAnalystChats.ts | 158 + quadratic-client/src/app/ai/tools/aiTools.ts | 7 + .../src/app/ai/tools/aiToolsSpec.ts | 334 + .../src/app/ai/tools/endpoint.helper.ts | 19 + .../src/app/ai/tools/message.helper.ts | 206 + .../src/app/ai/tools/model.helper.ts | 24 + .../src/app/ai/tools/tool.helpers.ts | 82 + .../src/app/atoms/aiAnalystAtom.ts | 287 + .../src/app/atoms/codeEditorAtom.ts | 187 +- .../src/app/atoms/codeHintAtom.ts | 5 +- .../app/atoms/editorInteractionStateAtom.ts | 13 +- .../src/app/atoms/gridSettingsAtom.ts | 7 +- .../src/app/atoms/inlineEditorAtom.ts | 11 +- quadratic-client/src/app/bigint.ts | 10 + quadratic-client/src/app/debugFlags.ts | 6 + quadratic-client/src/app/events/events.ts | 15 +- .../app/grid/actions/clipboard/clipboard.ts | 32 +- .../src/app/grid/actions/openCodeEditor.ts | 5 +- .../grid/computations/formulas/runFormula.ts | 17 - .../src/app/grid/computations/types.ts | 16 - .../src/app/grid/controller/Sheets.ts | 126 +- quadratic-client/src/app/grid/sheet/Bounds.ts | 6 +- .../src/app/grid/sheet/GridOverflowLines.ts | 6 +- quadratic-client/src/app/grid/sheet/Sheet.ts | 66 +- .../src/app/grid/sheet/SheetCursor.ts | 406 +- .../app/grid/sheet/SheetCursorUtils.test.ts | 117 - .../src/app/grid/sheet/selection.test.ts | 135 - .../src/app/grid/sheet/selection.ts | 221 +- .../src/app/grid/sheet/sheetCursorUtils.ts | 73 - .../src/app/gridGL/HTMLGrid/CodeHint.tsx | 2 +- .../app/gridGL/HTMLGrid/GridContextMenu.tsx | 127 + .../app/gridGL/HTMLGrid/HTMLGridContainer.tsx | 15 +- .../gridGL/HTMLGrid/SuggestionDropdown.tsx | 60 +- .../HTMLGrid/annotations/Annotations.tsx | 3 +- .../HTMLGrid/annotations/CalendarPicker.tsx | 4 +- .../askAISelection/AskAISelection.tsx | 177 + .../HTMLGrid/codeRunning/CodeRunning.tsx | 8 +- .../gridGL/HTMLGrid/hoverCell/HoverCell.css | 43 - .../gridGL/HTMLGrid/hoverCell/HoverCell.tsx | 279 +- .../HTMLGrid/hoverTooltip/HoverTooltip.tsx | 6 +- .../app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts | 2 +- .../HTMLGrid/inlineEditor/InlineEditor.tsx | 36 +- .../inlineEditor/inlineEditorFormula.ts | 45 +- .../inlineEditor/inlineEditorHandler.ts | 140 +- .../inlineEditor/inlineEditorKeyboard.ts | 104 +- .../inlineEditor/inlineEditorMonaco.ts | 81 +- .../multiplayerCursor/MultiplayerCursors.tsx | 6 +- .../multiplayerInput/MultiplayerCellEdit.tsx | 2 +- .../multiplayerInput/MultiplayerCellEdits.tsx | 5 +- .../gridGL/HTMLGrid/usePositionCellMessage.ts | 28 +- .../validations/HtmlValidationCheckbox.tsx | 4 +- .../validations/HtmlValidationList.tsx | 28 +- .../validations/translateValidationError.tsx | 4 +- .../validations/useHtmlValidations.ts | 13 +- .../src/app/gridGL/PixiAppEffects.tsx | 8 + .../src/app/gridGL/QuadraticGrid.tsx | 1 + .../src/app/gridGL/UI/AxesLines.ts | 33 - .../src/app/gridGL/UI/Background.ts | 37 + quadratic-client/src/app/gridGL/UI/Cursor.ts | 167 +- .../src/app/gridGL/UI/GridLines.ts | 38 +- .../src/app/gridGL/UI/UICellImages.ts | 10 + .../src/app/gridGL/UI/UIMultiplayerCursor.ts | 38 +- .../src/app/gridGL/UI/UIValidations.ts | 153 +- .../src/app/gridGL/UI/boxCells.ts | 14 +- .../UI/cellHighlights/CellHighlights.ts | 192 +- .../UI/cellHighlights/cellHighlightsDraw.ts | 104 +- .../src/app/gridGL/UI/drawCursor.ts | 257 +- .../gridGL/UI/gridHeadings/GridHeadings.ts | 225 +- .../UI/gridHeadings/getA1Notation.test.ts | 50 + .../gridGL/UI/gridHeadings/getA1Notation.ts | 21 +- .../gridHeadings/tests/getA1Notation.test.ts | 78 - .../src/app/gridGL/cells/Borders.ts | 217 + .../src/app/gridGL/cells/CellsArray.ts | 285 + .../src/app/gridGL/cells/CellsFills.ts | 138 +- .../src/app/gridGL/cells/CellsSearch.ts | 9 +- .../src/app/gridGL/cells/CellsSheet.ts | 28 + .../src/app/gridGL/cells/CellsSheets.ts | 19 +- .../src/app/gridGL/cells/borders/Borders.ts | 411 -- .../gridGL/cells/borders/bordersUtil.test.ts | 250 - .../app/gridGL/cells/borders/bordersUtil.ts | 91 - .../gridGL/cells/cellsImages/CellsImages.ts | 11 +- .../gridGL/cells/cellsLabel/CellsLabels.ts | 27 +- .../src/app/gridGL/helpers/selectCells.ts | 92 - .../gridGL/helpers/selectCells.ts.deprecated | 92 + .../src/app/gridGL/helpers/selection.ts | 48 + .../src/app/gridGL/helpers/zoom.ts | 130 +- .../interaction/keyboard/keyboardCell.ts | 93 +- .../interaction/keyboard/keyboardCode.ts | 8 +- .../interaction/keyboard/keyboardPosition.ts | 149 +- .../interaction/keyboard/keyboardSearch.ts | 7 +- .../interaction/keyboard/keyboardSelect.ts | 33 +- .../interaction/keyboard/keyboardViewport.ts | 155 +- .../interaction/keyboard/useKeyboard.ts | 4 +- .../pointer/PointerAutoComplete.ts | 84 +- .../interaction/pointer/PointerCellMoving.ts | 52 +- .../gridGL/interaction/pointer/PointerDown.ts | 201 +- .../interaction/pointer/PointerHeading.ts | 29 +- .../interaction/pointer/PointerHtmlCells.ts | 19 +- .../gridGL/interaction/pointer/PointerLink.ts | 3 +- .../interaction/pointer/doubleClickCell.ts | 17 +- .../app/gridGL/interaction/viewportHelper.ts | 100 +- .../gridGL/pixiApp/MomentumScrollDetector.ts | 58 + .../src/app/gridGL/pixiApp/PixiApp.ts | 75 +- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 55 +- .../src/app/gridGL/pixiApp/Update.ts | 30 +- .../src/app/gridGL/pixiApp/copyAsPNG.ts | 61 +- .../src/app/gridGL/pixiApp/messages.tsx | 20 + .../gridGL/pixiApp/urlParams/UrlParamsDev.ts | 11 +- .../gridGL/pixiApp/urlParams/UrlParamsUser.ts | 10 +- .../app/gridGL/pixiApp/viewport/Decelerate.ts | 286 + .../src/app/gridGL/pixiApp/viewport/Drag.ts | 478 ++ .../app/gridGL/pixiApp/viewport/Viewport.ts | 244 + .../viewport}/Wheel.ts | 94 +- .../src/app/gridGL/types/codeCell.ts | 5 +- .../src/app/gridGL/types/links.ts | 4 +- quadratic-client/src/app/gridGL/types/size.ts | 22 +- .../src/app/helpers/codeCellLanguage.ts | 10 +- .../src/app/helpers/formulaNotation.ts | 106 +- quadratic-client/src/app/keyboard/defaults.ts | 14 +- .../src/app/quadratic-core-types/index.d.ts | 53 +- quadratic-client/src/app/theme/colors.ts | 1 + .../src/app/ui/QuadraticSidebar.tsx | 28 + quadratic-client/src/app/ui/QuadraticUI.tsx | 47 +- .../app/ui/components/AIUserMessageForm.tsx | 210 + .../src/app/ui/components/BorderMenu.tsx | 4 +- .../src/app/ui/components/CodeSnippet.tsx | 118 - .../ui/components/CurrentSheetNameDisplay.tsx | 20 + .../ui/components/CursorSelectionDisplay.tsx | 24 + .../src/app/ui/components/DateFormat.tsx | 16 +- .../app/ui/components/FileDragDropWrapper.tsx | 15 +- .../src/app/ui/components/FixSpillError.tsx | 99 + .../src/app/ui/components/Following.tsx | 6 +- .../src/app/ui/components/ImportProgress.tsx | 2 +- .../src/app/ui/components/LanguageIcon.tsx | 22 + .../src/app/ui/components/Markdown.scss | 21 + .../src/app/ui/components/Markdown.tsx | 7 +- .../src/app/ui/components/ResizeControl.css | 2 +- .../src/app/ui/components/Search.tsx | 63 +- .../src/app/ui/components/SheetRange.tsx | 110 +- .../src/app/ui/components/TooltipHint.tsx | 26 - .../src/app/ui/components/TopBarLoading.tsx | 1 + .../src/app/ui/components/qColorPicker.scss | 33 + .../src/app/ui/components/qColorPicker.tsx | 3 +- .../src/app/ui/helpers/formatCells.ts | 70 +- .../src/app/ui/hooks/useBorders.tsx | 27 +- .../src/app/ui/hooks/useFileImport.tsx | 28 +- quadratic-client/src/app/ui/icons/index.tsx | 105 +- .../src/app/ui/loading/QuadraticLoading.tsx | 3 +- .../src/app/ui/menus/AIAnalyst/AIAnalyst.tsx | 89 + .../menus/AIAnalyst/AIAnalystChatHistory.tsx | 188 + .../ui/menus/AIAnalyst/AIAnalystContext.tsx | 183 + .../ui/menus/AIAnalyst/AIAnalystEffects.tsx | 32 + .../AIAnalyst/AIAnalystExamplePrompts.tsx | 93 + .../ui/menus/AIAnalyst/AIAnalystHeader.tsx | 90 + .../ui/menus/AIAnalyst/AIAnalystMessages.tsx | 157 + .../AIAnalyst/AIAnalystSelectContextMenu.tsx | 91 + .../ui/menus/AIAnalyst/AIAnalystToolCard.tsx | 30 + .../AIAnalyst/AIAnalystUserMessageForm.tsx | 53 + .../const/defaultAIAnalystContext.ts | 7 + .../app/ui/menus/AIAnalyst/const/maxRects.ts | 1 + .../hooks/useAIAnalystPanelWidth.tsx | 16 + .../hooks/useSubmitAIAnalystPrompt.tsx | 227 + .../menus/AIAnalyst/toolCards/DeleteCells.tsx | 46 + .../menus/AIAnalyst/toolCards/MoveCells.tsx | 52 + .../AIAnalyst/toolCards/SetCellValues.tsx | 51 + .../AIAnalyst/toolCards/SetCodeCellValue.tsx | 177 + .../ui/menus/AIAnalyst/toolCards/ToolCard.tsx | 60 + .../app/ui/menus/AIAssistant/AIAssistant.css | 8 - .../app/ui/menus/AIAssistant/AIAssistant.tsx | 408 -- .../src/app/ui/menus/AIAssistant/MODELS.ts | 36 - .../menus/AIAssistant/useAIAssistantModel.tsx | 7 - .../menus/AIAssistant/useAIRequestToAPI.tsx | 190 - .../src/app/ui/menus/BottomBar/BottomBar.tsx | 12 + .../src/app/ui/menus/BottomBar/KernelMenu.tsx | 20 +- .../ui/menus/BottomBar/SelectionSummary.tsx | 163 +- .../ui/menus/CellTypeMenu/CellTypeMenu.tsx | 2 +- .../CodeEditor/AIAssistant/AIAssistant.tsx | 20 + .../AIAssistant/AIAssistantMessages.tsx | 131 + .../AIAssistantUserMessageForm.tsx | 27 + .../AIAssistant/AICodeBlockParser.test.ts | 0 .../AIAssistant/AICodeBlockParser.tsx | 4 +- .../CodeEditor/AIAssistant/CodeSnippet.tsx | 224 + .../app/ui/menus/CodeEditor/CodeEditor.css | 4 + .../app/ui/menus/CodeEditor/CodeEditor.tsx | 114 +- .../ui/menus/CodeEditor/CodeEditorBody.tsx | 164 +- .../CodeEditor/CodeEditorDiffButtons.tsx | 82 + .../ui/menus/CodeEditor/CodeEditorEffects.tsx | 5 +- .../menus/CodeEditor/CodeEditorEmptyState.tsx | 11 +- .../ui/menus/CodeEditor/CodeEditorHeader.tsx | 115 +- .../menus/CodeEditor/CodeEditorRefButton.tsx | 93 +- .../src/app/ui/menus/CodeEditor/Console.tsx | 13 +- .../menus/CodeEditor/FormulaLanguageModel.ts | 2 +- .../app/ui/menus/CodeEditor/QuadraticDocs.ts | 1580 ----- .../menus/CodeEditor/ReturnTypeInspector.tsx | 65 +- .../ui/menus/CodeEditor/SaveChangesAlert.tsx | 11 +- .../ui/menus/CodeEditor/SnippetsPopover.tsx | 48 +- .../CodeEditor/hooks/parseCellsAccessed.ts} | 4 +- .../hooks/useEditorCellHighlights.ts | 134 +- .../hooks/useEditorOnSelectionChange.ts | 45 - .../CodeEditor/hooks/useSaveAndRunCell.tsx | 10 +- .../hooks/useSubmitAIAssistantPrompt.tsx | 120 + .../CodeEditor/hooks/useUpdateCodeEditor.tsx | 28 +- .../app/ui/menus/CodeEditor/insertCellRef.ts | 112 +- .../CodeEditor/panels/CodeEditorPanel.tsx | 27 +- .../panels/CodeEditorPanelBottom.tsx | 38 +- .../CodeEditor/panels/CodeEditorPanelSide.tsx | 5 +- .../panels/CodeEditorPanelsResize.tsx | 4 +- .../ui/menus/CodeEditor/panels/PanelBox.tsx | 2 +- .../src/app/ui/menus/CodeEditor/snippetsJS.ts | 27 +- .../src/app/ui/menus/CodeEditor/snippetsPY.ts | 26 +- .../ui/menus/CommandPalette/commands/Code.tsx | 4 +- .../CommandPalette/commands/ColumnRow.tsx | 2 +- .../ui/menus/CommandPalette/commands/File.tsx | 18 +- .../menus/CommandPalette/commands/Format.tsx | 30 +- .../ui/menus/CommandPalette/commands/View.tsx | 21 +- .../src/app/ui/menus/GoTo/GoTo.tsx | 97 +- .../GoTo/getCoordinatesFromUserInput.test.ts | 112 - .../menus/GoTo/getCoordinatesFromUserInput.ts | 49 - .../src/app/ui/menus/SheetBar/SheetBar.tsx | 17 +- .../src/app/ui/menus/SheetBar/SheetBarTab.tsx | 16 +- .../SheetBar/SheetBarTabDropdownMenu.tsx | 6 +- .../app/ui/menus/Toolbar/CursorPosition.tsx | 32 +- .../app/ui/menus/Toolbar/FormattingBar.tsx | 2 +- .../src/app/ui/menus/Toolbar/Toolbar.tsx | 2 +- .../src/app/ui/menus/Toolbar/ZoomMenu.tsx | 6 + .../TopBar/TopBarFileNameAndLocationMenu.tsx | 2 +- .../TopBar/TopBarMenus/EditMenubarMenu.tsx | 12 +- .../TopBar/TopBarMenus/FileMenubarMenu.tsx | 23 +- .../TopBar/TopBarMenus/MenubarItemAction.tsx | 7 +- .../menus/TopBar/TopBarMenus/TopBarMenus.tsx | 9 +- .../TopBar/TopBarMenus/ViewMenubarMenu.tsx | 6 +- .../Validations/Validation/Validation.tsx | 3 +- .../Validation/ValidationHeader.tsx | 31 +- .../Validations/Validation/ValidationList.tsx | 18 +- .../ValidationUI/ValidationInput.tsx | 11 +- .../Validation/useValidationData.ts | 58 +- .../Validations/ValidationEntry.tsx | 21 +- .../Validations/Validations/Validations.tsx | 8 +- .../Validations/ValidationsHeader.tsx | 15 +- .../javascriptClientMessages.ts | 26 +- .../javascriptCoreMessages.ts | 27 +- .../javascriptWebWorker.ts | 13 +- .../javascript/getJavascriptFetchOverride.ts | 28 + .../javascript/getJavascriptXHROverride.ts | 50 + .../worker/javascript/javascript.ts | 14 +- .../worker/javascript/javascriptAPI.ts | 51 +- .../worker/javascript/javascriptCompile.ts | 14 +- .../worker/javascript/javascriptOutput.ts | 8 +- .../javascript/javascriptRunnerMessages.ts | 12 +- .../runner/generateJavascriptForRunner.ts | 2 +- .../runner/generatedJavascriptForEditor.ts | 405 +- .../javascript/runner/javascriptLibrary.ts | 405 +- .../worker/javascriptClient.ts | 30 +- .../worker/javascriptCore.ts | 34 +- .../src/app/web-workers/monacoInit.ts | 3 + .../multiplayerWebWorker/multiplayer.ts | 21 +- .../multiplayerClientMessages.ts | 2 +- .../multiplayerWebWorker/multiplayerTypes.ts | 6 +- .../worker/multiplayerServer.ts | 2 +- .../pythonLanguageServer/client.ts | 11 +- .../pyright-initialization.json | 2 +- .../pythonWebWorker/pythonCoreMessages.ts | 16 +- .../pythonWebWorker/pythonWebWorker.ts | 2 +- .../pythonWebWorker/worker/python.test.ts | 1 - .../pythonWebWorker/worker/python.ts | 77 +- .../pythonWebWorker/worker/pythonClient.ts | 2 +- .../pythonWebWorker/worker/pythonCore.ts | 22 +- .../quadraticCore/coreClientMessages.ts | 298 +- .../quadraticCore/quadraticCore.ts | 335 +- .../web-workers/quadraticCore/worker/core.ts | 420 +- .../quadraticCore/worker/coreClient.ts | 156 +- .../quadraticCore/worker/coreConnection.ts | 2 +- .../quadraticCore/worker/coreJavascript.ts | 65 +- .../quadraticCore/worker/corePython.ts | 49 +- .../quadraticCore/worker/offline.ts | 10 +- .../quadraticCore/worker/rustCallbacks.ts | 11 +- .../quadraticCore/worker/rustConversions.ts | 9 +- .../renderWebWorker/renderClientMessages.ts | 9 +- .../renderWebWorker/renderWebWorker.ts | 2 +- .../worker/cellsLabel/CellLabel.ts | 27 +- .../worker/cellsLabel/CellsLabels.ts | 22 +- .../worker/cellsLabel/CellsTextHash.ts | 159 +- .../worker/cellsLabel/CellsTextHashSpecial.ts | 18 +- .../worker/cellsLabel/convertNumber.ts | 8 +- .../renderWebWorker/worker/renderClient.ts | 15 +- quadratic-client/src/auth.ts | 188 - quadratic-client/src/auth/auth.test.ts | 15 + quadratic-client/src/auth/auth.ts | 129 + quadratic-client/src/auth/auth0.ts | 97 + quadratic-client/src/auth/ory.ts | 128 + .../src/dashboard/atoms/newFileDialogAtom.ts | 16 - .../dashboard/components/DashboardSidebar.tsx | 71 +- .../dashboard/components/EducationDialog.tsx | 2 +- .../src/dashboard/components/FileDragDrop.tsx | 9 +- .../src/dashboard/components/FilesList.tsx | 37 +- .../components/FilesListEmptyState.tsx | 18 +- .../dashboard/components/FilesListItem.tsx | 12 +- .../components/FilesListItemCore.tsx | 37 +- .../components/FilesListViewControls.tsx | 6 +- .../dashboard/components/NewFileButton.tsx | 150 +- .../dashboard/components/NewFileDialog.tsx | 243 - .../dashboard/components/OnboardingBanner.tsx | 24 +- .../src/dashboard/components/TeamSwitcher.tsx | 69 +- ...onnectionSchemaBrowserTableQueryAction.tsx | 40 +- quadratic-client/src/index.css | 1 - quadratic-client/src/index.tsx | 6 +- quadratic-client/src/router.tsx | 2 +- quadratic-client/src/routes/_dashboard.tsx | 60 +- quadratic-client/src/routes/_root.tsx | 3 +- .../src/routes/api.files.$uuid.ts | 10 +- quadratic-client/src/routes/file.$uuid.tsx | 31 +- quadratic-client/src/routes/login-result.tsx | 2 +- quadratic-client/src/routes/login.tsx | 2 +- quadratic-client/src/routes/logout.tsx | 2 +- .../routes/teams.$teamUuid.files.create.tsx | 2 +- .../routes/teams.$teamUuid.files.private.tsx | 7 +- .../src/routes/teams.$teamUuid.index.tsx | 8 +- quadratic-client/src/shared/api/apiClient.ts | 35 +- .../src/shared/api/connectionClient.ts | 2 +- .../src/shared/api/fetchFromApi.ts | 3 +- quadratic-client/src/shared/api/xhrFromApi.ts | 2 +- .../components/GlobalSnackbarProvider.tsx | 50 +- .../src/shared/components/Icons.tsx | 121 +- .../src/shared/components/ShareDialog.tsx | 22 + .../shared/components/SlideUpBottomAlert.tsx | 2 +- .../connections/ConnectionSchemaBrowser.tsx | 99 +- .../src/shared/constants/gridConstants.ts | 2 - .../src/shared/constants/routes.ts | 14 +- quadratic-client/src/shared/constants/urls.ts | 5 + .../shared/hooks/useThemeAppearanceMode.tsx | 27 +- quadratic-client/src/shared/shadcn/styles.css | 38 +- .../src/shared/shadcn/ui/menubar.tsx | 42 +- .../src/shared/shadcn/ui/textarea.tsx | 32 +- .../src/shared/shadcn/ui/tooltip.tsx | 6 +- .../src/shared/utils/analytics.ts | 4 +- quadratic-client/src/shared/utils/colors.ts | 15 + quadratic-client/src/shared/utils/userUtil.ts | 4 +- quadratic-connection/.env.docker | 4 +- quadratic-connection/Cargo.toml | 2 +- quadratic-connection/src/error.rs | 13 +- quadratic-connection/src/proxy.rs | 22 +- quadratic-connection/src/server.rs | 36 +- quadratic-core/.vscode/settings.json | 2 + quadratic-core/Cargo.toml | 2 +- quadratic-core/benches/grid_benchmark.rs | 78 +- .../proptest-regressions/a1/cell_ref.txt | 8 + .../a1/cell_ref_range.txt | 8 + .../proptest-regressions/a1/selection.txt | 11 + .../proptest-regressions/a1/subspaces.txt | 9 + .../proptest-regressions/grid/contiguous.txt | 7 + .../grid/contiguous/contiguous_2d.txt | 7 + .../grid/contiguous/contiguous_blocks.txt | 13 + .../grid/sheet/formats/mod.txt | 7 + quadratic-core/proptest-regressions/rect.txt | 8 + .../src/controller/operations/code_cell.rs | 1 + .../src/grid/file/sheet_schema.rs | 1 + .../quadratic-core/src/grid/file/v1_7/file.rs | 1 + .../a1/a1_selection/a1_selection_exclude.rs | 576 ++ .../a1/a1_selection/a1_selection_mutate.rs | 429 ++ .../src/a1/a1_selection/a1_selection_query.rs | 483 ++ .../a1/a1_selection/a1_selection_select.rs | 768 ++ quadratic-core/src/a1/a1_selection/mod.rs | 1012 +++ quadratic-core/src/a1/a1_sheet_name.rs | 181 + quadratic-core/src/a1/cell_ref_coord.rs | 226 + quadratic-core/src/a1/cell_ref_end.rs | 387 + .../src/a1/cell_ref_range/cell_ref_col_row.rs | 276 + .../src/a1/cell_ref_range/cell_ref_query.rs | 122 + quadratic-core/src/a1/cell_ref_range/mod.rs | 574 ++ quadratic-core/src/a1/column_names.rs | 91 + quadratic-core/src/a1/error.rs | 51 + quadratic-core/src/a1/js_selection.rs | 380 + quadratic-core/src/a1/mod.rs | 39 + quadratic-core/src/a1/ref_range_bounds/mod.rs | 196 + .../ref_range_bounds_contains.rs | 107 + .../ref_range_bounds_create.rs | 187 + .../ref_range_bounds_intersection.rs | 316 + .../ref_range_bounds_query.rs | 499 ++ .../ref_range_bounds_translate.rs | 162 + quadratic-core/src/a1/sheet_cell_ref_range.rs | 47 + quadratic-core/src/bin/export_types.rs | 108 +- quadratic-core/src/clear_option.rs | 132 + .../pending_transaction.rs | 199 +- quadratic-core/src/controller/dependencies.rs | 43 +- .../execution/auto_resize_row_heights.rs | 279 +- .../execution/control_transaction.rs | 33 +- .../execute_operation/execute_borders.rs | 171 +- .../execute_operation/execute_borders_old.rs | 111 + .../execute_operation/execute_code.rs | 66 +- .../execute_operation/execute_col_rows.rs | 509 +- .../execute_operation/execute_cursor.rs | 122 +- .../execute_operation/execute_data_table.rs | 10 +- .../execute_operation/execute_formats.rs | 261 +- .../execute_operation/execute_formats_old.rs | 188 + .../execute_operation/execute_move_cells.rs | 11 +- .../execute_operation/execute_sheets.rs | 36 +- .../execute_operation/execute_validation.rs | 21 +- .../execute_operation/execute_values.rs | 5 +- .../execution/execute_operation/mod.rs | 57 +- .../src/controller/execution/mod.rs | 11 +- .../execution/receive_multiplayer.rs | 145 +- .../execution/run_code/get_cells.rs | 410 +- .../src/controller/execution/run_code/mod.rs | 39 +- .../execution/run_code/run_connection.rs | 258 +- .../execution/run_code/run_formula.rs | 170 +- .../execution/run_code/run_javascript.rs | 321 +- .../execution/run_code/run_python.rs | 247 +- .../src/controller/execution/spills.rs | 140 +- quadratic-core/src/controller/export.rs | 39 +- quadratic-core/src/controller/formula.rs | 75 +- quadratic-core/src/controller/mod.rs | 3 +- .../src/controller/operations/autocomplete.rs | 615 +- .../src/controller/operations/borders.rs | 1166 ++-- .../src/controller/operations/cell_value.rs | 93 +- .../src/controller/operations/clipboard.rs | 436 +- .../src/controller/operations/code_cell.rs | 58 +- .../src/controller/operations/formats.rs | 96 +- .../src/controller/operations/formatting.rs | 13 +- .../src/controller/operations/import.rs | 81 +- .../src/controller/operations/mod.rs | 8 +- .../src/controller/operations/operation.rs | 182 +- .../src/controller/operations/sheets.rs | 4 +- quadratic-core/src/controller/send_render.rs | 124 +- quadratic-core/src/controller/thumbnail.rs | 375 +- quadratic-core/src/controller/transaction.rs | 12 +- .../controller/user_actions/auto_complete.rs | 247 +- .../src/controller/user_actions/borders.rs | 175 +- .../src/controller/user_actions/cells.rs | 84 +- .../src/controller/user_actions/clipboard.rs | 723 +- .../src/controller/user_actions/col_row.rs | 194 +- .../src/controller/user_actions/data_table.rs | 8 +- .../src/controller/user_actions/formats.rs | 881 ++- .../src/controller/user_actions/import.rs | 46 +- .../src/controller/user_actions/mod.rs | 1 - .../src/controller/user_actions/sheets.rs | 65 +- .../src/controller/user_actions/undo.rs | 6 +- .../controller/user_actions/validations.rs | 57 +- quadratic-core/src/copy_formats.rs | 10 + quadratic-core/src/date_time.rs | 18 +- quadratic-core/src/error_core.rs | 11 + quadratic-core/src/error_run.rs | 4 +- quadratic-core/src/formulas/ast.rs | 31 +- quadratic-core/src/formulas/cell_ref.rs | 133 +- quadratic-core/src/formulas/ctx.rs | 60 +- .../src/formulas/functions/array.rs | 4 +- .../src/formulas/functions/logic.rs | 7 +- .../src/formulas/functions/lookup.rs | 61 +- .../src/formulas/functions/mathematics.rs | 12 +- .../src/formulas/functions/operators.rs | 2 +- .../src/formulas/functions/statistics.rs | 34 +- quadratic-core/src/formulas/lexer.rs | 20 +- quadratic-core/src/formulas/mod.rs | 5 +- quadratic-core/src/formulas/parser/mod.rs | 32 +- quadratic-core/src/formulas/tests.rs | 52 +- quadratic-core/src/grid/block/mod.rs | 2 + quadratic-core/src/grid/borders/cell.rs | 51 - quadratic-core/src/grid/borders/mod.rs | 6 - quadratic-core/src/grid/borders/sheet.rs | 80 - quadratic-core/src/grid/bounds.rs | 2 + quadratic-core/src/{ => grid}/cell.rs | 0 quadratic-core/src/grid/cells_accessed.rs | 265 + quadratic-core/src/grid/code_cell.rs | 210 + quadratic-core/src/grid/code_run.rs | 99 +- quadratic-core/src/grid/column.rs | 263 +- quadratic-core/src/grid/contiguous/block.rs | 195 + .../src/grid/contiguous/contiguous_2d.rs | 982 +++ .../src/grid/contiguous/contiguous_blocks.rs | 769 ++ quadratic-core/src/grid/contiguous/mod.rs | 7 + quadratic-core/src/grid/data_table/mod.rs | 16 +- .../src/grid/data_table/table_formats.rs | 102 +- .../grid/file/migrate_code_cell_references.rs | 829 +++ quadratic-core/src/grid/file/mod.rs | 339 +- .../src/grid/file/serialize/borders.rs | 219 +- .../src/grid/file/serialize/cell_value.rs | 18 +- .../src/grid/file/serialize/code_cell.rs | 401 ++ .../src/grid/file/serialize/column.rs | 374 +- .../src/grid/file/serialize/contiguous_2d.rs | 59 + .../src/grid/file/serialize/data_table.rs | 167 +- .../src/grid/file/serialize/format.rs | 161 - .../src/grid/file/serialize/formats.rs | 146 + quadratic-core/src/grid/file/serialize/mod.rs | 8 +- .../src/grid/file/serialize/row_resizes.rs | 34 + .../src/grid/file/serialize/selection.rs | 115 +- .../src/grid/file/serialize/sheets.rs | 24 +- .../src/grid/file/serialize/validations.rs | 52 +- quadratic-core/src/grid/file/sheet_schema.rs | 15 +- .../src/grid/file/shift_negative_offsets.rs | 175 + quadratic-core/src/grid/file/v1_4/file.rs | 7 +- .../src/grid/file/v1_6/borders_upgrade.rs | 103 + quadratic-core/src/grid/file/v1_6/file.rs | 41 +- quadratic-core/src/grid/file/v1_6/mod.rs | 1 + quadratic-core/src/grid/file/v1_6/schema.rs | 2 +- .../src/grid/file/v1_7/borders_a1_upgrade.rs | 48 + .../grid/file/v1_7/contiguous_2d_upgrade.rs | 571 ++ quadratic-core/src/grid/file/v1_7/file.rs | 108 +- quadratic-core/src/grid/file/v1_7/mod.rs | 8 + quadratic-core/src/grid/file/v1_7/schema.rs | 9 +- .../file/v1_7/sheet_formatting_upgrade.rs | 106 + quadratic-core/src/grid/file/v1_7/upgrade.rs | 664 ++ .../grid/file/v1_7_1/a1_selection_schema.rs | 42 + .../src/grid/file/v1_7_1/borders_a1_schema.rs | 13 + .../grid/file/v1_7_1/cells_accessed_schema.rs | 38 + .../grid/file/v1_7_1/contiguous_2d_schema.rs | 10 + quadratic-core/src/grid/file/v1_7_1/mod.rs | 91 + .../file/v1_7_1/sheet_formatting_schema.rs | 51 + .../src/grid/file/v1_7_1/upgrade.rs | 131 + .../grid/file/v1_7_1/validations_schema.rs | 48 + quadratic-core/src/grid/file/v1_8/mod.rs | 1 + quadratic-core/src/grid/file/v1_8/schema.rs | 155 +- quadratic-core/src/grid/formats/format.rs | 280 +- .../src/grid/formats/format_update.rs | 60 +- quadratic-core/src/grid/formats/mod.rs | 12 +- .../src/grid/formats/sheet_format_updates.rs | 267 + quadratic-core/src/grid/formatting.rs | 220 +- quadratic-core/src/grid/ids.rs | 10 +- quadratic-core/src/grid/js_types.rs | 69 +- quadratic-core/src/grid/mod.rs | 23 +- quadratic-core/src/grid/selection.rs | 45 + quadratic-core/src/grid/series/date_series.rs | 26 +- .../src/grid/series/date_time_series.rs | 62 +- quadratic-core/src/grid/series/mod.rs | 61 +- .../src/grid/series/number_series.rs | 20 +- .../src/grid/series/string_series.rs | 19 +- quadratic-core/src/grid/series/time_series.rs | 43 +- quadratic-core/src/grid/sheet.rs | 720 +- quadratic-core/src/grid/sheet/a1_selection.rs | 691 ++ .../src/grid/sheet/borders/borders_bounds.rs | 587 -- .../src/grid/sheet/borders/borders_clear.rs | 534 -- .../grid/sheet/borders/borders_clipboard.rs | 183 +- .../src/grid/sheet/borders/borders_col_row.rs | 688 +- .../src/grid/sheet/borders/borders_get.rs | 235 - .../src/grid/sheet/borders/borders_old.rs | 232 + .../src/grid/sheet/borders/borders_query.rs | 158 + .../src/grid/sheet/borders/borders_render.rs | 641 +- .../src/grid/sheet/borders/borders_set.rs | 308 +- .../src/grid/sheet/borders/borders_style.rs | 539 +- .../src/grid/sheet/borders/borders_test.rs | 77 +- .../src/grid/sheet/borders/borders_toggle.rs | 564 -- quadratic-core/src/grid/sheet/borders/mod.rs | 193 +- .../src/grid/sheet/borders/sides.rs | 4 + quadratic-core/src/grid/sheet/bounds.rs | 679 +- quadratic-core/src/grid/sheet/cell_array.rs | 29 +- quadratic-core/src/grid/sheet/cell_values.rs | 40 +- quadratic-core/src/grid/sheet/clipboard.rs | 186 +- quadratic-core/src/grid/sheet/code.rs | 21 +- .../src/grid/sheet/col_row/column.rs | 357 +- quadratic-core/src/grid/sheet/col_row/row.rs | 437 +- quadratic-core/src/grid/sheet/data_table.rs | 8 +- quadratic-core/src/grid/sheet/formats.rs | 288 + .../src/grid/sheet/formats/format_all.rs | 534 -- .../src/grid/sheet/formats/format_cell.rs | 327 - .../src/grid/sheet/formats/format_columns.rs | 485 -- .../src/grid/sheet/formats/format_rects.rs | 222 - .../src/grid/sheet/formats/format_rows.rs | 417 -- quadratic-core/src/grid/sheet/formats/mod.rs | 211 - quadratic-core/src/grid/sheet/formatting.rs | 83 - quadratic-core/src/grid/sheet/jump_cursor.rs | 105 +- quadratic-core/src/grid/sheet/rendering.rs | 396 +- .../src/grid/sheet/rendering_date_time.rs | 39 +- quadratic-core/src/grid/sheet/search.rs | 30 +- quadratic-core/src/grid/sheet/selection.rs | 949 --- quadratic-core/src/grid/sheet/send_render.rs | 40 +- quadratic-core/src/grid/sheet/sheet_test.rs | 23 +- quadratic-core/src/grid/sheet/summarize.rs | 134 +- .../src/grid/sheet/validations/mod.rs | 156 +- .../src/grid/sheet/validations/validation.rs | 60 +- .../sheet/validations/validation_col_row.rs | 158 +- .../sheet/validations/validation_rules/mod.rs | 15 +- .../validation_rules/validation_list.rs | 24 +- .../sheet/validations/validation_warnings.rs | 6 +- .../validations/validations_clipboard.rs | 32 +- .../src/grid/sheet_formatting/mod.rs | 42 + .../sheet_formatting_clipboard.rs | 70 + .../sheet_formatting_col_row.rs | 322 + .../sheet_formatting_query.rs | 354 + .../sheet_formatting_update.rs | 90 + quadratic-core/src/grid/sheets.rs | 33 + quadratic-core/src/lib.rs | 9 +- quadratic-core/src/pos.rs | 222 +- quadratic-core/src/rect.rs | 282 +- quadratic-core/src/rle.rs | 8 + .../src/{selection.rs => selection/mod.rs} | 631 +- .../src/selection/selection_create.rs | 332 + quadratic-core/src/sheet_offsets/mod.rs | 91 +- quadratic-core/src/sheet_offsets/offsets.rs | 270 +- .../src/sheet_offsets/sheet_offsets_wasm.rs | 28 +- quadratic-core/src/sheet_rect.rs | 5 +- quadratic-core/src/test_util.rs | 80 +- quadratic-core/src/util.rs | 255 +- quadratic-core/src/values/cell_values.rs | 1 + quadratic-core/src/values/cellvalue.rs | 55 +- quadratic-core/src/values/from_js.rs | 51 +- quadratic-core/src/values/mod.rs | 2 +- quadratic-core/src/viewport.rs | 7 +- .../wasm_bindings/controller/auto_complete.rs | 11 +- .../src/wasm_bindings/controller/borders.rs | 4 +- .../src/wasm_bindings/controller/bounds.rs | 74 +- .../src/wasm_bindings/controller/cells.rs | 115 +- .../src/wasm_bindings/controller/clipboard.rs | 58 +- .../src/wasm_bindings/controller/code.rs | 32 +- .../wasm_bindings/controller/data_table.rs | 7 +- .../src/wasm_bindings/controller/export.rs | 8 +- .../wasm_bindings/controller/formatting.rs | 172 +- .../src/wasm_bindings/controller/import.rs | 4 +- .../src/wasm_bindings/controller/mod.rs | 7 +- .../src/wasm_bindings/controller/render.rs | 4 +- .../src/wasm_bindings/controller/search.rs | 14 +- .../src/wasm_bindings/controller/sheets.rs | 4 +- .../src/wasm_bindings/controller/summarize.rs | 34 +- .../wasm_bindings/controller/transactions.rs | 13 +- .../wasm_bindings/controller/validation.rs | 44 +- quadratic-core/src/wasm_bindings/js.rs | 15 +- .../test-files/test_getCells_migration.grid | Bin 0 -> 2762 bytes .../test-files/v1.7_negative_offsets.grid | Bin 0 -> 538 bytes quadratic-files/.env.docker | 13 +- quadratic-files/.env.example | 11 +- quadratic-files/.env.test | 13 +- quadratic-files/Cargo.toml | 8 +- quadratic-files/src/auth.rs | 71 + quadratic-files/src/config.rs | 25 +- quadratic-files/src/error.rs | 52 +- quadratic-files/src/file.rs | 35 +- quadratic-files/src/main.rs | 2 + quadratic-files/src/server.rs | 56 +- quadratic-files/src/state/mod.rs | 5 +- quadratic-files/src/state/settings.rs | 55 +- quadratic-files/src/storage.rs | 82 + quadratic-files/src/test_util.rs | 2 +- .../python-wasm/quadratic_py/plotly_patch.py | 12 +- .../quadratic_py/process_output.py | 10 +- .../quadratic_py/quadratic_api/quadratic.py | 291 +- .../python-wasm/quadratic_py/run_python.py | 11 +- quadratic-kernels/python-wasm/tests/test.py | 120 +- quadratic-multiplayer/.env.docker | 6 +- quadratic-multiplayer/Cargo.toml | 2 +- .../src/background_worker.rs | 2 +- quadratic-multiplayer/src/error.rs | 4 +- quadratic-multiplayer/src/state/pubsub.rs | 4 +- quadratic-rust-client/.vscode/settings.json | 3 +- quadratic-rust-client/Cargo.toml | 4 +- quadratic-rust-client/package.json | 3 +- quadratic-rust-client/src/lib.rs | 7 + quadratic-rust-client/src/parse_formula.rs | 25 +- quadratic-rust-shared/Cargo.toml | 10 +- quadratic-rust-shared/package.json | 19 + quadratic-rust-shared/src/arrow/error.rs | 8 + quadratic-rust-shared/src/arrow/mod.rs | 1 + quadratic-rust-shared/src/auth/error.rs | 8 + quadratic-rust-shared/src/auth/jwt.rs | 15 +- quadratic-rust-shared/src/auth/mod.rs | 1 + quadratic-rust-shared/src/aws/error.rs | 8 + quadratic-rust-shared/src/aws/mod.rs | 1 + quadratic-rust-shared/src/aws/s3.rs | 54 +- quadratic-rust-shared/src/crypto/aes_cbc.rs | 75 + quadratic-rust-shared/src/crypto/error.rs | 8 + quadratic-rust-shared/src/crypto/mod.rs | 2 + quadratic-rust-shared/src/error.rs | 56 +- quadratic-rust-shared/src/lib.rs | 2 + quadratic-rust-shared/src/sql/error.rs | 17 + quadratic-rust-shared/src/sql/mod.rs | 1 + .../src/sql/mssql_connection.rs | 32 +- .../src/sql/mysql_connection.rs | 21 +- .../src/sql/postgres_connection.rs | 23 +- .../src/sql/snowflake_connection.rs | 13 +- quadratic-rust-shared/src/storage/error.rs | 17 + .../src/storage/file_system.rs | 135 + quadratic-rust-shared/src/storage/mod.rs | 74 + quadratic-rust-shared/src/storage/s3.rs | 75 + quadratic-shared/AI_MODELS.ts | 100 + quadratic-shared/package.json | 2 +- quadratic-shared/typesAndSchemas.ts | 9 + quadratic-shared/typesAndSchemasAI.ts | 336 +- 768 files changed, 53258 insertions(+), 29100 deletions(-) create mode 100644 .github/workflows/production-publish-images.yml create mode 100644 docker/ory-auth/config/identity.schema.json create mode 100644 docker/ory-auth/config/kratos.yml create mode 100755 docker/postgres/scripts/init.sh create mode 100644 docker/snowflake-connection/data/cache/machine.json create mode 100644 docker/snowflake-connection/data/cache/server.test.pem create mode 100644 docker/snowflake-connection/data/cache/server.test.pem.crt create mode 100644 docker/snowflake-connection/data/cache/server.test.pem.key create mode 100644 docker/snowflake-connection/data/cache/service-catalog-3_7_1_dev1-1_35_5.pickle create mode 100644 quadratic-api/src/auth/auth.ts rename quadratic-api/src/{auth0/profile.ts => auth/auth0.ts} (70%) create mode 100644 quadratic-api/src/auth/ory.ts delete mode 100644 quadratic-api/src/aws/s3.ts create mode 100644 quadratic-api/src/licenseClient.ts create mode 100644 quadratic-api/src/routes/ai/aiRateLimiter.ts create mode 100644 quadratic-api/src/routes/ai/bedrock.ts create mode 100644 quadratic-api/src/storage/fileSystem.ts create mode 100644 quadratic-api/src/storage/s3.ts create mode 100644 quadratic-api/src/storage/storage.ts create mode 100644 quadratic-client/.env.test create mode 100644 quadratic-client/public/.well-known/jwks.json create mode 100644 quadratic-client/src/app/ai/components/SelectAIModelMenu.tsx create mode 100644 quadratic-client/src/app/ai/docs/ConnectionDocs.ts create mode 100644 quadratic-client/src/app/ai/docs/FormulaDocs.ts create mode 100644 quadratic-client/src/app/ai/docs/JavascriptDocs.ts create mode 100644 quadratic-client/src/app/ai/docs/PythonDocs.ts create mode 100644 quadratic-client/src/app/ai/docs/QuadraticDocs.ts create mode 100644 quadratic-client/src/app/ai/hooks/useAIModel.tsx create mode 100644 quadratic-client/src/app/ai/hooks/useAIRequestToAPI.tsx create mode 100644 quadratic-client/src/app/ai/hooks/useCodeCellContextMessages.tsx create mode 100644 quadratic-client/src/app/ai/hooks/useCurrentSheetContextMessages.tsx create mode 100644 quadratic-client/src/app/ai/hooks/useGetChatName.tsx create mode 100644 quadratic-client/src/app/ai/hooks/useOtherSheetsContextMessages.tsx create mode 100644 quadratic-client/src/app/ai/hooks/useQuadraticContextMessages.tsx create mode 100644 quadratic-client/src/app/ai/hooks/useSelectionContextMessages.tsx create mode 100644 quadratic-client/src/app/ai/hooks/useToolUseMessages.tsx create mode 100644 quadratic-client/src/app/ai/hooks/useVisibleContextMessages.tsx create mode 100644 quadratic-client/src/app/ai/offline/aiAnalystChats.test.ts create mode 100644 quadratic-client/src/app/ai/offline/aiAnalystChats.ts create mode 100644 quadratic-client/src/app/ai/tools/aiTools.ts create mode 100644 quadratic-client/src/app/ai/tools/aiToolsSpec.ts create mode 100644 quadratic-client/src/app/ai/tools/endpoint.helper.ts create mode 100644 quadratic-client/src/app/ai/tools/message.helper.ts create mode 100644 quadratic-client/src/app/ai/tools/model.helper.ts create mode 100644 quadratic-client/src/app/ai/tools/tool.helpers.ts create mode 100644 quadratic-client/src/app/atoms/aiAnalystAtom.ts create mode 100644 quadratic-client/src/app/bigint.ts delete mode 100644 quadratic-client/src/app/grid/computations/formulas/runFormula.ts delete mode 100644 quadratic-client/src/app/grid/computations/types.ts delete mode 100644 quadratic-client/src/app/grid/sheet/SheetCursorUtils.test.ts delete mode 100644 quadratic-client/src/app/grid/sheet/selection.test.ts delete mode 100644 quadratic-client/src/app/grid/sheet/sheetCursorUtils.ts create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/GridContextMenu.tsx create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/askAISelection/AskAISelection.tsx delete mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/hoverCell/HoverCell.css delete mode 100644 quadratic-client/src/app/gridGL/UI/AxesLines.ts create mode 100644 quadratic-client/src/app/gridGL/UI/Background.ts create mode 100644 quadratic-client/src/app/gridGL/UI/gridHeadings/getA1Notation.test.ts delete mode 100644 quadratic-client/src/app/gridGL/UI/gridHeadings/tests/getA1Notation.test.ts create mode 100644 quadratic-client/src/app/gridGL/cells/Borders.ts create mode 100644 quadratic-client/src/app/gridGL/cells/CellsArray.ts delete mode 100644 quadratic-client/src/app/gridGL/cells/borders/Borders.ts delete mode 100644 quadratic-client/src/app/gridGL/cells/borders/bordersUtil.test.ts delete mode 100644 quadratic-client/src/app/gridGL/cells/borders/bordersUtil.ts delete mode 100644 quadratic-client/src/app/gridGL/helpers/selectCells.ts create mode 100644 quadratic-client/src/app/gridGL/helpers/selectCells.ts.deprecated create mode 100644 quadratic-client/src/app/gridGL/helpers/selection.ts create mode 100644 quadratic-client/src/app/gridGL/pixiApp/MomentumScrollDetector.ts create mode 100644 quadratic-client/src/app/gridGL/pixiApp/messages.tsx create mode 100644 quadratic-client/src/app/gridGL/pixiApp/viewport/Decelerate.ts create mode 100644 quadratic-client/src/app/gridGL/pixiApp/viewport/Drag.ts create mode 100644 quadratic-client/src/app/gridGL/pixiApp/viewport/Viewport.ts rename quadratic-client/src/app/gridGL/{pixiOverride => pixiApp/viewport}/Wheel.ts (75%) create mode 100644 quadratic-client/src/app/ui/components/AIUserMessageForm.tsx delete mode 100644 quadratic-client/src/app/ui/components/CodeSnippet.tsx create mode 100644 quadratic-client/src/app/ui/components/CurrentSheetNameDisplay.tsx create mode 100644 quadratic-client/src/app/ui/components/CursorSelectionDisplay.tsx create mode 100644 quadratic-client/src/app/ui/components/FixSpillError.tsx create mode 100644 quadratic-client/src/app/ui/components/Markdown.scss delete mode 100644 quadratic-client/src/app/ui/components/TooltipHint.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalyst.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystChatHistory.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystContext.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystEffects.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystHeader.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystMessages.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystSelectContextMenu.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystToolCard.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystUserMessageForm.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/const/defaultAIAnalystContext.ts create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/const/maxRects.ts create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/hooks/useAIAnalystPanelWidth.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/hooks/useSubmitAIAnalystPrompt.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/toolCards/DeleteCells.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/toolCards/MoveCells.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/toolCards/SetCellValues.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/toolCards/SetCodeCellValue.tsx create mode 100644 quadratic-client/src/app/ui/menus/AIAnalyst/toolCards/ToolCard.tsx delete mode 100644 quadratic-client/src/app/ui/menus/AIAssistant/AIAssistant.css delete mode 100644 quadratic-client/src/app/ui/menus/AIAssistant/AIAssistant.tsx delete mode 100644 quadratic-client/src/app/ui/menus/AIAssistant/MODELS.ts delete mode 100644 quadratic-client/src/app/ui/menus/AIAssistant/useAIAssistantModel.tsx delete mode 100644 quadratic-client/src/app/ui/menus/AIAssistant/useAIRequestToAPI.tsx create mode 100644 quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/AIAssistant.tsx create mode 100644 quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/AIAssistantMessages.tsx create mode 100644 quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/AIAssistantUserMessageForm.tsx rename quadratic-client/src/app/ui/menus/{ => CodeEditor}/AIAssistant/AICodeBlockParser.test.ts (100%) rename quadratic-client/src/app/ui/menus/{ => CodeEditor}/AIAssistant/AICodeBlockParser.tsx (90%) create mode 100644 quadratic-client/src/app/ui/menus/CodeEditor/AIAssistant/CodeSnippet.tsx create mode 100644 quadratic-client/src/app/ui/menus/CodeEditor/CodeEditorDiffButtons.tsx delete mode 100644 quadratic-client/src/app/ui/menus/CodeEditor/QuadraticDocs.ts rename quadratic-client/src/app/{helpers/parseEditorPythonCell.ts => ui/menus/CodeEditor/hooks/parseCellsAccessed.ts} (85%) delete mode 100644 quadratic-client/src/app/ui/menus/CodeEditor/hooks/useEditorOnSelectionChange.ts create mode 100644 quadratic-client/src/app/ui/menus/CodeEditor/hooks/useSubmitAIAssistantPrompt.tsx delete mode 100644 quadratic-client/src/app/ui/menus/GoTo/getCoordinatesFromUserInput.test.ts delete mode 100644 quadratic-client/src/app/ui/menus/GoTo/getCoordinatesFromUserInput.ts 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 delete mode 100644 quadratic-client/src/auth.ts create mode 100644 quadratic-client/src/auth/auth.test.ts create mode 100644 quadratic-client/src/auth/auth.ts create mode 100644 quadratic-client/src/auth/auth0.ts create mode 100644 quadratic-client/src/auth/ory.ts delete mode 100644 quadratic-client/src/dashboard/atoms/newFileDialogAtom.ts delete mode 100644 quadratic-client/src/dashboard/components/NewFileDialog.tsx create mode 100644 quadratic-client/src/shared/utils/colors.ts create mode 100644 quadratic-core/proptest-regressions/a1/cell_ref.txt create mode 100644 quadratic-core/proptest-regressions/a1/cell_ref_range.txt create mode 100644 quadratic-core/proptest-regressions/a1/selection.txt create mode 100644 quadratic-core/proptest-regressions/a1/subspaces.txt create mode 100644 quadratic-core/proptest-regressions/grid/contiguous.txt create mode 100644 quadratic-core/proptest-regressions/grid/contiguous/contiguous_2d.txt create mode 100644 quadratic-core/proptest-regressions/grid/contiguous/contiguous_blocks.txt create mode 100644 quadratic-core/proptest-regressions/grid/sheet/formats/mod.txt create mode 100644 quadratic-core/proptest-regressions/rect.txt create mode 100644 quadratic-core/quadratic-core/src/controller/operations/code_cell.rs create mode 100644 quadratic-core/quadratic-core/src/grid/file/sheet_schema.rs create mode 100644 quadratic-core/quadratic-core/src/grid/file/v1_7/file.rs create mode 100644 quadratic-core/src/a1/a1_selection/a1_selection_exclude.rs create mode 100644 quadratic-core/src/a1/a1_selection/a1_selection_mutate.rs create mode 100644 quadratic-core/src/a1/a1_selection/a1_selection_query.rs create mode 100644 quadratic-core/src/a1/a1_selection/a1_selection_select.rs create mode 100644 quadratic-core/src/a1/a1_selection/mod.rs create mode 100644 quadratic-core/src/a1/a1_sheet_name.rs create mode 100644 quadratic-core/src/a1/cell_ref_coord.rs create mode 100644 quadratic-core/src/a1/cell_ref_end.rs create mode 100644 quadratic-core/src/a1/cell_ref_range/cell_ref_col_row.rs create mode 100644 quadratic-core/src/a1/cell_ref_range/cell_ref_query.rs create mode 100644 quadratic-core/src/a1/cell_ref_range/mod.rs create mode 100644 quadratic-core/src/a1/column_names.rs create mode 100644 quadratic-core/src/a1/error.rs create mode 100644 quadratic-core/src/a1/js_selection.rs create mode 100644 quadratic-core/src/a1/mod.rs create mode 100644 quadratic-core/src/a1/ref_range_bounds/mod.rs create mode 100644 quadratic-core/src/a1/ref_range_bounds/ref_range_bounds_contains.rs create mode 100644 quadratic-core/src/a1/ref_range_bounds/ref_range_bounds_create.rs create mode 100644 quadratic-core/src/a1/ref_range_bounds/ref_range_bounds_intersection.rs create mode 100644 quadratic-core/src/a1/ref_range_bounds/ref_range_bounds_query.rs create mode 100644 quadratic-core/src/a1/ref_range_bounds/ref_range_bounds_translate.rs create mode 100644 quadratic-core/src/a1/sheet_cell_ref_range.rs create mode 100644 quadratic-core/src/clear_option.rs create mode 100644 quadratic-core/src/controller/execution/execute_operation/execute_borders_old.rs create mode 100644 quadratic-core/src/controller/execution/execute_operation/execute_formats_old.rs create mode 100644 quadratic-core/src/copy_formats.rs delete mode 100644 quadratic-core/src/grid/borders/cell.rs delete mode 100644 quadratic-core/src/grid/borders/mod.rs delete mode 100644 quadratic-core/src/grid/borders/sheet.rs rename quadratic-core/src/{ => grid}/cell.rs (100%) create mode 100644 quadratic-core/src/grid/cells_accessed.rs create mode 100644 quadratic-core/src/grid/code_cell.rs create mode 100644 quadratic-core/src/grid/contiguous/block.rs create mode 100644 quadratic-core/src/grid/contiguous/contiguous_2d.rs create mode 100644 quadratic-core/src/grid/contiguous/contiguous_blocks.rs create mode 100644 quadratic-core/src/grid/contiguous/mod.rs create mode 100644 quadratic-core/src/grid/file/migrate_code_cell_references.rs create mode 100644 quadratic-core/src/grid/file/serialize/code_cell.rs create mode 100644 quadratic-core/src/grid/file/serialize/contiguous_2d.rs delete mode 100644 quadratic-core/src/grid/file/serialize/format.rs create mode 100644 quadratic-core/src/grid/file/serialize/formats.rs create mode 100644 quadratic-core/src/grid/file/serialize/row_resizes.rs create mode 100644 quadratic-core/src/grid/file/shift_negative_offsets.rs create mode 100644 quadratic-core/src/grid/file/v1_6/borders_upgrade.rs create mode 100644 quadratic-core/src/grid/file/v1_7/borders_a1_upgrade.rs create mode 100644 quadratic-core/src/grid/file/v1_7/contiguous_2d_upgrade.rs create mode 100644 quadratic-core/src/grid/file/v1_7/sheet_formatting_upgrade.rs create mode 100644 quadratic-core/src/grid/file/v1_7/upgrade.rs create mode 100644 quadratic-core/src/grid/file/v1_7_1/a1_selection_schema.rs create mode 100644 quadratic-core/src/grid/file/v1_7_1/borders_a1_schema.rs create mode 100644 quadratic-core/src/grid/file/v1_7_1/cells_accessed_schema.rs create mode 100644 quadratic-core/src/grid/file/v1_7_1/contiguous_2d_schema.rs create mode 100644 quadratic-core/src/grid/file/v1_7_1/mod.rs create mode 100644 quadratic-core/src/grid/file/v1_7_1/sheet_formatting_schema.rs create mode 100644 quadratic-core/src/grid/file/v1_7_1/upgrade.rs create mode 100644 quadratic-core/src/grid/file/v1_7_1/validations_schema.rs create mode 100644 quadratic-core/src/grid/formats/sheet_format_updates.rs create mode 100644 quadratic-core/src/grid/selection.rs create mode 100644 quadratic-core/src/grid/sheet/a1_selection.rs delete mode 100644 quadratic-core/src/grid/sheet/borders/borders_bounds.rs delete mode 100644 quadratic-core/src/grid/sheet/borders/borders_clear.rs delete mode 100644 quadratic-core/src/grid/sheet/borders/borders_get.rs create mode 100644 quadratic-core/src/grid/sheet/borders/borders_old.rs create mode 100644 quadratic-core/src/grid/sheet/borders/borders_query.rs delete mode 100644 quadratic-core/src/grid/sheet/borders/borders_toggle.rs create mode 100644 quadratic-core/src/grid/sheet/formats.rs delete mode 100644 quadratic-core/src/grid/sheet/formats/format_all.rs delete mode 100644 quadratic-core/src/grid/sheet/formats/format_cell.rs delete mode 100644 quadratic-core/src/grid/sheet/formats/format_columns.rs delete mode 100644 quadratic-core/src/grid/sheet/formats/format_rects.rs delete mode 100644 quadratic-core/src/grid/sheet/formats/format_rows.rs delete mode 100644 quadratic-core/src/grid/sheet/formats/mod.rs delete mode 100644 quadratic-core/src/grid/sheet/formatting.rs delete mode 100644 quadratic-core/src/grid/sheet/selection.rs create mode 100644 quadratic-core/src/grid/sheet_formatting/mod.rs create mode 100644 quadratic-core/src/grid/sheet_formatting/sheet_formatting_clipboard.rs create mode 100644 quadratic-core/src/grid/sheet_formatting/sheet_formatting_col_row.rs create mode 100644 quadratic-core/src/grid/sheet_formatting/sheet_formatting_query.rs create mode 100644 quadratic-core/src/grid/sheet_formatting/sheet_formatting_update.rs rename quadratic-core/src/{selection.rs => selection/mod.rs} (68%) create mode 100644 quadratic-core/src/selection/selection_create.rs create mode 100644 quadratic-core/test-files/test_getCells_migration.grid create mode 100644 quadratic-core/test-files/v1.7_negative_offsets.grid create mode 100644 quadratic-files/src/auth.rs create mode 100644 quadratic-files/src/storage.rs create mode 100644 quadratic-rust-shared/package.json create mode 100644 quadratic-rust-shared/src/arrow/error.rs create mode 100644 quadratic-rust-shared/src/auth/error.rs create mode 100644 quadratic-rust-shared/src/aws/error.rs create mode 100644 quadratic-rust-shared/src/crypto/aes_cbc.rs create mode 100644 quadratic-rust-shared/src/crypto/error.rs create mode 100644 quadratic-rust-shared/src/crypto/mod.rs create mode 100644 quadratic-rust-shared/src/sql/error.rs create mode 100644 quadratic-rust-shared/src/storage/error.rs create mode 100644 quadratic-rust-shared/src/storage/file_system.rs create mode 100644 quadratic-rust-shared/src/storage/mod.rs create mode 100644 quadratic-rust-shared/src/storage/s3.rs create mode 100644 quadratic-shared/AI_MODELS.ts diff --git a/.github/workflows/production-publish-images.yml b/.github/workflows/production-publish-images.yml new file mode 100644 index 0000000000..5e4315b3e9 --- /dev/null +++ b/.github/workflows/production-publish-images.yml @@ -0,0 +1,60 @@ +name: Build and Publish Images to ECR + +on: + push: + branches: + - self-hosting-setup #remove + - main + +concurrency: + group: production-publish-images + +jobs: + publish_images: + runs-on: ubuntu-latest-8-cores + strategy: + matrix: + service: [multiplayer, files, connection, client, api] + steps: + - uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to Amazon ECR Public + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + with: + registry-type: public + + - name: Define repository name + id: repo-name + run: | + echo "REPO_NAME=quadratic-${{ matrix.service }}" >> $GITHUB_OUTPUT + + - name: Create Public ECR Repository if not exists + id: create-ecr + env: + REPO_NAME: ${{ steps.repo-name.outputs.REPO_NAME }} + run: | + aws ecr-public create-repository --repository-name $REPO_NAME || true + REPO_INFO=$(aws ecr-public describe-repositories --repository-names $REPO_NAME) + ECR_URL=$(echo $REPO_INFO | jq -r '.repositories[0].repositoryUri') + echo "ECR_URL=$ECR_URL" >> $GITHUB_OUTPUT + + - name: Read VERSION file + id: version + run: echo "VERSION=$(cat VERSION)" >> $GITHUB_OUTPUT + + - name: Build, Tag, and Push Image to Amazon ECR Public + env: + ECR_URL: ${{ steps.create-ecr.outputs.ECR_URL }} + IMAGE_TAG: ${{ steps.version.outputs.VERSION }} + run: | + docker build -t $ECR_URL:$IMAGE_TAG -t $ECR_URL:latest -f quadratic-${{ matrix.service }}/Dockerfile . + docker push $ECR_URL:$IMAGE_TAG + docker push $ECR_URL:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7fc4040921..26f3d1bb3f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,10 +39,15 @@ quadratic-connection/target/ quadratic-core/target/ quadratic-core/tmp.txt quadratic-files/target/ +quadratic-files/storage quadratic-multiplayer/target/ quadratic-multiplayer/updateAlertVersion.json +<<<<<<< HEAD quadratic-rust-shared/src/auto_gen_path.rs +======= +quadratic-shared/src/auto_gen_path.rs +>>>>>>> origin/qa # Generated JS files quadratic-api/node_modules/ @@ -66,6 +71,7 @@ docker/mysql/data docker/postgres/data docker/redis/data docker/static/html +docker/file-storage docker/postgres-connection/data docker/mysql-connection/data docker/snowflake-connection/data diff --git a/.vscode/settings.json b/.vscode/settings.json index e868bfa695..f5e14ab892 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,10 +2,15 @@ "editor.formatOnSave": true, "cSpell.words": [ "actix", + "ayush", "bigdecimal", "bincode", "bindgen", +<<<<<<< HEAD "cellvalue", +======= + "CRPXNLSKVLJFHH", +>>>>>>> origin/qa "dashmap", "dbgjs", "dcell", @@ -13,7 +18,10 @@ "dgraph", "dotenv", "endregion", + "finitize", "Fuzzysort", + "GETCELL", + "getcells", "gramm", "grammarly", "Hasher", @@ -27,6 +35,7 @@ "jwks", "MDSL", "micropip", + "minmax", "msdf", "nonblank", "Northbridge", @@ -35,10 +44,14 @@ "peekable", "pixi", "pixiapp", + "Plotly", "pulumi", "pyimport", "rects", + "RELCELL", + "relcells", "reqwest", + "scrollend", "shadcn", "Signin", "smallpop", @@ -47,6 +60,7 @@ "szhsin", "thiserror", "Timelike", + "trackpad", "undoable", "unspill", "vals", @@ -56,6 +70,10 @@ "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, +<<<<<<< HEAD +======= + "rust-analyzer.check.extraArgs": ["--target-dir=target/rust-analyzer"], +>>>>>>> origin/qa "rust-analyzer.checkOnSave": true, "rust-analyzer.cargo.unsetTest": true, // "rust-analyzer.checkOnSave.command": "clippy", @@ -92,5 +110,8 @@ }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/Cargo.lock b/Cargo.lock index 6bf3521f41..09623a4803 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -10,18 +10,29 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] [[package]] name = "ahash" @@ -59,9 +70,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" [[package]] name = "android-tzdata" @@ -105,9 +116,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arrayvec" @@ -144,23 +155,23 @@ dependencies = [ [[package]] name = "arrow" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45aef0d9cf9a039bf6cd1acc451b137aca819977b0928dece52bd92811b640ba" +checksum = "4caf25cdc4a985f91df42ed9e9308e1adbcd341a31a72605c697033fcef163e3" dependencies = [ - "arrow-arith 53.0.0", - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-cast 53.0.0", - "arrow-csv 53.0.0", - "arrow-data 53.0.0", - "arrow-ipc 53.0.0", - "arrow-json 53.0.0", - "arrow-ord 53.0.0", - "arrow-row 53.0.0", - "arrow-schema 53.0.0", - "arrow-select 53.0.0", - "arrow-string 53.0.0", + "arrow-arith 53.2.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-cast 53.2.0", + "arrow-csv 53.2.0", + "arrow-data 53.2.0", + "arrow-ipc 53.2.0", + "arrow-json 53.2.0", + "arrow-ord 53.2.0", + "arrow-row 53.2.0", + "arrow-schema 53.2.0", + "arrow-select 53.2.0", + "arrow-string 53.2.0", ] [[package]] @@ -180,14 +191,14 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03675e42d1560790f3524800e41403b40d0da1c793fe9528929fde06d8c7649a" +checksum = "91f2dfd1a7ec0aca967dfaa616096aec49779adc8eccec005e2f5e4111b1192a" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-data 53.2.0", + "arrow-schema 53.2.0", "chrono", "half", "num", @@ -211,14 +222,14 @@ dependencies = [ [[package]] name = "arrow-array" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd2bf348cf9f02a5975c5962c7fa6dee107a2009a7b41ac5fb1a027e12dc033f" +checksum = "d39387ca628be747394890a6e47f138ceac1aa912eab64f02519fed24b637af8" dependencies = [ "ahash 0.8.11", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-buffer 53.2.0", + "arrow-data 53.2.0", + "arrow-schema 53.2.0", "chrono", "half", "hashbrown 0.14.5", @@ -238,9 +249,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3092e37715f168976012ce52273c3989b5793b0db5f06cbaa246be25e5f0924d" +checksum = "9e51e05228852ffe3eb391ce7178a0f97d2cf80cc6ef91d3c4a6b3cb688049ec" dependencies = [ "bytes", "half", @@ -262,28 +273,28 @@ dependencies = [ "base64 0.22.1", "chrono", "half", - "lexical-core", + "lexical-core 0.8.5", "num", "ryu", ] [[package]] name = "arrow-cast" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce1018bb710d502f9db06af026ed3561552e493e989a79d0d0f5d9cf267a785" +checksum = "d09aea56ec9fa267f3f3f6cdab67d8a9974cbba90b3aa38c8fe9d0bb071bd8c1" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", - "arrow-select 53.0.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-data 53.2.0", + "arrow-schema 53.2.0", + "arrow-select 53.2.0", "atoi", "base64 0.22.1", "chrono", "comfy-table", "half", - "lexical-core", + "lexical-core 1.0.2", "num", "ryu", ] @@ -303,26 +314,26 @@ dependencies = [ "csv", "csv-core", "lazy_static", - "lexical-core", + "lexical-core 0.8.5", "regex", ] [[package]] name = "arrow-csv" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd178575f45624d045e4ebee714e246a05d9652e41363ee3f57ec18cca97f740" +checksum = "c07b5232be87d115fde73e32f2ca7f1b353bff1b44ac422d3c6fc6ae38f11f0d" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-cast 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-cast 53.2.0", + "arrow-data 53.2.0", + "arrow-schema 53.2.0", "chrono", "csv", "csv-core", "lazy_static", - "lexical-core", + "lexical-core 1.0.2", "regex", ] @@ -340,12 +351,12 @@ dependencies = [ [[package]] name = "arrow-data" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4ac0c4ee79150afe067dc4857154b3ee9c1cd52b5f40d59a77306d0ed18d65" +checksum = "b98ae0af50890b494cebd7d6b04b35e896205c1d1df7b29a6272c5d0d0249ef5" dependencies = [ - "arrow-buffer 53.0.0", - "arrow-schema 53.0.0", + "arrow-buffer 53.2.0", + "arrow-schema 53.2.0", "half", "num", ] @@ -366,15 +377,15 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb307482348a1267f91b0912e962cd53440e5de0f7fb24c5f7b10da70b38c94a" +checksum = "0ed91bdeaff5a1c00d28d8f73466bcb64d32bbd7093b5a30156b4b9f4dba3eee" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-cast 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-cast 53.2.0", + "arrow-data 53.2.0", + "arrow-schema 53.2.0", "flatbuffers 24.3.25", ] @@ -391,8 +402,8 @@ dependencies = [ "arrow-schema 51.0.0", "chrono", "half", - "indexmap 2.3.0", - "lexical-core", + "indexmap 2.6.0", + "lexical-core 0.8.5", "num", "serde", "serde_json", @@ -400,19 +411,19 @@ dependencies = [ [[package]] name = "arrow-json" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24805ba326758effdd6f2cbdd482fcfab749544f21b134701add25b33f474e6" +checksum = "0471f51260a5309307e5d409c9dc70aede1cd9cf1d4ff0f0a1e8e1a2dd0e0d3c" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-cast 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-cast 53.2.0", + "arrow-data 53.2.0", + "arrow-schema 53.2.0", "chrono", "half", - "indexmap 2.3.0", - "lexical-core", + "indexmap 2.6.0", + "lexical-core 1.0.2", "num", "serde", "serde_json", @@ -435,15 +446,15 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644046c479d80ae8ed02a7f1e1399072ea344ca6a7b0e293ab2d5d9ed924aa3b" +checksum = "2883d7035e0b600fb4c30ce1e50e66e53d8656aa729f2bfa4b51d359cf3ded52" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", - "arrow-select 53.0.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-data 53.2.0", + "arrow-schema 53.2.0", + "arrow-select 53.2.0", "half", "num", ] @@ -465,15 +476,15 @@ dependencies = [ [[package]] name = "arrow-row" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a29791f8eb13b340ce35525b723f5f0df17ecb955599e11f65c2a94ab34e2efb" +checksum = "552907e8e587a6fde4f8843fd7a27a576a260f65dab6c065741ea79f633fc5be" dependencies = [ "ahash 0.8.11", - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-data 53.2.0", + "arrow-schema 53.2.0", "half", ] @@ -485,9 +496,9 @@ checksum = "02d9483aaabe910c4781153ae1b6ae0393f72d9ef757d38d09d450070cf2e528" [[package]] name = "arrow-schema" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85320a3a2facf2b2822b57aa9d6d9d55edb8aee0b6b5d3b8df158e503d10858" +checksum = "539ada65246b949bd99ffa0881a9a15a4a529448af1a07a9838dd78617dafab1" [[package]] name = "arrow-select" @@ -505,15 +516,15 @@ dependencies = [ [[package]] name = "arrow-select" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc7e6b582e23855fd1625ce46e51647aa440c20ea2e71b1d748e0839dd73cba" +checksum = "6259e566b752da6dceab91766ed8b2e67bf6270eb9ad8a6e07a33c1bede2b125" dependencies = [ "ahash 0.8.11", - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-data 53.2.0", + "arrow-schema 53.2.0", "num", ] @@ -531,24 +542,35 @@ dependencies = [ "memchr", "num", "regex", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] name = "arrow-string" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0775b6567c66e56ded19b87a954b6b1beffbdd784ef95a3a2b03f59570c1d230" +checksum = "f3179ccbd18ebf04277a095ba7321b93fd1f774f18816bd5f6b3ce2f594edb6c" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", - "arrow-select 53.0.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-data 53.2.0", + "arrow-schema 53.2.0", + "arrow-select 53.2.0", "memchr", "num", "regex", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", +] + +[[package]] +name = "assert-json-diff" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4259cbe96513d2f1073027a259fc2ca917feb3026a5a8d984e3628e490255cc0" +dependencies = [ + "extend", + "serde", + "serde_json", ] [[package]] @@ -567,7 +589,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" dependencies = [ - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", ] @@ -596,9 +618,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.12" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" +checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" dependencies = [ "flate2", "futures-core", @@ -637,9 +659,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", @@ -753,9 +775,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -764,13 +786,13 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -781,13 +803,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -831,15 +853,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-config" -version = "1.5.4" +version = "1.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf6cfe2881cb1fcbba9ae946fb9a6480d3b7a714ca84c74925014a89ef3387a" +checksum = "9b49afaa341e8dd8577e1a2200468f98956d6eda50bcf4a53246cc00174ba924" dependencies = [ "aws-credential-types", "aws-runtime", @@ -857,7 +879,6 @@ dependencies = [ "fastrand", "hex", "http 0.2.12", - "hyper 0.14.30", "ring", "time", "tokio", @@ -868,9 +889,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9" +checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -880,9 +901,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" +checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -892,9 +913,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.21.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ddc4a5b231dd6958b140ff3151b6412b3f4321fab354f399eec8f14b06df62" +checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" dependencies = [ "bindgen", "cc", @@ -907,15 +928,16 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.3.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c5f920ffd1e0526ec9e70e50bf444db50b204395a0fa7016bbf9e31ea1698f" +checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468" dependencies = [ "aws-credential-types", "aws-sigv4", "aws-smithy-async", "aws-smithy-eventstream", "aws-smithy-http", + "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", @@ -923,6 +945,7 @@ dependencies = [ "fastrand", "http 0.2.12", "http-body 0.4.6", + "once_cell", "percent-encoding", "pin-project-lite", "tracing", @@ -931,11 +954,10 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.42.0" +version = "1.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558bbcec8db82a1a8af1610afcb3b10d00652d25ad366a0558eecdff2400a1d1" +checksum = "0506cc60e392e33712d47717d5ae5760a3b134bf8ee7aea7e43df3d7e2669ae0" dependencies = [ - "ahash 0.8.11", "aws-credential-types", "aws-runtime", "aws-sigv4", @@ -966,9 +988,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.36.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6acca681c53374bf1d9af0e317a41d12a44902ca0f2d1e10e5cb5bb98ed74f35" +checksum = "09677244a9da92172c8dc60109b4a9658597d4d298b188dd0018b6a66b410ca4" dependencies = [ "aws-credential-types", "aws-runtime", @@ -988,9 +1010,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.37.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79c6bdfe612503a526059c05c9ccccbf6bd9530b003673cb863e547fd7c0c9a" +checksum = "81fea2f3a8bb3bd10932ae7ad59cc59f65f270fc9183a7e91f501dc5efbef7ee" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1010,9 +1032,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.36.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e6ecdb2bd756f3b2383e6f0588dc10a4e65f5d551e70a56e0bfe0c884673ce" +checksum = "53dcf5e7d9bd1517b8b998e170e650047cea8a2b85fe1835abe3210713e541b7" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1033,9 +1055,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.3" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be" +checksum = "5619742a0d8f253be760bfbb8e8e8368c69e3587e4637af5754e488a611499b1" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -1073,9 +1095,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.60.11" +version = "0.60.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c4134cf3adaeacff34d588dbe814200357b0c466d730cf1c0d8054384a2de4" +checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -1094,9 +1116,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.4" +version = "0.60.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6363078f927f612b970edf9d1903ef5cef9a64d1e8423525ebb1f0a1633c858" +checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" dependencies = [ "aws-smithy-types", "bytes", @@ -1105,9 +1127,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.9" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9cd0ae3d97daa0a2bf377a4d8e8e1362cae590c4a1aad0d40058ebca18eb91e" +checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -1133,6 +1155,25 @@ dependencies = [ "aws-smithy-types", ] +[[package]] +name = "aws-smithy-protocol-test" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b92b62199921f10685c6b588fdbeb81168ae4e7950ae3e5f50145a01bb5f1ad" +dependencies = [ + "assert-json-diff 1.1.0", + "aws-smithy-runtime-api", + "base64-simd", + "cbor-diag", + "ciborium", + "http 0.2.12", + "pretty_assertions", + "regex-lite", + "roxmltree", + "serde_json", + "thiserror", +] + [[package]] name = "aws-smithy-query" version = "0.60.7" @@ -1145,12 +1186,13 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.6.2" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce87155eba55e11768b8c1afa607f3e864ae82f03caf63258b37455b0ad02537" +checksum = "be28bd063fa91fd871d131fc8b68d7cd4c5fa0869bea68daca50dcb1cbd76be2" dependencies = [ "aws-smithy-async", "aws-smithy-http", + "aws-smithy-protocol-test", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", @@ -1160,21 +1202,25 @@ dependencies = [ "http-body 0.4.6", "http-body 1.0.1", "httparse", - "hyper 0.14.30", + "hyper 0.14.31", "hyper-rustls 0.24.2", + "indexmap 2.6.0", "once_cell", "pin-project-lite", "pin-utils", "rustls 0.21.12", + "serde", + "serde_json", "tokio", "tracing", + "tracing-subscriber", ] [[package]] name = "aws-smithy-runtime-api" -version = "1.7.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30819352ed0a04ecf6a2f3477e344d2d1ba33d43e0f09ad9047c12e0d923616f" +checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -1189,9 +1235,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.0" +version = "1.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe321a6b21f5d8eabd0ade9c55d3d0335f3c3157fc2b3e87f05f34b539e4df5" +checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510" dependencies = [ "base64-simd", "bytes", @@ -1215,9 +1261,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.8" +version = "0.60.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" dependencies = [ "xmlparser", ] @@ -1238,19 +1284,20 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", "axum-core", - "base64 0.21.7", + "axum-macros", + "base64 0.22.1", "bytes", "futures-util", "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "itoa", "matchit", @@ -1266,8 +1313,8 @@ dependencies = [ "sha1", "sync_wrapper 1.0.1", "tokio", - "tokio-tungstenite", - "tower", + "tokio-tungstenite 0.24.0", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -1275,9 +1322,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", @@ -1288,7 +1335,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -1296,9 +1343,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9" dependencies = [ "axum", "axum-core", @@ -1311,7 +1358,7 @@ dependencies = [ "mime", "pin-project-lite", "serde", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -1319,29 +1366,28 @@ dependencies = [ [[package]] name = "axum-macros" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ - "heck 0.4.1", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -1391,9 +1437,9 @@ dependencies = [ [[package]] name = "bigdecimal" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d712318a27c7150326677b321a5fa91b55f6d9034ffd67f20319e147d40cee" +checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1" dependencies = [ "autocfg", "libm", @@ -1414,24 +1460,24 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.4" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.12.1", "lazy_static", "lazycell", "log", "prettyplease", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.79", + "syn 2.0.87", "which", ] @@ -1486,6 +1532,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "blocking" version = "1.6.1" @@ -1501,9 +1556,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +checksum = "f5327f6c99920069d1fe374aa743be1af0031dea9f250852cdf1ae6a0861ee24" dependencies = [ "borsh-derive", "cfg_aliases", @@ -1511,16 +1566,24 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +checksum = "10aedd8f1a81a8aafbfde924b0e3061cd6fedd6f6bbcfc6a76e6fd426d7bfe26" dependencies = [ "once_cell", "proc-macro-crate", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", - "syn_derive", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", ] [[package]] @@ -1546,8 +1609,8 @@ version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "syn 1.0.109", ] @@ -1565,9 +1628,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" dependencies = [ "serde", ] @@ -1605,14 +1668,43 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbor-diag" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc245b6ecd09b23901a4fbad1ad975701fd5061ceaef6afa93a2d70605a64429" +dependencies = [ + "bs58", + "chrono", + "data-encoding", + "half", + "nom", + "num-bigint", + "num-rational", + "num-traits", + "separator", + "url", + "uuid", +] + [[package]] name = "cc" -version = "1.1.7" +version = "1.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -1678,6 +1770,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1844,15 +1946,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -1998,9 +2100,9 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", @@ -2035,10 +2137,10 @@ checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "strsim", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -2048,8 +2150,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", - "quote 1.0.36", - "syn 2.0.79", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -2122,6 +2224,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -2134,6 +2242,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -2148,14 +2267,14 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "dummy" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e57e12b69e57fad516e01e2b3960f122696fdb13420e1a88ed8e210316f2876" +checksum = "1cac124e13ae9aa56acc4241f8c8207501d93afdd8d8e62f0c1f2e12f6508c65" dependencies = [ "darling", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -2207,9 +2326,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -2229,9 +2348,9 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -2297,11 +2416,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "extend" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47da3a72ec598d9c8937a7ebca8962a5c7a1f28444e38c2b33c771ba3f55f05" +dependencies = [ + "proc-macro-error", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 1.0.109", +] + [[package]] name = "fake" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c25829bde82205da46e1823b2259db6273379f626fc211f126f65654a2669be" +checksum = "2d391ba4af7f1d93f01fcf7b2f29e2bc9348e109dfdbf4dcbdc51dfa38dab0b6" dependencies = [ "deunicode", "dummy", @@ -2310,9 +2441,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "ff" @@ -2346,9 +2477,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.31" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -2356,9 +2487,9 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -2371,6 +2502,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -2409,9 +2546,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -2424,9 +2561,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -2434,15 +2571,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -2462,15 +2599,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "fastrand", "futures-core", @@ -2481,26 +2618,26 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -2510,9 +2647,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -2562,9 +2699,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" @@ -2607,7 +2744,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.3.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -2626,7 +2763,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.3.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -2663,6 +2800,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashlink" version = "0.9.1" @@ -2832,9 +2980,9 @@ checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -2847,7 +2995,7 @@ name = "httpmock" version = "0.8.0-alpha.1" source = "git+https://github.com/quadratichq/httpmock#bd58822ed1261a0f04a6615b5ddb43b118f5fa69" dependencies = [ - "assert-json-diff", + "assert-json-diff 2.0.2", "async-object-pool", "async-std", "async-trait", @@ -2860,14 +3008,14 @@ dependencies = [ "headers", "http 1.1.0", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-rustls 0.27.3", "hyper-util", "lazy_static", "log", "path-tree", "regex", - "rustls 0.23.13", + "rustls 0.23.16", "serde", "serde_json", "serde_regex", @@ -2888,9 +3036,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -2912,9 +3060,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ "bytes", "futures-channel", @@ -2939,7 +3087,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.30", + "hyper 0.14.31", "log", "rustls 0.21.12", "rustls-native-certs 0.6.3", @@ -2955,10 +3103,10 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "log", - "rustls 0.23.13", + "rustls 0.23.16", "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", @@ -2974,7 +3122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.30", + "hyper 0.14.31", "native-tls", "tokio", "tokio-native-tls", @@ -2982,29 +3130,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.6" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.0", "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3023,6 +3170,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -3041,12 +3306,23 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -3062,15 +3338,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.3.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.1", "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -3091,9 +3377,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "itertools" @@ -3104,6 +3390,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -3130,9 +3425,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -3182,11 +3477,24 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" dependencies = [ - "lexical-parse-float", - "lexical-parse-integer", - "lexical-util", - "lexical-write-float", - "lexical-write-integer", + "lexical-parse-float 0.8.5", + "lexical-parse-integer 0.8.6", + "lexical-util 0.8.5", + "lexical-write-float 0.8.5", + "lexical-write-integer 0.8.5", +] + +[[package]] +name = "lexical-core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431c65b318a590c1de6b8fd6e72798c92291d27762d94c9e6c37ed7a73d8458" +dependencies = [ + "lexical-parse-float 1.0.2", + "lexical-parse-integer 1.0.2", + "lexical-util 1.0.3", + "lexical-write-float 1.0.2", + "lexical-write-integer 1.0.2", ] [[package]] @@ -3195,8 +3503,19 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" dependencies = [ - "lexical-parse-integer", - "lexical-util", + "lexical-parse-integer 0.8.6", + "lexical-util 0.8.5", + "static_assertions", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb17a4bdb9b418051aa59d41d65b1c9be5affab314a872e5ad7f06231fb3b4e0" +dependencies = [ + "lexical-parse-integer 1.0.2", + "lexical-util 1.0.3", "static_assertions", ] @@ -3204,39 +3523,79 @@ dependencies = [ name = "lexical-parse-integer" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +dependencies = [ + "lexical-util 0.8.5", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df98f4a4ab53bf8b175b363a34c7af608fe31f93cc1fb1bf07130622ca4ef61" +dependencies = [ + "lexical-util 1.0.3", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85314db53332e5c192b6bca611fb10c114a80d1b831ddac0af1e9be1b9232ca0" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" dependencies = [ - "lexical-util", + "lexical-util 0.8.5", + "lexical-write-integer 0.8.5", "static_assertions", ] [[package]] -name = "lexical-util" -version = "0.8.5" +name = "lexical-write-float" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +checksum = "6e7c3ad4e37db81c1cbe7cf34610340adc09c322871972f74877a712abc6c809" dependencies = [ + "lexical-util 1.0.3", + "lexical-write-integer 1.0.2", "static_assertions", ] [[package]] -name = "lexical-write-float" +name = "lexical-write-integer" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" +checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" dependencies = [ - "lexical-util", - "lexical-write-integer", + "lexical-util 0.8.5", "static_assertions", ] [[package]] name = "lexical-write-integer" -version = "0.8.5" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" +checksum = "eb89e9f6958b83258afa3deed90b5de9ef68eef090ad5086c791cd2345610162" dependencies = [ - "lexical-util", + "lexical-util 1.0.3", "static_assertions", ] @@ -3248,9 +3607,9 @@ checksum = "b14c52534dd690e23b687bdbbbe300d7ec5c45f25e9261f72b70751af67067d3" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libloading" @@ -3264,9 +3623,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libsqlite3-sys" @@ -3285,6 +3644,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" @@ -3306,11 +3671,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.1", ] [[package]] @@ -3376,6 +3741,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "minicov" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3384,18 +3759,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", @@ -3545,18 +3920,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.2" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "object_store" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a0c4b3a0e31f8b66f71ad8064521efa773910196e2cde791436f13409f3b45" +checksum = "6eb4c22c6154a1e759d7099f9ffad7cc5ef8245f9efbab4a41b92623079c82f3" dependencies = [ "async-trait", "base64 0.22.1", @@ -3564,14 +3939,14 @@ dependencies = [ "chrono", "futures", "humantime", - "hyper 1.4.1", + "hyper 1.5.0", "itertools 0.13.0", "md-5", "parking_lot 0.12.3", "percent-encoding", "quick-xml 0.36.2", "rand 0.8.5", - "reqwest 0.12.8", + "reqwest 0.12.9", "ring", "serde", "serde_json", @@ -3584,9 +3959,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oorandom" @@ -3596,9 +3971,9 @@ checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -3615,9 +3990,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -3628,18 +4003,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.1+3.3.1" +version = "300.4.0+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -3748,7 +4123,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] @@ -3784,18 +4159,18 @@ dependencies = [ [[package]] name = "parquet" -version = "53.0.0" +version = "53.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0fbf928021131daaa57d334ca8e3904fe9ae22f73c56244fc7db9b04eedc3d8" +checksum = "dea02606ba6f5e856561d8d507dba8bac060aefca2a6c0f1aa1d361fed91ff3e" dependencies = [ "ahash 0.8.11", - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-cast 53.0.0", - "arrow-data 53.0.0", - "arrow-ipc 53.0.0", - "arrow-schema 53.0.0", - "arrow-select 53.0.0", + "arrow-array 53.2.0", + "arrow-buffer 53.2.0", + "arrow-cast 53.2.0", + "arrow-data 53.2.0", + "arrow-ipc 53.2.0", + "arrow-schema 53.2.0", + "arrow-select 53.2.0", "base64 0.22.1", "bytes", "chrono", @@ -3853,29 +4228,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -3927,15 +4302,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", @@ -3973,14 +4348,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ - "proc-macro2 1.0.86", - "syn 2.0.79", + "proc-macro2 1.0.89", + "syn 2.0.87", ] [[package]] @@ -3999,8 +4384,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "syn 1.0.109", "version_check", ] @@ -4011,8 +4396,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "version_check", ] @@ -4027,9 +4412,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -4048,7 +4433,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "rusty-fork", "tempfile", "unarray", @@ -4086,8 +4471,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "syn 1.0.109", ] @@ -4103,7 +4488,7 @@ dependencies = [ [[package]] name = "quadratic-connection" -version = "0.5.2" +version = "0.5.4" dependencies = [ "arrow 51.0.0", "arrow-schema 51.0.0", @@ -4120,7 +4505,7 @@ dependencies = [ "headers", "http 1.1.0", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "jsonwebtoken", "log", @@ -4135,7 +4520,7 @@ dependencies = [ "strum_macros 0.25.3", "thiserror", "tokio", - "tower", + "tower 0.4.13", "tower-http", "tracing", "tracing-subscriber", @@ -4145,7 +4530,7 @@ dependencies = [ [[package]] name = "quadratic-core" -version = "0.5.2" +version = "0.5.4" dependencies = [ "anyhow", "arrow-array 51.0.0", @@ -4153,7 +4538,7 @@ dependencies = [ "arrow-data 51.0.0", "arrow-schema 51.0.0", "async-trait", - "bigdecimal 0.4.5", + "bigdecimal 0.4.6", "bincode", "bytes", "calamine", @@ -4167,7 +4552,7 @@ dependencies = [ "getrandom 0.2.15", "half", "htmlescape", - "indexmap 2.3.0", + "indexmap 2.6.0", "itertools 0.10.5", "js-sys", "lazy_static", @@ -4200,10 +4585,11 @@ dependencies = [ [[package]] name = "quadratic-files" -version = "0.5.2" +version = "0.5.4" dependencies = [ "axum", "axum-extra", + "bytes", "chrono", "dotenv", "envy", @@ -4223,7 +4609,8 @@ dependencies = [ "strum_macros 0.25.3", "thiserror", "tokio", - "tower", + "tokio-util", + "tower 0.4.13", "tower-http", "tracing", "tracing-subscriber", @@ -4232,7 +4619,7 @@ dependencies = [ [[package]] name = "quadratic-multiplayer" -version = "0.5.2" +version = "0.5.4" dependencies = [ "axum", "axum-extra", @@ -4256,8 +4643,8 @@ dependencies = [ "strum_macros 0.25.3", "thiserror", "tokio", - "tokio-tungstenite", - "tower", + "tokio-tungstenite 0.21.0", + "tower 0.4.13", "tower-http", "tracing", "tracing-subscriber", @@ -4266,7 +4653,7 @@ dependencies = [ [[package]] name = "quadratic-rust-client" -version = "0.5.2" +version = "0.5.4" dependencies = [ "chrono", "console_error_panic_hook", @@ -4281,20 +4668,27 @@ dependencies = [ [[package]] name = "quadratic-rust-shared" -version = "0.5.2" +version = "0.5.4" dependencies = [ - "arrow 53.0.0", - "arrow-array 53.0.0", + "aes", + "arrow 53.2.0", + "arrow-array 53.2.0", "async-trait", "aws-config", "aws-sdk-s3", - "bigdecimal 0.4.5", + "aws-smithy-async", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "bigdecimal 0.4.6", "bytes", + "cbc", "chrono", "futures-util", + "hex", + "http 1.1.0", "httpmock", "jsonwebtoken", - "parquet 53.0.0", + "parquet 53.2.0", "redis", "reqwest 0.11.27", "rust_decimal", @@ -4309,6 +4703,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "tracing-test", "uuid", ] @@ -4349,7 +4744,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.16", "socket2", "thiserror", "tokio", @@ -4366,7 +4761,7 @@ dependencies = [ "rand 0.8.5", "ring", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.16", "slab", "thiserror", "tinyvec", @@ -4375,15 +4770,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.4" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" dependencies = [ + "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4397,11 +4793,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.89", ] [[package]] @@ -4542,32 +4938,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -4581,13 +4968,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -4604,9 +4991,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rend" @@ -4633,7 +5020,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "hyper-tls", "ipnet", "js-sys", @@ -4663,9 +5050,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.8" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "async-compression", "base64 0.22.1", @@ -4676,7 +5063,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-rustls 0.27.3", "hyper-util", "ipnet", @@ -4687,7 +5074,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.13", + "rustls 0.23.16", "rustls-native-certs 0.8.0", "rustls-pemfile 2.2.0", "rustls-pki-types", @@ -4717,7 +5104,7 @@ dependencies = [ "anyhow", "async-trait", "http 1.1.0", - "reqwest 0.12.8", + "reqwest 0.12.9", "serde", "thiserror", "tower-service", @@ -4734,9 +5121,9 @@ dependencies = [ "futures", "getrandom 0.2.15", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "parking_lot 0.11.2", - "reqwest 0.12.8", + "reqwest 0.12.9", "reqwest-middleware", "retry-policies", "tokio", @@ -4803,11 +5190,20 @@ version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "syn 1.0.109", ] +[[package]] +name = "roxmltree" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" +dependencies = [ + "xmlparser", +] + [[package]] name = "rsa" version = "0.9.6" @@ -4864,18 +5260,18 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags 2.6.0", "errno", @@ -4898,9 +5294,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "aws-lc-rs", "log", @@ -4957,9 +5353,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" [[package]] name = "rustls-webpki" @@ -4985,9 +5381,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "rusty-fork" @@ -5018,20 +5414,20 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.7" +version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a870e34715d5d59c8536040d4d4e7a41af44d527dc50237036ba4090db7996fc" +checksum = "d8d25269dd3a12467afe2e510f69fb0b46b698e5afb296b59f2145259deaf8e8" dependencies = [ "sdd", ] [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5058,9 +5454,9 @@ dependencies = [ [[package]] name = "sdd" -version = "2.1.0" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177258b64c0faaa9ffd3c65cd3262c2bc7e2588dbbd9c1641d0346145c1bbda8" +checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95" [[package]] name = "seahash" @@ -5097,9 +5493,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -5111,6 +5507,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +[[package]] +name = "separator" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5" + [[package]] name = "seq-macro" version = "0.3.5" @@ -5119,9 +5521,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] @@ -5139,21 +5541,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] name = "serde_json" -version = "1.0.122" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ + "indexmap 2.6.0", "itoa", "memchr", "ryu", @@ -5186,9 +5589,9 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -5205,15 +5608,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.3.0", + "indexmap 2.6.0", "serde", "serde_derive", "serde_json", @@ -5223,14 +5626,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ "darling", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -5239,7 +5642,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.3.0", + "indexmap 2.6.0", "itoa", "ryu", "serde", @@ -5248,9 +5651,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" dependencies = [ "futures", "log", @@ -5262,13 +5665,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -5401,9 +5804,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" dependencies = [ "heck 0.5.0", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -5417,7 +5820,7 @@ name = "snowflake-api" version = "0.10.0" source = "git+https://github.com/quadratichq/snowflake-rs#b7d6e0930b8f01f0b8f395bd68a56e0efe47f3c6" dependencies = [ - "arrow 53.0.0", + "arrow 53.2.0", "async-trait", "base64 0.22.1", "bytes", @@ -5427,7 +5830,7 @@ dependencies = [ "log", "object_store", "regex", - "reqwest 0.12.8", + "reqwest 0.12.9", "reqwest-middleware", "reqwest-retry", "serde", @@ -5495,9 +5898,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ "nom", "unicode_categories", @@ -5523,7 +5926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" dependencies = [ "atoi", - "bigdecimal 0.4.5", + "bigdecimal 0.4.6", "byteorder", "bytes", "chrono", @@ -5539,7 +5942,7 @@ dependencies = [ "hashbrown 0.14.5", "hashlink", "hex", - "indexmap 2.3.0", + "indexmap 2.6.0", "log", "memchr", "native-tls", @@ -5565,11 +5968,11 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "sqlx-core", "sqlx-macros-core", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5583,8 +5986,8 @@ dependencies = [ "heck 0.5.0", "hex", "once_cell", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "serde", "serde_json", "sha2", @@ -5592,7 +5995,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.79", + "syn 2.0.87", "tempfile", "tokio", "url", @@ -5606,7 +6009,7 @@ checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" dependencies = [ "atoi", "base64 0.22.1", - "bigdecimal 0.4.5", + "bigdecimal 0.4.6", "bitflags 2.6.0", "byteorder", "bytes", @@ -5651,7 +6054,7 @@ checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" dependencies = [ "atoi", "base64 0.22.1", - "bigdecimal 0.4.5", + "bigdecimal 0.4.6", "bitflags 2.6.0", "byteorder", "chrono", @@ -5710,6 +6113,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -5755,10 +6164,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "rustversion", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5768,10 +6177,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck 0.5.0", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "rustversion", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5797,34 +6206,22 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.79" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "unicode-ident", ] -[[package]] -name = "syn_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" -dependencies = [ - "proc-macro-error", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", -] - [[package]] name = "sync_wrapper" version = "0.1.2" @@ -5840,6 +6237,17 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -5882,8 +6290,8 @@ checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4" dependencies = [ "heck 0.4.1", "proc-macro-error", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "syn 1.0.109", ] @@ -5904,15 +6312,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.11.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5932,22 +6340,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -6040,6 +6448,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -6067,9 +6485,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -6089,9 +6507,9 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -6120,16 +6538,16 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.13", + "rustls 0.23.16", "rustls-pki-types", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -6158,14 +6576,26 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.21.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.24.0", ] [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -6187,7 +6617,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.3.0", + "indexmap 2.6.0", "toml_datetime", "winnow", ] @@ -6202,6 +6632,21 @@ dependencies = [ "futures-util", "pin-project", "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", "tokio", "tower-layer", "tower-service", @@ -6231,7 +6676,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -6239,15 +6684,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -6267,9 +6712,9 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -6293,6 +6738,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -6303,12 +6758,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -6328,8 +6786,8 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ - "quote 1.0.36", - "syn 2.0.79", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] @@ -6355,9 +6813,9 @@ version = "7.0.0" source = "git+https://github.com/quadratichq/ts-rs/?rev=812c1a8#812c1a8b5ff3128916426e95e228f51430eb02cc" dependencies = [ "Inflector", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", "termcolor", ] @@ -6380,6 +6838,24 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -6404,18 +6880,15 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-case-mapping" @@ -6425,30 +6898,30 @@ checksum = "b92e07ac57786e12073609c8a91417884306b753d974108ca48c8434ed9b99cb" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" @@ -6476,12 +6949,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.3", "percent-encoding", ] @@ -6497,6 +6970,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -6505,9 +6990,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom 0.2.15", "serde", @@ -6521,9 +7006,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" [[package]] name = "vcpkg" @@ -6560,8 +7045,8 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", ] [[package]] @@ -6612,34 +7097,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -6649,41 +7135,42 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ - "quote 1.0.36", + "quote 1.0.37", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-bindgen-test" -version = "0.3.42" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" +checksum = "d381749acb0943d357dcbd8f0b100640679883fcdeeef04def49daf8d33a5426" dependencies = [ "console_error_panic_hook", "js-sys", + "minicov", "scoped-tls", "wasm-bindgen", "wasm-bindgen-futures", @@ -6692,20 +7179,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.42" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" +checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", ] [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -6731,9 +7218,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -6762,11 +7249,11 @@ dependencies = [ [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall 0.5.7", "wasite", ] @@ -7020,6 +7507,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -7035,6 +7534,36 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -7051,9 +7580,30 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", + "synstructure", ] [[package]] @@ -7062,6 +7612,28 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.87", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 0c19ce40f7..8873c8ce37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ edition = "2021" description = "Infinite data grid with Python, JavaScript, and SQL built-in" repository = "https://github.com/quadratichq/quadratic" license-file = "LICENSE" -version = "0.5.2" +version = "0.5.4" [profile.release] # Tell `rustc` to optimize for small code size. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b12960269e..af132690ee 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -78,7 +78,7 @@ defined, along with configuration information, in the `docker compose.yml` file. Services can talk to each other and can communicate with services in the user's host network. -To pull up the Docker network with just the required depedencies (Redis, Postgres, Localstack): +To pull up the Docker network with just the required dependencies (Redis, Postgres, Localstack): ```shell npm run docker:up @@ -234,7 +234,7 @@ To run coverage and generate HTML reports, bring up the docker network npm run coverage ``` -Coverage infomration will be available in the generated `coverage` folder located at `coverage/html/index.html`. +Coverage information will be available in the generated `coverage` folder located at `coverage/html/index.html`. ## Linting diff --git a/VERSION b/VERSION index cb0c939a93..7d8568351b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.2 +0.5.4 diff --git a/client.Dockerfile b/client.Dockerfile index 0f55931755..ea1366e10e 100644 --- a/client.Dockerfile +++ b/client.Dockerfile @@ -8,33 +8,45 @@ ENV PATH="/root/.cargo/bin:${PATH}" # Install wasm-pack RUN echo 'Installing wasm-pack...' && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -# Install python -RUN apt-get update || : && apt-get install python-is-python3 -y && apt install python3-pip -y +# Install wasm32-unknown-unknown target +RUN rustup target add wasm32-unknown-unknown -# Install binaryen -RUN apt install binaryen -y +# Install python, binaryen & clean up +RUN apt-get update && apt-get install -y python-is-python3 python3-pip binaryen && apt-get clean && rm -rf /var/lib/apt/lists/* -# Copy the rest of the application code +# Install npm dependencies WORKDIR /app - COPY package.json . COPY package-lock.json . +COPY ./quadratic-kernels/python-wasm/package*.json ./quadratic-kernels/python-wasm/ +COPY ./quadratic-core/package*.json ./quadratic-core/ +COPY ./quadratic-rust-client/package*.json ./quadratic-rust-client/ +COPY ./quadratic-shared/package*.json ./quadratic-shared/ +COPY ./quadratic-client/package*.json ./quadratic-client/ +RUN npm install + +# Install typescript +RUN npm install -D typescript + +# Copy the rest of the application +WORKDIR /app COPY updateAlertVersion.json . -COPY ./quadratic-client/. ./quadratic-client/ -COPY ./quadratic-core/. ./quadratic-core/ COPY ./quadratic-kernels/python-wasm/. ./quadratic-kernels/python-wasm/ +COPY ./quadratic-core/. ./quadratic-core/ COPY ./quadratic-rust-client/. ./quadratic-rust-client/ COPY ./quadratic-shared/. ./quadratic-shared/ +COPY ./quadratic-client/. ./quadratic-client/ # Run the packaging script for quadratic_py +WORKDIR /app RUN ./quadratic-kernels/python-wasm/package.sh --no-poetry # Build wasm WORKDIR /app/quadratic-core -RUN rustup target add wasm32-unknown-unknown -RUN echo 'Building wasm...' && wasm-pack build --target web --out-dir ../quadratic-client/src/app/quadratic-core --weak-refs +RUN echo 'Building wasm...' && wasm-pack build --target web --out-dir ../quadratic-client/src/app/quadratic-core --weak-refs # Export TS/Rust types +WORKDIR /app/quadratic-core RUN echo 'Exporting TS/Rust types...' && cargo run --bin export_types # Build the quadratic-rust-client @@ -43,11 +55,19 @@ ARG GIT_COMMIT ENV GIT_COMMIT=$GIT_COMMIT RUN echo 'Building quadratic-rust-client...' && npm run build --workspace=quadratic-rust-client +# Build the quadratic-shared +WORKDIR /app +RUN echo 'Building quadratic-shared...' && npm run compile --workspace=quadratic-shared + # Build the front-end WORKDIR /app RUN echo 'Building front-end...' -RUN npm ci -RUN npx tsc ./quadratic-shared/*.ts +ENV VITE_DEBUG=VITE_DEBUG_VAL +ENV VITE_QUADRATIC_API_URL=VITE_QUADRATIC_API_URL_VAL +ENV VITE_QUADRATIC_MULTIPLAYER_URL=VITE_QUADRATIC_MULTIPLAYER_URL_VAL +ENV VITE_QUADRATIC_CONNECTION_URL=VITE_QUADRATIC_CONNECTION_URL_VAL +ENV VITE_AUTH_TYPE=VITE_AUTH_TYPE_VAL +ENV VITE_ORY_HOST=VITE_ORY_HOST_VAL RUN npm run build --workspace=quadratic-client # The default command to run the application @@ -58,4 +78,11 @@ COPY --from=build /app/build /usr/share/nginx/html EXPOSE 80 443 3000 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] + + + + + + + diff --git a/dev/control.js b/dev/control.js index 3b746105db..17395f08d9 100644 --- a/dev/control.js +++ b/dev/control.js @@ -200,6 +200,7 @@ export class Control { togglePerf() { this.cli.options.perf = !this.cli.options.perf; this.restartCore(); + this.restartRustClient(); } async runCore(restart) { if (this.quitting) @@ -445,7 +446,7 @@ export class Control { this.signals.rustClient = new AbortController(); this.rustClient = spawn("npm", [ "run", - this.cli.options.rustClient ? "dev" : "build", + this.cli.options.rustClient ? (this.cli.options.perf ? "dev:perf" : "dev") : "build", "--workspace=quadratic-rust-client", ], { signal: this.signals.rustClient.signal }); this.ui.printOutput("rustClient", (data) => this.handleResponse("rustClient", data, { diff --git a/dev/control.ts b/dev/control.ts index b68cfd23d8..3f520ada5a 100644 --- a/dev/control.ts +++ b/dev/control.ts @@ -241,6 +241,7 @@ export class Control { togglePerf() { this.cli.options.perf = !this.cli.options.perf; this.restartCore(); + this.restartRustClient(); } async runCore(restart?: boolean) { @@ -530,7 +531,7 @@ export class Control { "npm", [ "run", - this.cli.options.rustClient ? "dev" : "build", + this.cli.options.rustClient ? (this.cli.options.perf ? "dev:perf" :"dev") : "build", "--workspace=quadratic-rust-client", ], { signal: this.signals.rustClient.signal } diff --git a/dev/help.js b/dev/help.js index 6a5489e003..650cf444cf 100644 --- a/dev/help.js +++ b/dev/help.js @@ -12,7 +12,7 @@ export const helpCLI = "\n\nOptions:" + "\n -e, --rustClient Watch the quadratic-rust-client directory" + "\n -s, --skipTypes Skip WASM types compilation" + "\n -l, --all Watch all directories" + - "\n -p, --perf Run quadratic-core in perf mode (slower linking but faster runtime)" + + "\n -p, --perf Run quadratic-core and rust-client in perf mode (slower linking but faster runtime)" + "\n -R, --hideReact Hide React output" + "\n -A, --hideApi Hide API output" + "\n -C, --hideCore Hide Core output" + @@ -29,7 +29,7 @@ export const helpCLI = "\n\nOptions:" + export const helpKeyboard = "\n\nPress:" + "\n a c e m f o y - Toggle watch for component" + "\n A C T E M F O Y - Toggle showing logs for component" + - "\n p - Toggle performance build for Core" + + "\n p - Toggle performance build for Core and RustClient" + "\n r - Restart React" + "\n t - Rebuild WASM types from Core for React" + "\n l - Watch all" + diff --git a/dev/help.ts b/dev/help.ts index 0948bd5dff..647d44f82e 100644 --- a/dev/help.ts +++ b/dev/help.ts @@ -13,7 +13,7 @@ export const helpCLI = "\n -e, --rustClient Watch the quadratic-rust-client directory" + "\n -s, --skipTypes Skip WASM types compilation" + "\n -l, --all Watch all directories" + - "\n -p, --perf Run quadratic-core in perf mode (slower linking but faster runtime)" + + "\n -p, --perf Run quadratic-core and rust-client in perf mode (slower linking but faster runtime)" + "\n -R, --hideReact Hide React output" + "\n -A, --hideApi Hide API output" + "\n -C, --hideCore Hide Core output" + @@ -32,7 +32,7 @@ export const helpKeyboard = "\n\nPress:" + "\n a c e m f o y - Toggle watch for component" + "\n A C T E M F O Y - Toggle showing logs for component" + - "\n p - Toggle performance build for Core" + + "\n p - Toggle performance build for Core and RustClient" + "\n r - Restart React" + "\n t - Rebuild WASM types from Core for React" + "\n l - Watch all" + diff --git a/dev/ui.js b/dev/ui.js index 0580ded8eb..a554f30ee7 100644 --- a/dev/ui.js +++ b/dev/ui.js @@ -106,7 +106,7 @@ export class UI { else { this.write(" " + DONE, "green"); } - if (component === "core" && this.cli.options.perf) { + if ((component === "core" && this.cli.options.perf) || (component === "rustClient" && this.cli.options.perf && this.cli.options.rustClient)) { this.write(PERF); } this.write(SPACE); diff --git a/dev/ui.ts b/dev/ui.ts index 918d4bf127..85c19978b6 100644 --- a/dev/ui.ts +++ b/dev/ui.ts @@ -129,7 +129,7 @@ export class UI { } else { this.write(" " + DONE, "green"); } - if (component === "core" && this.cli.options.perf) { + if ((component === "core" && this.cli.options.perf) || (component === "rustClient" && this.cli.options.perf && this.cli.options.rustClient)) { this.write(PERF); } this.write(SPACE); diff --git a/docker-compose.base.yml b/docker-compose.base.yml index 5c36d88738..278c217a08 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -23,7 +23,6 @@ services: POSTGRES_USER: postgres PGUSER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_DB: postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] interval: 10s diff --git a/docker-compose.yml b/docker-compose.yml index 3c8229bac7..db29d07208 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,8 +14,11 @@ services: extends: file: docker-compose.base.yml service: postgres + environment: + ADDITIONAL_DATABASES: kratos volumes: - ./docker/postgres/data:/var/lib/postgresql/data + - ./docker/postgres/scripts:/docker-entrypoint-initdb.d profiles: - base @@ -83,14 +86,13 @@ services: build: context: . dockerfile: client.Dockerfile - env_file: - - quadratic-client/.env.local - - quadratic-client/.env.docker - # override env vars here environment: + VITE_DEBUG: 1 VITE_QUADRATIC_API_URL: http://localhost:8000 - VITE_QUADRATIC_MULTIPLAYER_URL: ws://localhost:3001 - VITE_QUADRATIC_CONNECTION_URL: http://0.0.0.0:3003 + VITE_QUADRATIC_MULTIPLAYER_URL: ws://localhost:3001/ws + VITE_QUADRATIC_CONNECTION_URL: http://localhost:3003 + VITE_AUTH_TYPE: ory + VITE_ORY_HOST: http://localhost:4433 restart: "always" ports: # - "3000:3000" @@ -209,10 +211,82 @@ services: # condition: service_healthy # quadratic-api: # condition: service_started + profiles: - backend - quadratic-connection + # Auth Providers + + ory-auth: + image: oryd/kratos:v1.2.0 + ports: + - "4433:4433" # public + - "4434:4434" # admin + command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier + volumes: + - ./docker/ory-auth/config:/etc/config/kratos + environment: + DSN: postgresql://postgres:postgres@host.docker.internal:5432/kratos?sslmode=disable + LOG_LEVEL: trace + restart: unless-stopped + depends_on: + - postgres + - ory-auth-migrate + profiles: + - ory + - all + networks: + - host + + ory-auth-migrate: + image: oryd/kratos:v1.2.0 + command: migrate -c /etc/config/kratos/kratos.yml sql -e --yes + volumes: + - ./docker/ory-auth/config:/etc/config/kratos + environment: + DSN: postgresql://postgres:postgres@host.docker.internal:5432/kratos?sslmode=disable + restart: on-failure + depends_on: + - postgres + profiles: + - ory + - all + networks: + - host + + ory-auth-node: + image: oryd/kratos-selfservice-ui-node:v1.2.0 + ports: + - "4455:4455" + environment: + PORT: 4455 + SECURITY_MODE: + KRATOS_PUBLIC_URL: http://host.docker.internal:4433/ + KRATOS_BROWSER_URL: http://localhost:4433/ + COOKIE_SECRET: changeme + CSRF_COOKIE_NAME: ory_csrf_ui + CSRF_COOKIE_SECRET: changeme + restart: on-failure + profiles: + - ory + - all + networks: + - host + + ory-auth-mail: + image: oryd/mailslurper:latest-smtps + ports: + - "1025:1025" + - "4436:4436" + - "4437:4437" + - "8080:8080" + profiles: + - ory + - all + networks: + - host + # Databases to be used for testing by the connection service postgres-connection: diff --git a/docker/ory-auth/config/identity.schema.json b/docker/ory-auth/config/identity.schema.json new file mode 100644 index 0000000000..a953fc68ec --- /dev/null +++ b/docker/ory-auth/config/identity.schema.json @@ -0,0 +1,47 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "name": { + "type": "object", + "properties": { + "first": { + "title": "First Name", + "type": "string" + }, + "last": { + "title": "Last Name", + "type": "string" + } + } + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/docker/ory-auth/config/kratos.yml b/docker/ory-auth/config/kratos.yml new file mode 100644 index 0000000000..c815b5713f --- /dev/null +++ b/docker/ory-auth/config/kratos.yml @@ -0,0 +1,133 @@ +# https://raw.githubusercontent.com/ory/kratos/v1.2.0/.schemastore/config.schema.json +version: v1.2.0 + +dsn: memory + +serve: + public: + base_url: http://localhost:4433/ + cors: + enabled: true + allowed_origins: + - http://localhost:3000 + allowed_methods: + - POST + - GET + - PUT + - PATCH + - DELETE + allowed_headers: + - Authorization + - Access-Control-Allow-Origin + - Cookie + - Content-Type + exposed_headers: + - Content-Type + - Set-Cookie + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://localhost:3000 + allowed_return_urls: + - http://localhost + - http://localhost:4455 + - http://localhost:3000 + - http://localhost:19006/Callback + - exp://localhost:8081/--/Callback + + methods: + password: + enabled: true + totp: + config: + issuer: Kratos + enabled: true + lookup_secret: + enabled: true + link: + enabled: true + code: + enabled: true + + flows: + error: + ui_url: http://localhost:4455/error + + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 15m + required_aal: highest_available + + recovery: + enabled: true + ui_url: http://localhost:4455/recovery + use: link + + verification: + # we disable verification for self-hosting + enabled: false + ui_url: http://localhost:4455/verification + use: link + after: + default_browser_return_url: http://localhost:3000 + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + login: + ui_url: http://localhost:4455/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://localhost:4455/registration + after: + default_browser_return_url: http://localhost:3000/login-result + password: + default_browser_return_url: http://localhost:3000/login-result + hooks: + - hook: session + - hook: show_verification_ui + +session: + whoami: + tokenizer: + templates: + jwt_template: + jwks_url: http://host.docker.internal:3000/.well-known/jwks.json + # claims_mapper_url: base64://... # A JsonNet template for modifying the claims + ttl: 24h # 24 hours (defaults to 10 minutes) + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + cipher: + - 32-LONG-SECRET-NOT-SECURE-AT-ALL + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + algorithm: bcrypt + bcrypt: + cost: 8 + +identity: + default_schema_id: default + schemas: + - id: default + url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@host.docker.internal:1025/?skip_ssl_verify=true + +feature_flags: + use_continue_with_transitions: true \ No newline at end of file diff --git a/docker/postgres/scripts/init.sh b/docker/postgres/scripts/init.sh new file mode 100755 index 0000000000..5e5b12df77 --- /dev/null +++ b/docker/postgres/scripts/init.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e +set -u + +function create_user_and_database() { + local database=$1 + echo "Creating database '$database' with user '$POSTGRES_USER'" + psql -c "CREATE DATABASE $database;" || { echo "Failed to create database '$database'"; exit 1; } + echo "Database '$database' created" +} + +if [ -n "$ADDITIONAL_DATABASES" ]; then + for i in ${ADDITIONAL_DATABASES//,/ } + do + create_user_and_database $i + done +fi diff --git a/docker/snowflake-connection/data/cache/machine.json b/docker/snowflake-connection/data/cache/machine.json new file mode 100644 index 0000000000..32a03c0569 --- /dev/null +++ b/docker/snowflake-connection/data/cache/machine.json @@ -0,0 +1 @@ +{"machine_id": "gen_49a3c8c6-017"} \ No newline at end of file diff --git a/docker/snowflake-connection/data/cache/server.test.pem b/docker/snowflake-connection/data/cache/server.test.pem new file mode 100644 index 0000000000..59edcd2b5b --- /dev/null +++ b/docker/snowflake-connection/data/cache/server.test.pem @@ -0,0 +1,169 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPW8q9f0r+EoF8 +5PzNkhLel7JVliMPTLkCngU5W/cloPyQ7Erpq6MVMQzy5ESsM9AcRrTNYXVj6bm9 +MBCLU2Gvik0SJ6UeGu3nQZtnnW56bEnuJNldwtCu+l2YdOEPkb/5zTMPnZxS7taS +rthj0nomwdcBvfTaQtWixCycIXLKoJx+M06sauHnuofANpSNwYN0RlJHxcT0/ePc +rgxtA0ZmArrYuG+Vw9Gy0rJx1yobMjlg5WXNiQrpE/uUUWT/EncG7nbS2eMT7325 +98ZkFU9aNTrbEuCzBxgSp54SudjgwJz+CncgDqRiRFR7FxdgvdKNIJb/HyiN8s5T +cWzCQBdPAgMBAAECggEAGv/7SqhoBeQ3+yC/8C6MiXJcNLu7bfMSBg64ZGsep8Yq +DN7PtFR2hDxiUMA7VubaOsxUH4gIpo1Y85LuHI4rYpWSCoKiA+UCxEFtMFU1/Pfb +uogOy6Ah1x7fkAnsAkB6rFa1RtvBbqUNyIS+xWSzJhfIXMA0wTTBp5N+sYfDcDG0 +myrpuT5H84Is9vJmQytUhtp4uSaPERNj12HBpcADsH+Bk6ez+CLTiSvwVLCkaK+E +zGRes4868K5Lq2pj/msDxZa0YjkV1BU1jAve55O1zVaqZANPWjn8yKhvfgAVV4sP +p7/ORUrzeSA9X/HLbzTdOL/Kv3oCJRsEL2RJAiGS9QKBgQDrTwF6hOulGLWGJPJi +7Nbgz/tEdUhtiKE6i3phRKuYfqVKEg4x4hGw7RY2+V7Is22lx55MzdSFBNy8sGGs +Ij2N/qm61lBXhHJaELK5sZ2mnHf6nMJRBrJDsoFAe0YV5VhTYtq9Yw45AA9nCIw/ +z0pDCbcg3iTzw1zugkGs09GnKwKBgQDhl5tmYbnKYdkO1SOrEGvtSjCjIColh7zb +ZJ+e32ThgJxzueRQ77c83e556/gCkT6GT8oDJXn6qrcAriVJ7jdRUnkGLVy9Tzqc +mJ9dQxsssS97WhaPt5Ipv+LsIsCQm/eWubJusRmn72SBrxsoaqNhvu4tzqLjp45p +JGfhsr6+bQKBgQDJYAWt6o8X7Tt8H6ZnzrReFN++SHjBdIo2ZiNHltMbYFboOud2 +/TeSqHO4fFUHgba2h00MAaJ8bBrUSEZuX6c6G9T5lmuPWkPanCu4Cy8V5RYwnXMW +kJqCoQNIQbdLCck7I4B7T4hec5S64m/UM/wjvu6/7BzHmEuxujumQmhLnQKBgAgn +k816oN2o9dCscbKgUFZuhR2QbxWWN4RyubZjeuEP5hfk01T9pVEE8LblibyGBY2T +WskMVMFz5FOY9+4ZN1SwN4G6qAyLzaGVfsU/RL8z1HSQCBq/1v+9WPWSOAXCLYv8 +QG/x5OyGIcrySngGistgvHlZa9fw2ZwBXePxsyVtAoGAIfNEpG8Wmzz+O0cWUDgt +CRuN6Gcx+RsUMBkhMPkbGXw0UFQq94KCPkUcU3q9LWw6PhJ5XUxw93koljoYdkfp +JdaAvBscSGIK/d+RHtNlQuwbUFBD0SZfNyOdTNXK1TW7JFagy24law+3xcr8BjHG +CAFPNoOWtBM/qVl/+jzB5iY= +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIMjjCCCnagAwIBAgIRAO751+shvgIfZO3XzfgeF3gwDQYJKoZIhvcNAQEMBQAw +SzELMAkGA1UEBhMCQVQxEDAOBgNVBAoTB1plcm9TU0wxKjAoBgNVBAMTIVplcm9T +U0wgUlNBIERvbWFpbiBTZWN1cmUgU2l0ZSBDQTAeFw0yNDA5MDUwMDAwMDBaFw0y +NDEyMDQyMzU5NTlaMCUxIzAhBgNVBAMTGmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNs +b3VkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz1vKvX9K/hKBfOT8 +zZIS3peyVZYjD0y5Ap4FOVv3JaD8kOxK6aujFTEM8uRErDPQHEa0zWF1Y+m5vTAQ +i1Nhr4pNEielHhrt50GbZ51uemxJ7iTZXcLQrvpdmHThD5G/+c0zD52cUu7Wkq7Y +Y9J6JsHXAb302kLVosQsnCFyyqCcfjNOrGrh57qHwDaUjcGDdEZSR8XE9P3j3K4M +bQNGZgK62LhvlcPRstKycdcqGzI5YOVlzYkK6RP7lFFk/xJ3Bu520tnjE+99uffG +ZBVPWjU62xLgswcYEqeeErnY4MCc/gp3IA6kYkRUexcXYL3SjSCW/x8ojfLOU3Fs +wkAXTwIDAQABo4IIkTCCCI0wHwYDVR0jBBgwFoAUyNl4aKLZGWjVPXLeXwo+3LWG +hqYwHQYDVR0OBBYEFKTGMH+/7ToKjQk2P2ZFSmZrJ1/ZMA4GA1UdDwEB/wQEAwIF +oDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBJ +BgNVHSAEQjBAMDQGCysGAQQBsjEBAgJOMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8v +c2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBiAYIKwYBBQUHAQEEfDB6MEsGCCsG +AQUFBzAChj9odHRwOi8vemVyb3NzbC5jcnQuc2VjdGlnby5jb20vWmVyb1NTTFJT +QURvbWFpblNlY3VyZVNpdGVDQS5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly96ZXJv +c3NsLm9jc3Auc2VjdGlnby5jb20wggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdwB2 +/4g/Crb7lVHCYcz1h7o0tKTNuyncaEIKn+ZnTFo6dAAAAZHDW8C3AAAEAwBIMEYC +IQDx2ITaoZMYCR/GFCihmAZZGZCE904XyeIyk8DM7rQhJgIhANYZv74TdKeYS5fW +XryiwmxU2oWEAIy/2CIIqPXrYcyoAHUAPxdLT9ciR1iUHWUchL4NEu2QN38fhWrr +wb8ohez4ZG4AAAGRw1vAcwAABAMARjBEAiBP2zRB0e+sAlX2Z2kjEn9gYzhFPpSW +pqPXqyWCIxb/vAIgFin9WqFSFh/QC23dlChnDVVhZZgOjbmgmWUCEqIW8iowggYu +BgNVHREEggYlMIIGIYIabG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCJyouYW1w +bGlmeWFwcC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIInKi5jbG91ZGZyb250 +LmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgjEqLmRrci5lY3IuZXUtY2VudHJh +bC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IuZXUtd2Vz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtZWFz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtZWFz +dC0yLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtd2Vz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtd2Vz +dC0yLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgiAqLmVsYi5sb2NhbGhvc3Qu +bG9jYWxzdGFjay5jbG91ZII0Ki5ldS1jZW50cmFsLTEub3BlbnNlYXJjaC5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5ldS13ZXN0LTEub3BlbnNlYXJjaC5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIoKi5leGVjdXRlLWFwaS5sb2NhbGhv +c3QubG9jYWxzdGFjay5jbG91ZII0Ki5sYW1iZGEtdXJsLmV1LWNlbnRyYWwtMS5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLmV1LXdlc3Qt +MS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLnVzLWVh +c3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLnVz +LWVhc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJs +LnVzLXdlc3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEt +dXJsLnVzLXdlc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIcKi5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIInKi5vcGVuc2VhcmNoLmxvY2FsaG9zdC5s +b2NhbHN0YWNrLmNsb3VkgicqLnMzLXdlYnNpdGUubG9jYWxob3N0LmxvY2Fsc3Rh +Y2suY2xvdWSCHyouczMubG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCICouc2Nt +LmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgiYqLnNub3dmbGFrZS5sb2NhbGhv +c3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy1lYXN0LTEub3BlbnNlYXJjaC5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy1lYXN0LTIub3BlbnNlYXJjaC5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy13ZXN0LTEub3BlbnNlYXJj +aC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy13ZXN0LTIub3BlbnNl +YXJjaC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIrc3FzLmV1LWNlbnRyYWwt +MS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIoc3FzLmV1LXdlc3QtMS5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIoc3FzLnVzLWVhc3QtMS5sb2NhbGhvc3Qu +bG9jYWxzdGFjay5jbG91ZIIoc3FzLnVzLWVhc3QtMi5sb2NhbGhvc3QubG9jYWxz +dGFjay5jbG91ZIIoc3FzLnVzLXdlc3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5j +bG91ZIIoc3FzLnVzLXdlc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZDAN +BgkqhkiG9w0BAQwFAAOCAgEAZasJ/uh27MthaZ9smCugqhq3Q6gVuoPYummeZCGE +RDG+f1/9bFpFCoRRMywxlLYtgXxb275V/aeauWbyNiY+YBsReXqbyfo0iyZtxpYg +zglp/p38curzfvBl9FxR8KsiKA0qlSb24jRlpU60xQaKieE0wA3oA5j3EN6LDMfL +7uBTjI+QKj6A5kGqS+zJzz2M4SeqRAp4xgGfrUrq4ByrlTKQtHrdHv7g2FNr6Ktr +6lDY++Mggm5DBupIRGkT1iNc+LESFDnHGuk63gj7RRlThCaZ1AZcqiR/SDsphyj5 +5vMqDTCs2vGRnhvH8VXF8Se1ydbKVq7Qk6nKgXyIwB2Fp1uOY0bScA6sIDynpBqT +Ry07TvYekt+jsuKD9qmm8bFpGxTMLk9JLyR3aUfKeru1cjAzoGfFChtskWK1WNiZ +CGbZF5b2VuUMw7jK8yxvpOdQ8QWKi3NuaCrUkm0RLoej1Z6zCRtUpyKeejNvrB2h +MRTc8goLzDBFMipoIgiWzD6gzdvGUDggOmbgcSdELSEhaVXYPbg+1IatP5577S0r +GSx9XUnPxVfSLp30tb4d7oGKbXx8ZybpsaTUFn0s3Y96iD4U/PxfGHRTGztcYNxJ +g7xxSZ9x/bnwpQqjPoAq+umpj4ohsiNUUsui4UJT8e6bzF8NoulC9Z6hGG1APWSc +WGo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIG1TCCBL2gAwIBAgIQbFWr29AHksedBwzYEZ7WvzANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjAw +MTMwMDAwMDAwWhcNMzAwMTI5MjM1OTU5WjBLMQswCQYDVQQGEwJBVDEQMA4GA1UE +ChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBSU0EgRG9tYWluIFNlY3VyZSBT +aXRlIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAhmlzfqO1Mdgj +4W3dpBPTVBX1AuvcAyG1fl0dUnw/MeueCWzRWTheZ35LVo91kLI3DDVaZKW+TBAs +JBjEbYmMwcWSTWYCg5334SF0+ctDAsFxsX+rTDh9kSrG/4mp6OShubLaEIUJiZo4 +t873TuSd0Wj5DWt3DtpAG8T35l/v+xrN8ub8PSSoX5Vkgw+jWf4KQtNvUFLDq8mF +WhUnPL6jHAADXpvs4lTNYwOtx9yQtbpxwSt7QJY1+ICrmRJB6BuKRt/jfDJF9Jsc +RQVlHIxQdKAJl7oaVnXgDkqtk2qddd3kCDXd74gv813G91z7CjsGyJ93oJIlNS3U +gFbD6V54JMgZ3rSmotYbz98oZxX7MKbtCm1aJ/q+hTv2YK1yMxrnfcieKmOYBbFD +hnW5O6RMA703dBK92j6XRN2EttLkQuujZgy+jXRKtaWMIlkNkWJmOiHmErQngHvt +iNkIcjJumq1ddFX4iaTI40a6zgvIBtxFeDs2RfcaH73er7ctNUUqgQT5rFgJhMmF +x76rQgB5OZUkodb5k2ex7P+Gu4J86bS15094UuYcV09hVeknmTh5Ex9CBKipLS2W +2wKBakf+aVYnNCU6S0nASqt2xrZpGC1v7v6DhuepyyJtn3qSV2PoBiU5Sql+aARp +wUibQMGm44gjyNDqDlVp+ShLQlUH9x8CAwEAAaOCAXUwggFxMB8GA1UdIwQYMBaA +FFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBTI2XhootkZaNU9ct5fCj7c +tYaGpjAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUE +FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwIgYDVR0gBBswGTANBgsrBgEEAbIxAQIC +TjAIBgZngQwBAgEwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1 +c3QuY29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYG +CCsGAQUFBwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3Qu +Y29tL1VTRVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRw +Oi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQAVDwoIzQDV +ercT0eYqZjBNJ8VNWwVFlQOtZERqn5iWnEVaLZZdzxlbvz2Fx0ExUNuUEgYkIVM4 +YocKkCQ7hO5noicoq/DrEYH5IuNcuW1I8JJZ9DLuB1fYvIHlZ2JG46iNbVKA3ygA +Ez86RvDQlt2C494qqPVItRjrz9YlJEGT0DrttyApq0YLFDzf+Z1pkMhh7c+7fXeJ +qmIhfJpduKc8HEQkYQQShen426S3H0JrIAbKcBCiyYFuOhfyvuwVCFDfFvrjADjd +4jX1uQXd161IyFRbm89s2Oj5oU1wDYz5sx+hoCuh6lSs+/uPuWomIq3y1GDFNafW ++LsHBU16lQo5Q2yh25laQsKRgyPmMpHJ98edm6y2sHUabASmRHxvGiuwwE25aDU0 +2SAeepyImJ2CzB80YG7WxlynHqNhpE7xfC7PzQlLgmfEHdU+tHFeQazRQnrFkW2W +kqRGIq7cKRnyypvjPMkjeiV9lRdAM9fSJvsB3svUuu1coIG1xxI1yegoGM4r5QP4 +RGIVvYaiI76C0djoSbQ/dkIUUXQuB8AL5jyH34g3BZaaXyvpmnV4ilppMXVAnAYG +ON51WhJ6W0xNdNJwzYASZYH+tmCWI+N60Gv2NNMGHwMZ7e9bXgzUCZH5FaBFDGR5 +S9VWqHB73Q+OyIVvIbKYcSc2w/aSuFKGSA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7 +MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE +AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 +MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 +MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO +ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI +s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG +vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ +Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb +IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0 +tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E +xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV +icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5 +D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ +WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ +5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG +KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg +EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID +ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG +BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t +L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr +BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA +A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+ +rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+ +/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA +CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F +zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA +vGp4z7h/jnZymQyd/teRCBaho1+V +-----END CERTIFICATE----- diff --git a/docker/snowflake-connection/data/cache/server.test.pem.crt b/docker/snowflake-connection/data/cache/server.test.pem.crt new file mode 100644 index 0000000000..846abd9a7b --- /dev/null +++ b/docker/snowflake-connection/data/cache/server.test.pem.crt @@ -0,0 +1,141 @@ +-----BEGIN CERTIFICATE----- +MIIMjjCCCnagAwIBAgIRAO751+shvgIfZO3XzfgeF3gwDQYJKoZIhvcNAQEMBQAw +SzELMAkGA1UEBhMCQVQxEDAOBgNVBAoTB1plcm9TU0wxKjAoBgNVBAMTIVplcm9T +U0wgUlNBIERvbWFpbiBTZWN1cmUgU2l0ZSBDQTAeFw0yNDA5MDUwMDAwMDBaFw0y +NDEyMDQyMzU5NTlaMCUxIzAhBgNVBAMTGmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNs +b3VkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz1vKvX9K/hKBfOT8 +zZIS3peyVZYjD0y5Ap4FOVv3JaD8kOxK6aujFTEM8uRErDPQHEa0zWF1Y+m5vTAQ +i1Nhr4pNEielHhrt50GbZ51uemxJ7iTZXcLQrvpdmHThD5G/+c0zD52cUu7Wkq7Y +Y9J6JsHXAb302kLVosQsnCFyyqCcfjNOrGrh57qHwDaUjcGDdEZSR8XE9P3j3K4M +bQNGZgK62LhvlcPRstKycdcqGzI5YOVlzYkK6RP7lFFk/xJ3Bu520tnjE+99uffG +ZBVPWjU62xLgswcYEqeeErnY4MCc/gp3IA6kYkRUexcXYL3SjSCW/x8ojfLOU3Fs +wkAXTwIDAQABo4IIkTCCCI0wHwYDVR0jBBgwFoAUyNl4aKLZGWjVPXLeXwo+3LWG +hqYwHQYDVR0OBBYEFKTGMH+/7ToKjQk2P2ZFSmZrJ1/ZMA4GA1UdDwEB/wQEAwIF +oDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBJ +BgNVHSAEQjBAMDQGCysGAQQBsjEBAgJOMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8v +c2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBiAYIKwYBBQUHAQEEfDB6MEsGCCsG +AQUFBzAChj9odHRwOi8vemVyb3NzbC5jcnQuc2VjdGlnby5jb20vWmVyb1NTTFJT +QURvbWFpblNlY3VyZVNpdGVDQS5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly96ZXJv +c3NsLm9jc3Auc2VjdGlnby5jb20wggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdwB2 +/4g/Crb7lVHCYcz1h7o0tKTNuyncaEIKn+ZnTFo6dAAAAZHDW8C3AAAEAwBIMEYC +IQDx2ITaoZMYCR/GFCihmAZZGZCE904XyeIyk8DM7rQhJgIhANYZv74TdKeYS5fW +XryiwmxU2oWEAIy/2CIIqPXrYcyoAHUAPxdLT9ciR1iUHWUchL4NEu2QN38fhWrr +wb8ohez4ZG4AAAGRw1vAcwAABAMARjBEAiBP2zRB0e+sAlX2Z2kjEn9gYzhFPpSW +pqPXqyWCIxb/vAIgFin9WqFSFh/QC23dlChnDVVhZZgOjbmgmWUCEqIW8iowggYu +BgNVHREEggYlMIIGIYIabG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCJyouYW1w +bGlmeWFwcC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIInKi5jbG91ZGZyb250 +LmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgjEqLmRrci5lY3IuZXUtY2VudHJh +bC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IuZXUtd2Vz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtZWFz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtZWFz +dC0yLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtd2Vz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtd2Vz +dC0yLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgiAqLmVsYi5sb2NhbGhvc3Qu +bG9jYWxzdGFjay5jbG91ZII0Ki5ldS1jZW50cmFsLTEub3BlbnNlYXJjaC5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5ldS13ZXN0LTEub3BlbnNlYXJjaC5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIoKi5leGVjdXRlLWFwaS5sb2NhbGhv +c3QubG9jYWxzdGFjay5jbG91ZII0Ki5sYW1iZGEtdXJsLmV1LWNlbnRyYWwtMS5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLmV1LXdlc3Qt +MS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLnVzLWVh +c3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLnVz +LWVhc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJs +LnVzLXdlc3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEt +dXJsLnVzLXdlc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIcKi5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIInKi5vcGVuc2VhcmNoLmxvY2FsaG9zdC5s +b2NhbHN0YWNrLmNsb3VkgicqLnMzLXdlYnNpdGUubG9jYWxob3N0LmxvY2Fsc3Rh +Y2suY2xvdWSCHyouczMubG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCICouc2Nt +LmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgiYqLnNub3dmbGFrZS5sb2NhbGhv +c3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy1lYXN0LTEub3BlbnNlYXJjaC5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy1lYXN0LTIub3BlbnNlYXJjaC5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy13ZXN0LTEub3BlbnNlYXJj +aC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy13ZXN0LTIub3BlbnNl +YXJjaC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIrc3FzLmV1LWNlbnRyYWwt +MS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIoc3FzLmV1LXdlc3QtMS5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIoc3FzLnVzLWVhc3QtMS5sb2NhbGhvc3Qu +bG9jYWxzdGFjay5jbG91ZIIoc3FzLnVzLWVhc3QtMi5sb2NhbGhvc3QubG9jYWxz +dGFjay5jbG91ZIIoc3FzLnVzLXdlc3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5j +bG91ZIIoc3FzLnVzLXdlc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZDAN +BgkqhkiG9w0BAQwFAAOCAgEAZasJ/uh27MthaZ9smCugqhq3Q6gVuoPYummeZCGE +RDG+f1/9bFpFCoRRMywxlLYtgXxb275V/aeauWbyNiY+YBsReXqbyfo0iyZtxpYg +zglp/p38curzfvBl9FxR8KsiKA0qlSb24jRlpU60xQaKieE0wA3oA5j3EN6LDMfL +7uBTjI+QKj6A5kGqS+zJzz2M4SeqRAp4xgGfrUrq4ByrlTKQtHrdHv7g2FNr6Ktr +6lDY++Mggm5DBupIRGkT1iNc+LESFDnHGuk63gj7RRlThCaZ1AZcqiR/SDsphyj5 +5vMqDTCs2vGRnhvH8VXF8Se1ydbKVq7Qk6nKgXyIwB2Fp1uOY0bScA6sIDynpBqT +Ry07TvYekt+jsuKD9qmm8bFpGxTMLk9JLyR3aUfKeru1cjAzoGfFChtskWK1WNiZ +CGbZF5b2VuUMw7jK8yxvpOdQ8QWKi3NuaCrUkm0RLoej1Z6zCRtUpyKeejNvrB2h +MRTc8goLzDBFMipoIgiWzD6gzdvGUDggOmbgcSdELSEhaVXYPbg+1IatP5577S0r +GSx9XUnPxVfSLp30tb4d7oGKbXx8ZybpsaTUFn0s3Y96iD4U/PxfGHRTGztcYNxJ +g7xxSZ9x/bnwpQqjPoAq+umpj4ohsiNUUsui4UJT8e6bzF8NoulC9Z6hGG1APWSc +WGo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIG1TCCBL2gAwIBAgIQbFWr29AHksedBwzYEZ7WvzANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjAw +MTMwMDAwMDAwWhcNMzAwMTI5MjM1OTU5WjBLMQswCQYDVQQGEwJBVDEQMA4GA1UE +ChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBSU0EgRG9tYWluIFNlY3VyZSBT +aXRlIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAhmlzfqO1Mdgj +4W3dpBPTVBX1AuvcAyG1fl0dUnw/MeueCWzRWTheZ35LVo91kLI3DDVaZKW+TBAs +JBjEbYmMwcWSTWYCg5334SF0+ctDAsFxsX+rTDh9kSrG/4mp6OShubLaEIUJiZo4 +t873TuSd0Wj5DWt3DtpAG8T35l/v+xrN8ub8PSSoX5Vkgw+jWf4KQtNvUFLDq8mF +WhUnPL6jHAADXpvs4lTNYwOtx9yQtbpxwSt7QJY1+ICrmRJB6BuKRt/jfDJF9Jsc +RQVlHIxQdKAJl7oaVnXgDkqtk2qddd3kCDXd74gv813G91z7CjsGyJ93oJIlNS3U +gFbD6V54JMgZ3rSmotYbz98oZxX7MKbtCm1aJ/q+hTv2YK1yMxrnfcieKmOYBbFD +hnW5O6RMA703dBK92j6XRN2EttLkQuujZgy+jXRKtaWMIlkNkWJmOiHmErQngHvt +iNkIcjJumq1ddFX4iaTI40a6zgvIBtxFeDs2RfcaH73er7ctNUUqgQT5rFgJhMmF +x76rQgB5OZUkodb5k2ex7P+Gu4J86bS15094UuYcV09hVeknmTh5Ex9CBKipLS2W +2wKBakf+aVYnNCU6S0nASqt2xrZpGC1v7v6DhuepyyJtn3qSV2PoBiU5Sql+aARp +wUibQMGm44gjyNDqDlVp+ShLQlUH9x8CAwEAAaOCAXUwggFxMB8GA1UdIwQYMBaA +FFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBTI2XhootkZaNU9ct5fCj7c +tYaGpjAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUE +FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwIgYDVR0gBBswGTANBgsrBgEEAbIxAQIC +TjAIBgZngQwBAgEwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1 +c3QuY29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYG +CCsGAQUFBwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3Qu +Y29tL1VTRVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRw +Oi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQAVDwoIzQDV +ercT0eYqZjBNJ8VNWwVFlQOtZERqn5iWnEVaLZZdzxlbvz2Fx0ExUNuUEgYkIVM4 +YocKkCQ7hO5noicoq/DrEYH5IuNcuW1I8JJZ9DLuB1fYvIHlZ2JG46iNbVKA3ygA +Ez86RvDQlt2C494qqPVItRjrz9YlJEGT0DrttyApq0YLFDzf+Z1pkMhh7c+7fXeJ +qmIhfJpduKc8HEQkYQQShen426S3H0JrIAbKcBCiyYFuOhfyvuwVCFDfFvrjADjd +4jX1uQXd161IyFRbm89s2Oj5oU1wDYz5sx+hoCuh6lSs+/uPuWomIq3y1GDFNafW ++LsHBU16lQo5Q2yh25laQsKRgyPmMpHJ98edm6y2sHUabASmRHxvGiuwwE25aDU0 +2SAeepyImJ2CzB80YG7WxlynHqNhpE7xfC7PzQlLgmfEHdU+tHFeQazRQnrFkW2W +kqRGIq7cKRnyypvjPMkjeiV9lRdAM9fSJvsB3svUuu1coIG1xxI1yegoGM4r5QP4 +RGIVvYaiI76C0djoSbQ/dkIUUXQuB8AL5jyH34g3BZaaXyvpmnV4ilppMXVAnAYG +ON51WhJ6W0xNdNJwzYASZYH+tmCWI+N60Gv2NNMGHwMZ7e9bXgzUCZH5FaBFDGR5 +S9VWqHB73Q+OyIVvIbKYcSc2w/aSuFKGSA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7 +MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE +AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 +MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 +MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO +ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI +s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG +vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ +Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb +IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0 +tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E +xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV +icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5 +D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ +WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ +5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG +KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg +EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID +ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG +BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t +L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr +BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA +A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+ +rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+ +/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA +CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F +zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA +vGp4z7h/jnZymQyd/teRCBaho1+V +-----END CERTIFICATE----- \ No newline at end of file diff --git a/docker/snowflake-connection/data/cache/server.test.pem.key b/docker/snowflake-connection/data/cache/server.test.pem.key new file mode 100644 index 0000000000..f4e1c0b9cc --- /dev/null +++ b/docker/snowflake-connection/data/cache/server.test.pem.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPW8q9f0r+EoF8 +5PzNkhLel7JVliMPTLkCngU5W/cloPyQ7Erpq6MVMQzy5ESsM9AcRrTNYXVj6bm9 +MBCLU2Gvik0SJ6UeGu3nQZtnnW56bEnuJNldwtCu+l2YdOEPkb/5zTMPnZxS7taS +rthj0nomwdcBvfTaQtWixCycIXLKoJx+M06sauHnuofANpSNwYN0RlJHxcT0/ePc +rgxtA0ZmArrYuG+Vw9Gy0rJx1yobMjlg5WXNiQrpE/uUUWT/EncG7nbS2eMT7325 +98ZkFU9aNTrbEuCzBxgSp54SudjgwJz+CncgDqRiRFR7FxdgvdKNIJb/HyiN8s5T +cWzCQBdPAgMBAAECggEAGv/7SqhoBeQ3+yC/8C6MiXJcNLu7bfMSBg64ZGsep8Yq +DN7PtFR2hDxiUMA7VubaOsxUH4gIpo1Y85LuHI4rYpWSCoKiA+UCxEFtMFU1/Pfb +uogOy6Ah1x7fkAnsAkB6rFa1RtvBbqUNyIS+xWSzJhfIXMA0wTTBp5N+sYfDcDG0 +myrpuT5H84Is9vJmQytUhtp4uSaPERNj12HBpcADsH+Bk6ez+CLTiSvwVLCkaK+E +zGRes4868K5Lq2pj/msDxZa0YjkV1BU1jAve55O1zVaqZANPWjn8yKhvfgAVV4sP +p7/ORUrzeSA9X/HLbzTdOL/Kv3oCJRsEL2RJAiGS9QKBgQDrTwF6hOulGLWGJPJi +7Nbgz/tEdUhtiKE6i3phRKuYfqVKEg4x4hGw7RY2+V7Is22lx55MzdSFBNy8sGGs +Ij2N/qm61lBXhHJaELK5sZ2mnHf6nMJRBrJDsoFAe0YV5VhTYtq9Yw45AA9nCIw/ +z0pDCbcg3iTzw1zugkGs09GnKwKBgQDhl5tmYbnKYdkO1SOrEGvtSjCjIColh7zb +ZJ+e32ThgJxzueRQ77c83e556/gCkT6GT8oDJXn6qrcAriVJ7jdRUnkGLVy9Tzqc +mJ9dQxsssS97WhaPt5Ipv+LsIsCQm/eWubJusRmn72SBrxsoaqNhvu4tzqLjp45p +JGfhsr6+bQKBgQDJYAWt6o8X7Tt8H6ZnzrReFN++SHjBdIo2ZiNHltMbYFboOud2 +/TeSqHO4fFUHgba2h00MAaJ8bBrUSEZuX6c6G9T5lmuPWkPanCu4Cy8V5RYwnXMW +kJqCoQNIQbdLCck7I4B7T4hec5S64m/UM/wjvu6/7BzHmEuxujumQmhLnQKBgAgn +k816oN2o9dCscbKgUFZuhR2QbxWWN4RyubZjeuEP5hfk01T9pVEE8LblibyGBY2T +WskMVMFz5FOY9+4ZN1SwN4G6qAyLzaGVfsU/RL8z1HSQCBq/1v+9WPWSOAXCLYv8 +QG/x5OyGIcrySngGistgvHlZa9fw2ZwBXePxsyVtAoGAIfNEpG8Wmzz+O0cWUDgt +CRuN6Gcx+RsUMBkhMPkbGXw0UFQq94KCPkUcU3q9LWw6PhJ5XUxw93koljoYdkfp +JdaAvBscSGIK/d+RHtNlQuwbUFBD0SZfNyOdTNXK1TW7JFagy24law+3xcr8BjHG +CAFPNoOWtBM/qVl/+jzB5iY= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/docker/snowflake-connection/data/cache/service-catalog-3_7_1_dev1-1_35_5.pickle b/docker/snowflake-connection/data/cache/service-catalog-3_7_1_dev1-1_35_5.pickle new file mode 100644 index 0000000000000000000000000000000000000000..939025e7090b38e39e3d6649f919736a1d2d10f6 GIT binary patch literal 635600 zcmeFa37lQWRVN%QTWd?*+9mIMY{%_r@k-WKOR^QKEm_^RLlRfaN#;AJ&Z(+Xb?Yv#?JUD* z=a<&I_x`Ku)Tyddr%u&5_4-TSG4-6!c@F-!w!77>C9T0Qshz(gxiGk6&`WDiuI-*r z`zy^_I-3lWR(JVSr=DJX^6gLF`i3W8`{XTa+XwXZ@lMiC2Ty)JyxNh}YUyB*bduKU zYtsIcYn$Ljchng^xwavxwe@p*x>rl^x!uF(cCXc3T743d(yv7zA??(kT$|z-qvqnM z*{Wl1Ta#XMIT@xG5=gM$J+z^~<+Po4hVbsmwF&oS<;at3TasR{*6l1cm!DkQZhzOI zPV{XlS?o8VwwvJFQmcF6$+b!N)$9z@<$f}3b~^)I7|>bMkJHI$*c|}7njOe-4`A(= zW@pe`ZVVy8ZuuHs0!M;uYTd!G*Y7ShTg0ed54hMMlKP`g2j3#;gJC~SH0$8^YKI9O zrUPK=WYVuqzorYBPY)W+C3v$r=?|MrNezB(OoolL1C^XeM)l^9m;{+_^XT6xo~C=P z1Z#Wg;*mw*<3?1%cjLqeL2OFE~!D{ zO|?ccT<*Un?vX<2i?ndT5l%Py=E_kBB0-{i3II59YVSA za<@zXJ$hL;_N05e(uIKmU9#3nlTN?eZ9}37_t0(u&9zo{RKHKZhaXUzVZRIScj))^ zWg412GBi#5J&<) zvCD7M4}%oC2ei^o@l@}&lV-=jhyA1p1Wjsye$cIHK3||#o~U)}kd^6ITkuhjNvs+C zhG7BY9sA=}{n%}{oA5A6573iqJc{(^dfIDsq0&3?^YW-an(n77&GZ8HclU0P)<%8Z zbW;ZB28%A$v>*&YM1bt-PXmixY5VN!P8VqB(F@H4IWBja!>;Hb+uSSY?~p$MXuJX0 z?Os2H!31InNHh=FlRymcyU{S6?)IQdH?`#3XhhB?CQ8$($wM+h(hjFvDULQEN58It zz)L_<=#FGw4XB6GV-Fjl*6bzNEp}io(4RE-yC8{xYW-r^)x6w`0>)58f~zrF1i=f8 zMUqF*huUd>n%N4aZpM+k*iS(MPT#};cR)-2~*Z)qZtz-b)z^{GydP!Z^U==2#E}|yu{FCG-eB6*O>c2G@AbZlc+ZI0xX!Vj>``rL>SvwgV_+C&`~NwwXw zJWBfY`Upk*#zva7uu-~Ah-8@yO*L8ItFkD2MG3r%cg zEKp0>bcmV;X)>5%&;;#DLomZ(11CdhiXN~Pvlw1zcG|#KC>+rjpxaPS+|XRnB(DrW znnN?_nRpYt#OWD2_W5M#JZd8PUpvw_qNdk}_dDpvG>nMzdZOI|6P@m?kn1LRl@735 zUdg|vu~&DeadrdYQlHjSI?UPrXMVfie@nWUE+okALb})=)~2zGK<5CCb3)MwyTVu4 zDJS@)CJK7Mc^o_2vHOV`ePb(WFV>-|@L|-cHKs2#Ujtni`co^tI1M5zMg4FaK3Fwq z0-jcm*pJ)k(SDwUP6QK3I&C$(8-h2}(6FP6(}U6Cpw@4~+@y8c>qFECWJ(Cy($b2o zRu4Ag;(!L+rLYVDI;UFQ?)fgvcu$O)ptkDih+nnSA!yT@1B8LjN|LD!yqau-ScK*T zrK#W1jj$2kG*kGr4HT1>t;$aNRbPa%&qF6don?RK-874%z3rqA+99Z~Ak7n$8AQ}> z`QA>H`{Y|vM+goDxNNj+ZE;3diZ7r5z`R)lfeu;^4bBPO*KEJGk07a`_ixc(TY4U( zPauZRQySfq5ts^mkB7rcuIa25)YsZO}4t zPTUQRllI}Qs10=3_A(4__zNt)s@>#8&osyl7|Xbn=tA$ni31vSxBZTvp~8AqbwIxD z!l(pojK%vN`ANS`mqA!x(tZM@LVGys!-RNg`$7UEV^b&XfjEMZx1E1Yv*_8z&$MQn z0(pC(+dn^zv(|;Ag&H-#vfanNFmicgr`cX?;e^n|3E{GCTX)i}-5w4h(v$bN2RX}b z22};igX#mEe&Skod$+%wbYK|sxVp5fh16yk7d=qD0KOSCH4r9ksN9}jvWlH-y4GLq z4ZFHxz=wme*W9pEuue9r2{b_hePfsZYEVl-TiwAVq->llCntD_FS$<_*0VCd)W7wW(rDzq<%4GCiYi z2W375+sW#M2I!K&KuA7J?>dSZ&@UK|q!>@vTA;q;G<;3=6RiU5;JiG8fEl{Zyxzed zX;}!gPVx)4Dll)b5-kvD$mJ2R{E`7swq8jF^-TL(v7*WUt5X6Qw_VteiH>EuY$ z&2Ky0`xMsnPp)0267{J-=NdIz*;9O4K(j<#t)#F!;s%F!1d>b!4vu~8Wtu7gElQgv|73y zADf+;)~I#G;F$&_D3fT?L%<+`UmFGkV8s^r3&N|7<8w3q?l$YNuG$X$x3>hU5KGS; ztan)((*`XCe0e@l8 zFM}|`etsD|V(U*{NLwwf{o?vM#clvfJB(q-5N9e_AYAUf*R<+Pj zyPT56bE}@hhiZQyJ+gMIvLzk4y0-&G!bVy2yd#bk!8vnI>yJTM} zUW%J%9-BW#_Sl(DeHJtuYDg3v$H7W;f-S$MBDZ;|_hV{pv1KOT&-Qx*Kb(P|Vk5fV zZU~#^hzGv`a-Y>k*2hpm!~W}t3zNRkOFF63&Gecf>_r}IzlJ?9Xy>v1m~2^?+yQzan(kq@#hG94fYvyC^f0u>9lGEJG|)b7-q{v9>E*U-76#EV+$2Hd z-G>hyy4xaO<{@_@a#q`C5qj65I}abV(9<3?BoPv_T8690J18UQlO(?(wQGh0ae%Qet^%4SSj6E5Ny65oWBlp>-)lF>@HyZv0>H66`##jpE0TVFiyIg05pZ zdeH+OFu-}OC=IgsRS$n9)D&h_5Z&6MK1=%*A$PvWOLHUD6OsWO>=!Vo+Z~Z?W zZVT3*jP3^T6&_&cNj9-l*Sh!6oi?Yh_JEU+)5*Dcgx%{kJ!o4(E!sg0ufHQ=xDz$o(`NG_ZoJOnb7`ExRF=Ao zB%6SY`*FL$O zAwzo&AqW4nhueRuyD$Ui4i=L}2ZsyT3Q#9>*}m6>J`s?bk)i4H*Z9@NJBJiX{AJv>g2gg*xxKibHjm++^&gi{Zijew9p z9|&-0(P(Nxg^I`#+b%!t;SN4XxEW)-$EY$>n;h(!M!%i-|&#zPlIJ1L3bWHDnP&Gfv(4L z$t-`unC|o1;kUh%J4uqvoz_JhK62PG`1d^Ejycd+xl;2hHB0SBJQU81b7w(2(Ux=} z5{?}5M8Y3=sc=S?L!Ec;%=6q9>ZFk_*T_7sl1oLNJ+TEyOCG$9P zvBsc*3h3W>P!`2=+RT3Zn62^Od#F8{_Bl4dhlKxwhs8Ny&KTlBv9eiy+=F3jO4!^8 zHr_xU;(@EMfdB2Kyn1c{Ebr%F`vye472Y@*0!I!z{qx_vR0kd+!#hr8Xm^TGT^t`sOBSpM%&4Ng64&gh`^agq*hde(D>r&g>)s_&u&tPZgwBWzn zSE|(3FageQXQas<>wC8xQfgb6=9;r)wx1PN`v z8ngPP>G6nL45)o!vR!A{JU>4-|0o*gA9XEpc?jMOwFf&&4W?8(yIYE`3eh`fE(~D( z0#*%d<3slxv5KO)Cd6)g`1J8(^I%OvleuZy8$;9t=ug@bkNUiYJuigqGO$Q@xjh25 z#`>VctLYGY-8r__-w!74-e7^O@Su>;_&$8ZD$VLhn0DtvcX_#$&hnWVNMvhpcZfZ3 zj*Ruk!LHxzX!{Dt%Ubul z=g?6%>8f*KDpbbiwUHa!=k7yyyG4z9Bn01tU@c#)JY=$rXgKt^sMyQH^r)VkIx){y z@RxNLbFE0~m0{W~G&9aWJZ~HL^YbwCa~AXJ5VLK5eoilh(9CYy?C}t_YyPogCobZl zlcW`Lz8->637rQs`$=nNH~B7w(ET{oLEqvvImzFIolCFZ=$>bZslx1H<*bmW=C*@-axl!q1nUr2D~E+mdEbRU};Q-I9k5!3}2^kAo3!t zMB#KS?s(huQz4Mje?j;Rlm>Qtu95Q))ipZ>qQ_L?!O*zrpOyVy0lL5q1oP zTYv%Y%Ywl>!MO#%VNvTaVAAYYUuPR%ItzfxKrRFZctCw~6gZ9j8NCV&@L-mTgOUn; z9+>y_T`#{pn+hEvv@e3m`HCzCJriJG9k-qh>Pq!4OQ>*MFrTkd-)TQ%3Hto7`FM4< z1jJ}B2ZOmLi^=#u80@uKtn-X8pzE?A<1=9(*Q@tfqJ1|CgSa7^iRWNpa5utP7kze? zgh9mZ!XR%_-)GAtPYmv7 zKW_7}ZXb4tJ}=CQhUo3_*VFOWcZ9D~@Hq*cLkvC755b9b8!?bWS%`HgF_^>Zr){;+ z$HcrpqJGxCw{9oqf%(46`^3z9BH z=KDkHpX_(%QDnW04D3vnB!@QVyKDu{7NIk<>cd&I=8N^AF+`kG|7K~8`!N~hy!xC= zj4GMu+GMZ`+2XTKPX_#m+GsQO{GbfrQMJtipccY4${-%gmM5L24D97<%4P(tHv8Uq zfEoR$4D9pNULS_aqjRk?uve(dEzEdZnQyOD*Vu2?0n3iRSB2!#82tS3b&B3z`i#T0 zbIvkFygEcD;DVUoDLsRDJc|gOwhT1MLV?qIn}^)F3~(_E%yINGDQfCQ%L?uLWj@r^ zt@Z;P*43VLI|WkJ5n^_IHzUo)t779~B~46LJGWQ+2g-3*|c%_ne!GsvDgX0sBW zapvVErKJIk35{cH)^jm~@@sM9VX9-FqKV<_hZ8X2F=yi&J-*phG4&@ayvG>Q=d z?ri4!O7=VY@|n*U)Wep}&BmwC5qnWR%AbkSRrN}KPRZfVIoux?oo0<=A&!*O%sOI~V>Pu`w=bUQU z^bYl9{F&PRYt-BL8HAJXuxfJrTJ_Z;N8`h4zJ8thTKiSIwVDrKufEBCaGtK_>o=%( z+OO96)qMQMY#k$Ru?GE3>N{*!ue6#v)`^7rXnvdU-EkwH2T8{n45J!yr*6=m( z4}Djh=dCH|ogoqw8RwI0Q18m3jJK|Ve4F}STOH}ZYrg*{^@H}i_USbrzFiJ*ShvE7 zOYIa*{&EKO9qK8Y+;{#pi0{nO=(`0Q;Jf_-g|o0h{AcxkTUh8nY%t%IElzqC8`yVe zVHuxegMCjHtG$uU_wUJm-vmy)+NsW9{@*Mna9}pb|B^+bKQg&CJJI#MA&h$1_l2+1 zz~9*v^Zg+Z<`yJcAJsI@6$boYvtZ#PZBRduMPVJuSI*7gr_|5eynH8agZSZWW}bVu!Tm@UryakoJo(WO zK&|~_;p=qG`i7gl-ydRNIYnOM2Jz!rgzryofIpE18c%Zr`APN5mM5X}xxxID`VEWO z0xLUk%|mB(^Z5hnckO5EqHaF^wE6@4(f1Fxy!!7UmBg$65r6$Zu)Jcq!G(*`VX!{U$B9$BNt zABTx)nEXln^`EMPUh02P)BK#S6^B7E#QvGOnp1#PSw|;eE1K<)e;yVsfaqCafq#(& zif9U^z`x9*{5T5+_|YtIN)rg4V;GP@M_@4EzsiEc*bD~x*IB5H+F)h=->4fr-u`Wv z4t5MQJsi%#%qS;zB2gfkz4NcLb1KJn`U7BXV2KOBW`;!rsjb_8H*&DKmr&oSj5jb;>t z)-m8aqhK3%$H4AFSmyKy*h4Wo52y}7OdgXAJpNdqemSi9=Lx?Bvid<3Dx7$AA#a1%8 zosJebVGic#z)J=@7scwBOXPBckY)OT4@b)MiwW#Ty5u7qwc z>c4nh>ETE@pq&jU@{Nn_WQsb6S^R)4%EWjwpV8|@y-$3m$WP|;qmj~*Ojsy`ek_XK zfH9*CY!l==25%){$JIuey-^5M&a-yE;JBuiKjF;9Ffdf8MYRNu!x;guo4O+S;Pf zK}h|M%_oe`Wr|xy*hef($LuoS8d#8M7*NRuHETam%_toVZ>}fohZb`7Bg9)tCIR#TUs>Yn8BitlKQyC zZgp|UpjhFUr2d65G#T|V*(dlFba@O+X0jYa$&^vc404FMTym)~{km9Y27vBLYLf+s z2xkVdf`|!=*b&4%Gq4L$nxa9`Op=S3^G=)drWJH?g-B`U`zn6lXTN*#(+uV{(PDLQ zH3LECCUu3)#t*q>KwpHQYb}Tav6-*0#jiKnubJ>{2KTxsH7;bE!M#2or{lR9+#B+7 z7~#!w>WxU>ZI-@h2so4HmqhcF@!<^eO_=NRZLV7}R2)Z&GmtNhW)_B#Gw3%*(G+9O zVBQkLjMkd-Inluvls-vb+!v}kyr1BD5aE9 z6uWn}k=D**%#p$1q3#eOW`IAC?UG)$QyThE_o4Ix;iDPuX4F|7ZoUu*5etVYAs~1g zUYQWhamOqHGwy7n$~zfT!kHb%Ay6}vS(p`LO0v3&8JnJjz#H4{dVCt z?lbWIbXZ>9-H0Lr>*siN8Rg412XsD5vLEa~KuI;W0)}f4E#!dvUZI8)4((j`Js#`= zgyCGBdT6xVgXq=oF^j3RGrFH`1|G-l)wqDWap-md6{kBTgQqmaaX6mQ1fm4>yiMma z(i33gUFy#^(!52_?$8ZAf4ADXNq+x*^-uhquI-RTR1yyytt> zr1yNE+T}feuG;TCzd&8-Jv;v@qVL7(R`2^u)lu&`bWeIfEqLEgsn7GCoeK`-@4Rj3 z*?HH{^BFbp(w|iqz2}G3YrSXZ@j~gHUkg1is4w^M&OL>`J8u+vc1|Vq>|8(S`BmyY zUVg7uKj1w-uHNT8FRGvNo}HTn<(H~o^u8~tU-h1s)o*#v&S`^5YR=2= z4eFft{3YsD-t(JO;yr(^Y^PG-t&J|_ju1gpkC-b|ByQA zJ-aiIME`%YM;+<;eQMsryW@-W{fE`7yzf7v67Ts()spx8W2)&rzh8B{=O0%C@A)Ux zMeo@i6{Paru|Rru=K$$B|1=$a|2g&bUjDzRzTbQPCG}qK`Ips?de80*8s+zE>Sw&~ zzo~xRdv<5L2=7jD(ev-9Kl1RutNz@3{;>LM@7XC*ls-5=)%XLoWwV?g|5)wyp8rH0 z@Sgu%-Q+$0g}Tjq{!8_I@A;$ZPVf1z)P3IbU#k~;&*7PykExe=-~UdX@t*%tz1n;J zCsp&F|5-J>=TE3F^qxPd_Iu;;|ESl??>a`BJ-VS;s4l^k^jCPO4fyKq-m6RT)z^8i zF2h&f?7iBEuioXo@=j4uiQbtAdgUE}pjUY8LA}?@3r{$xAMsw{VFvXR-m6J`Mf6x5 z;mq!wQ;E1;uC{q7+!$9=-mB~J)n4z_4fyIR@6}EC>PGJsZau3zyjQp2tIK6iQr_M# z(dBLR(ktBARj0hXaQ{|4+Kr5bv#aL-V^ z(R=j*d}a0piB#MURB!cAxXY*B?!CfYFZI3NE8N^tKj^)}Z7lVa_X@YH)cd_xxT&N* z=)J@!tEIK58f->Pf?%n zUg7qL+PGErRd0KPviCMK=oRiI6^#mp?Fn<#Yt=_8ZW$&4{3FR5I>6K@}rdOV6nO>nKS-s86 z-m@GN%Ci&GE6+Gguh0&xzQxN6PsykQUTdRuSH0Uqd4^od3r(!*DG!BqRP}!E6`D=e z2l!RGwig4v4@2ul)UvcwLycJjuA{&moE+1}OVk~_bb+0DFo>8``6T&P+|8K^U=c=Fc>i?|q|xL7WFv^AbJo_uy0Ph3m~YM*h9DK5GKr4NlOWyX^* z{J^K$c=GSfIO4pRiT|H&V@bG6@T_s0M5cV}Yx^xIROHJ%vJ@!4oR+5QOJR`#rIqw%Re z2=c6L<5PPuWPh^@ea)DFnA$J%NslRGK}WvRS==4?QGn``G=grLROB zrn7KqLQ-=9#ro1w5C$+8*ely?#=UE6&;rAr+v6&5;8 z1cil}?>uo4UHSd2iELMmWNkk*G4y`CzZv+x)Lb4}SJa$JJ7*ghT-f0}hUO%4_YOjw z%2VwuWjW~r4u_T0?tOv)?0q>ojmf@!!of}yV26pvL+ssg9%dPS@Ely)y^?C*y?(b{ zmiN>-@D9FEhJq`Mbuhs)++H{y2M(|-62S{r%erW#4v+*CIf4v;6mEb^7Fe}4FXi4Jft?An3nW0@wTbu0AeLV>f~!-w6B`^j+dMpdhipi)aARR3jp%mSEU*ZcCpmL&Q~w2d#%OE++S)UL51@*d#r{LTWveWyVfIt7MKUE?x8JQor+M!@-- zGE%QHH>OSv2BWl;XDG1*$YkGiAyyiWClJjQ@vd(R(dkgcx$uP4&#QOK6EP|nW}A)Z z%)wyV=ys`M``^>>$H6>T&$)rh8#1&X< z&gbeoP9V@s6V>?nXDZ^u^ge^DRdI8Lt=2=afFU&Vd)mrhs&mX zP#n|?!^*;8AHXA3h>ltoRb#b!!wC`^J)eV9fC+jHu@XuA*;>!Enn|2HTUhYvm|?7u z1YtK~Q%=OqFycw)O-w}S%aKqMIWJv1K*b+}81ta-pXjVK;bvNhedAPWM+}5>WHf{p zs$tZ&IK2%QrU604b!vna1x3JIFbZtLV+6A*tX9gnN z)tbIpL~?>>gU8avMz?!DpB3)*S0tHpEBzHiBA17K4Vpimi!Dp<`n-fV&hI1b(sHhYJliJ2yWs(cMKwSu{bZxWFrEust z?!i4}^TTeh5bb&v%s!-&6bhlp+BPy60=YCn8CzMB9dr8$k?CU~=Ck1KtU8S|vrf#m zx}B8Ah|P7oI%oG{c)l)zu%^P0nX{*&8F6?*gA&ne8?wCJo?b;tcQn5}9uQiz4|n}U zk=qEl04sap=j;k4Rt6zhREmB3sP(SqIj@ipo>Gv@2OpKkhdmywuzG~7Ts_cT&D?7m z(G={1DB*{BnHbtD<3^2pC zNG4=YEF))+2P-O0+XFc}o(i(1>bYkFI0H}V&FDH8no-6HViFRMm-gjPd!VIv8dm3;$p3l zbe3UfzMewX{veXfnc_`S(<-j3O4K$&A(3&w1OXP1$~Xe00#dV_c4S|YrC?;%3Jf2M z!FqHmFUSh$7f?_>!{Or`+<4?&N-N3KQi+>`2EY8Og@~(YoEh43WUGppc&}`PL!qSZ z4QFnpieH11q`8kM%8MX8u?w5 zR8PmsXf~51&MYr0voa$^D5LytLEkkiatjGL!DgkIUh}~ecORI4p*_e&bG2qA-c)#v zHpu5B3*)djlK=#pG1}K4J(~<0b4jl!uJ39%3g7?)L63K9BQ#@a)6iKsSYr7t!Mr)) zfger#0~t?R;*FI|udi_*BA*LkbtG3r_@Aw=dxoL22iN_4=q|stg23te zqU|7Lrx!mF%xRddQCPAaS(mCmJJnWpbky@5OY#Ym(}CUqu!3Ev!!F}c0O$=0Fh!J$ zak6cAxH#pw4^WNs`jl}vEyOcLcC2So2(78%7GcSx(FnH$y@dcBTXker;<#=Bbqp62 z@8U2SupNxmtE4z0&oan3&+tU92Q5-C@$!!tIG(j`j;E=MVs9mjuRd2(t&8KRqBU|p zKD=bX1*N9mUS&GP-X7rMoWNR$nK7R#H&AH|x=A-(+vjH@X21&5E3%|uZbchmg^3kc zmOT8#;|6i$G{ zW^2%AL<-C^oN-#Ec&FR#Hlzy5N~2Y zn?@YX85VWvU0~A+WC6P#5}(o%T}6Rn$%160W95 zGI>rDt0F|;-~^GA>D|#D$)^B4aFIe~XnLAj60@wznBkqPO2Z~-CFMo+h9y67Mr0+1 zK~8^hS=ugE&~bUgtAb{-jcBQ?oKUxOS_3^hn7O7^V-9B4>8U1KJ2v~-Yig zM`SDp(QI474XebQm4(mYMubH12^2ASAv0#a!|B2^ramfPBo*VH$aOi`E#Y%;i@n&8 za0p5fUds1{>R#`_mb;_|6nr8;=U7-Yn{Q#_gWF8uRONg+9Kz0wQ%oEcz*aI%<+9a& zE@CT}INR+HXuIDzxJEcbSw4fD0lxwC`e4pzCr!}AU?&Ki$D^GEoPtGyz;rCRBZ0+i zq?LCb8?{fUFj@t6JXs{2c8zy(9FQeFTpPOVAc(g)Zcals@sDU4iFEQf7!`j3j)QP@ zQPSG7pW`~Ayp!YF-9yF8#-)~q<$Sh!ezc!#JC`=KA9Y^4+Jk}d_A$JKOIrHN`#D zj+fvQM0lrx&ktc2DqVKIf)dRw;cdU(z?gya8V&s@h%^n(f!nS?U2#`*LG!^2R$Aa zc4fj>?aG9%)Rn{AZ^N$rY3RyKZE;r)K&4%o@MCsmn#0%8k3$h0_RA6huznG_h=SBy zF-9hEuT#Nflq17}n>v&yf58p`^S75%QTNmvK-O8VP|PBWD|O6Ie@8@ftCf{@7MG$9 znPL%TUGORZkJ-?e0Sfw1WBYS->R^UhP}%IU*F^U71YI<1+^Ihe3!6YFTI_DswJv1`!Ey_6 z(`2*FgRx2;@s;rX$C6>K0r@{NNc&{hT1W=x-Qe4!hBoj0kS$tPVIY}J8MD%*g`ouV z0o+PhKGO==W1~)8toCGv3_1m#5d(jH?NBtG1nwZbO0o$dY6V#B3MePlvKr+UTSPg5 zECIIR9N`c-CIFRDiKh}Ok%(GPF=r7`OdOf6nDJ$l?M1;Jk{0h#dY-Vv#W8y7vE+%u z<8Ym&g(HR)EXV9jV_ri&bX>Ek##j$IzTD4=EhbfQjZaUB8l2kCxTc(gFZD>tYJ8^|u zde40je3U$&pl=iuT+|c6&2t%~RNA*SfC>=3j{ZakIkP);-9Bs_Yu7UW7_dyRMN5e4 zO|%*1M0NP?M>_pVgtKt5wJRFdSCD#RvBVmCo>}>7gVXV3n20?N+X8Os zo2;?PGJwXEE18O=+KCnL{wOPf{T+f5qQtQLs+288AFh%o&f}Fbyn?4yV}unvaD!Pg zK`*cZ5=s;8!BT=m@$9cfWJq~w{#wF>sp19jDU^ViXv17fJu-j_Vxp;dkc8AkvAkai z*SWWvo#il);(qmYAjPa>SmPViJ9&o&y!mJ%n+NxTIz;uW&jd#k!n8qHRdpN6$9ui4 zl-u#u8#hpi5OUynYNP_vhw-=hhzik7-L(fh|F3Vwv~>VNuRE0^$Uk=C>ZgFh_YozKJ}SN3wQ%aT-*9+TbZ$_m_V% z2dj`(W#O4cSUa!eXJ}(7!Xyks(*-(^rP>IM3=)~Tktq|vVJ8|Jg?&cbMm?@Q08+c# zcUz<~&s`|eUA5s{M!aloLv^?~NCzz9+Qbn$9ETwvAv~rXNz}ffQ)O^&a0kWWFd0Qh z#g#_K;ST7rB$|Rx7`R$SQG86TsAc&upCB(@&G0znt;RGn5522dOHZdOY3t0Q!lP{~ z>9J8A1bh%^{GfWPUD#bgd%e-bwklo%o~L4^MHAp@an2sTbERC4)ONv6cx>nU9L{Dx zz_Xc7rCx)t-e!Fk&`+p(MRaH~hJ!s427B76$z_bAFw5 z^Bf=<{xhoxnNXH-2Ez1eQQ?M!gi_#AY63AL9E7+U9$)-o)%8wf57taH)S`@v_9ND+sV{r+mAOw{uD;SUDb&@iR=XnS91OSbr6e}F zgfJDG14(wi^G~QstZuN79@k(Jig$yANH-cBk_eKN+XKNBh~Gtn*&N8`^GuYdJLw4i zH-wvm*c_JC@;k@%dbHUej*=E<7e(@E8Ad(??o&}1ADv{FJ|^W32IiN{og%dabauTobJnO|2AR#aCHSX5W=L8nQ)w6@m#lUrX1qDFaX%A%}=kw%aTOtBhb zSr>ryjhATwW5V)_ShEwQtz?TEf3jcc7&pih5xOYzU6UllsRPgua^eyW17o>9f8r(U z{aeADI7+Kjj3Um&ak5xuOB}e1#H-{?w5{1lliOX`7s9V;G$-Dy6IG4E_^6~;*C^00 zxu_59H7odZb_7~$n>=@IrfwEY)~+VW2m4Z8Uf;!4Jeh{$+W;`*FW_L*L&^@~hyDR$ z`U9|)i)tWF8e6eXNY|~N0hS9;;OL^SV7WwtdU@4xEyCr{>b*Z-V`t< zx9uv}@jKi2c0MfaR(5Zra>qJ=m)UUu^>TS~6;1c=jJMQ?Q=;y}RlwJjtpdCwTj|*jp9~& zt6MvNY}9Pk;lKw!+EmW-0QNCuDq|x&*-C|3S;DW2!56xEqZEu@un`k2Wrt0cpEVs) zvA{v(_5u$Q?V zt^aj?b_|8XQ)$u$d-)^hPDhiO9kga2%t2tzBV{29wSEtk^=@nXVLd~)P!S_p0hG8>F1l!Zx-;d} z`6cR(%f#l6e;WowccB(6EsU4SItV2Q&HNtB8Imxno32UxbM29=T}1_JGX|L;=0}S- z`RkhwqkVzx12`h!drp0Wuv3j!srShGH{PTY&z174)H}WB@ZP`gP*2G=Zm0TtcIn>ea~f z5Auwm1wYkO3y%DBjS|UW7s}+`0q(_{~AaRILtF@MPPiy$aBn4Fq=j~t~_NUKu zZ5v${?W`WM&%7P6IdRrGQJR#Yl;kwo|6fFJdd* zY+t?l+aXc`XaeDJdnu~)=_=EpY2A+0Yy|_P?Y3E@-s0yv*8>TaMbi;x(;7v}%~mM0 zKg{RPPW@fVjVQNw;|a$q<-yTOJPMh&zMR6(rKRziu`1=6<7HKpH`2w+Iu=L1F?Gn9 z9?F(bZi!@r233-{T)OTl`IA$Ld}AYixpbSby;w$YaN)b8{a`MI!)W#U;fmVE6RmRi zybgL)0=LWXrhgb$QefiO!!c9J)3L?DXo!TTRYT|0pH#mrXQh~%m8VcyJHg3OCphmH z7X4Cn)LUM;+n`~>qa|K7UVwqC+k$O*I}BY(c4yd&%qYLxl~EBtNwvjOmd`Lh8^5Qv zdaJw071xnvVaRf#?`g7}GAUy@p^8~fm|T`m%6TYnSa?%orGez7s5F))POK!Y5jdFP zeI3!>v)>>%R*?w6O5;a%rR_RiHdH8oWMYgN02mn}z0+h)7+$h49~~0-8R(AVc0Y(! z0;IWt0B6km-9gVr9m_HWtMc?ZCf2p2S6=5qgm>kmNxzB5f}PGO+02oET@CPPYdeYd z1guA4uSZugO0Q)XG%d_X!%@6s4c6kmLT|v}O1Xn+o8GSpVw=lKlHa%OD?(3psP)YS z>ylMOj*Zh;K+bVB>&*{uQ*V{#Hsuu$q2-Vfrl%ETTQu!x;#^;gx1BD?bcHxXpTx6} zW2=U&XwT9+J|wUF&JfRAdjMf~D5#@Tc0iRvzG(ch)iSwr9USU%)vr8`uGAXwI8p(W zW6-hv;SyyIXulSyXMre+kD3;WOcNs1js(bwHPxp%5T2n)oOEr`RdT3v9T@Z^O61zC zh&`r5Mlrm%7VC#mJRh^kl}oIrVqD!>xbVQ{agF*;dsLS%anhLR0e1;zH)sUUkD59J zB8e>tVr{OtR8~)rveHudhEB}z+{if|$TIoXy<);}`Y5CdW2kFDDM=V;hUu9frg(dK zmJjIJTuhLbVum6oX=KGna)fM(WA&jhK~)^37x#=RE?b`mW@J1sz15Eh-5cVV2`xQ1 zn<%4Lau{Nw7>-=5F{Q1;L0&Ck(Hy79P5BkwPSTohG?&~K@wMtbc6jj4-q}7}SPkdk zq5sAEtCxm4_gHCehR%YU{sW|hPF$q@hq`c6Hb?XWE=om4mWSScb4dqwk?Sa4IIcf+ zNQu*}WeahwB{8~p1ewK=dY*RN%K_=ecPQHuvdmjafLhANtp3%Md@OcfM`<)+w%qcT zWl>*JA8ZefPcEX1@=S7_`abSl)Ii~er@NqA}pyOqx zc-GrCfTIJh(gK2M6W2HyM`0Mkb0O`wVFQ`aQIuAO={xp`oN7Bm6l`LH#*^h2RbP_1 z%Q&Ur^(pm-N74!4di5S)3_3xA@yFoK_S0U{PwO~#{8JzjzR$pQjy7b0t(?RjB%v{z z^pYAJlZ4?Wysx;b4o_8(TOJ3=rn2tze4YZ;yjl2vtLyH_bj4b4y%IjLdH_-;UF>$< zi58g?s|VFM$-zzlJZLU=(z?m{k$x-6O;#t+JL&#@cjTr~Ndb6);+f1i%ZS4g&H*D! zIYhaZafnfc95T|rN?&6=rMSjBDC)4`UX*pXc%4mJwGmuJ1IygvUVB9j*fY6ECSr8C zSnk0jB$HgPlhCk$B@VPd9=nQ*RGALf|5e)-wXU*;MI{e0i=^=8jL9%|d2q-O%^f9O z1ATu?3uUCHtRO0t)rJp>;R-Scqcdw5NS{$l`-|n&@}&Ah7K$`Z-lO)(NF$BkQqQrQ zlyHX}=Q@J$B5+m_?{kG%MKw3E%UY5Dor&#~Q+{|rE8u$LL+UAyo?lTvE}zx)IM?m< z4rqNbzB<6KVC@A|^PX2h-_#QqnxG0B3ib7pfk|#$rrF~IlWakDum1SiN9N(y(Kr{i z*&#O2;&5`P&CWe>v|bK~;SxvbHHaB72#L3)dLALQ2DO!`w4du`fbHmUYOV*B%t+;Nq&Y``t2!}}3Cd?rw6SRs z>2|ksI%#n5;s9>j#)en4j9rMXKnvVc)Q97-pi+)Y4_`8480%|~*?z{pPhtp1>E=a= z9SY99UJe9aWSgN7&56B24W@9o@Pa%u45AtHg4}kxDCoJyGMd5<@YO;6fz}ZxR+{KW zou2IC!Eu}T2KAI(r{AoV_|hDrX|L01Ni#=fZdt2T+EUA86eJ_Ba?6(xY$_YcZ%q6w6Lyg)|aF zUFp$`cv0X`#WWhfE=!|@&o^L!7%hSihPonxHLqkWTPTa_1?v(MCx_R%(Ne*~$-vI^ zmy=HOHM%p-)Is7DrMs<4hbkC9s3Ms)xNm9l>WL`JM5qAEVTU0p_Ajcv*#rne1>dEy zjE~md#}dQk;?b3p&^1Pl1R`=mm<%_INeC*bu1#`iP1KL^78jhGfCO$R$1}wRh8cL# zO5keC0FJGiUj@9!2on|t_f0&~!!SH~^~P0#Mdu5|t23}csYJV}IE`Z^wX$S2a^)0U z7O~ur3lfLWDb#6H4?k$NcZ%#uiAsq>wvII<&47{{;uNO&OA}_CKky>qL7}lY}AlH?G>r7!rtD^@Y>Q}Vo z7A-+*)u0ktOMF2w!O5*Ks?MZ|$mJw?4!!am-nAyxSiS!$SP%DOf;WH0V@1++iYPx= z@pCBVeiY2H@iJhriwEmat1JCTE7=fY^XhGQ>c;ZsfY4aS)P#K&N3MIR;Vr z0M}p8VfnP7HJ)a7HccG3hky(HWC^yXXzaw1>=3CfxOp+_B-z(%RU8(d7Hl@x!8~k( z7ZNLFy1itVH#|9)E`bf&g`ts92qX>{c!NdxbaN@Kt=3v;LB2-0bpZi1MkrRUd~X!rXBndVY)Qc@_IIHIUEha#WR?){A3<1{+yG>xzei!d2%_qmkk}8%9VN9EG`D8{<%v3W8F&KRVRRsdPJPkYJ7sp@5QTRSq&8Y|R!nX?aj}VN zXl@=?!paZ~ob{%TddWQ;Rt#w^8Mu=avQ!N5>j{!*Rj&k8?r4mrxN3ZrXd5Ic>`GTt zFn&uesX-5Kg z;&uYgp~uYduS!{AiBY%B6cGkF%Nj8pR?IA7nX7gzfRBN$SX=c$gEQS%i? zvlel4X+D+@fEZ~Dn!V-LP3rqVXP_f6daNgozTqc9Uc4R%8*WSK?XtbWMxfs|~xH+t9s|I|j)oiULxwo60%@_ny7 zH9HJRk5v7lJPxn!L?vy>+o1kjeDy}}RU}$O5Ji|<6=Z5>u+;6h#U_U%0gm;MMvdd7 zsLq5ctuw(TQmiiHt)mgwd-i!pe^;$;F3zA4OcYw%esrv z-mAB`ZRMp3=Jg6Ror(Ip;V4nsP{tfUM7)PTV?CPAB0Dz3iK6}A3Y^%6XL-ZB>Sgsb z0GKdok21<6Mj zGmK?;SqqSGtkeP$nqPg6CL_26h6~W)0{vLF2do^?P9~C^hVX6P(lOfDyxS_$NoChb z0$COxj4bXTY_CGS6(fM3?KVYVH<=qVxvZ_Jetzraw{;9%+}1WY46#Crc}cB}b}Tp+ zWh^j>Y#SIhE-yD*XfZGC()9jTcM&`w(qIEj5@;`Cf%yY9>WH6#gj<70c|%z$aE}AL zHH)aQ6{BX&tG~TMqonl(PLK7x{0_>U_3|2daY#J)NeB-D6AM`GqK$iiPW@6Jz+RZ~ zbCx_twa7uE8REU!4uYk>Z2rtcQ79h%^r3}?$x^u=+IzN`S#k}E-Xf2ncd5vya)BLkyIf^2T?bC4#A3zOc zaU6StULAyT8_t4=x<_S#j{WNmoeGI`8fM}3UJi4NdcwJB3lA_PLb5ayWFP)j=dF3+?P7%ljUDsLEC!*CHj74$Y{ zf*Xv{{P+%c?hj%NMA28#n%GC%K@d%H08UwSYfU(|&DU2fbn$eUFty#He%Klg*&@hL z$dLK$FvREgsd?$L(vVl+@~P0Lk%ulrEu%^CBX%p87dH<;_zAe1Yap&ZaKu4?Ua7V+ z38(`W)ET|Lj=^rog=T%&I0uZ_9o5t_LOlqGOQ_axm2Evv z0Lom^e8yLO%)wR^c$Y+pyNXUpyYk~pSDCY(#koRQ_Smf+Z81G{I5Ogul9#Y>t>Qr3 z3mUVTFo`R_5|8&olDPyH*l7n(4{3q80DfUnPG74(`%N-13h?}Drxs0h1&(q@R(b2N zjNtYGMH@`z`gnA1ONC>{&tbm1G`s*SvWMg~->y4E+mfZ3BRg8PEo6+Nse?Pwt?E}j z&+kL(W8U+!`YkD<8*f(U<%y8SyVV9y)O?S+!h8M;b(?qe<&`Kle#(jswrt_)C)gGh zab;`;9D#|6G!GR3kC>5bb&27_Q7jaC)ZC zS64%xk;~d7srE=imQ`**$^_Pk;pH92S|f(3TO4LjNdOEHIbCQ)-RO0Ze^jrw=kD!6 zn1DlaaVKkm4y}f$`A*UsG`h}EA%S}JQ8ClK=IpA+nrM&P0s-XV2o(UMpC|0@Y94~e zcDB```SFQOQ{yOxH)eyn#yj`~*`>*d1G#aF-KKt=bqbmse_8z~588AM8X3pLSV^Ff zvcBG~kYsH$AQsREFrS9d4fg};ckP)}y+pMoh_2_#AhGxglrEp}_BogE)$g#tA(Xef z0bzY{tw;6E&pHf)+_Nb?!I8vnn|+n?s>Js{>I9^jT=O<2db7Lv2PIS#g-Y^O#^k)5h_71w~B(H*NP(m52kiwJm6~T(Gu-<~LHj zgqv~ogi`2~?LU<}1;jS*_R0q%xY~DnQSL;0#6AmmB;`p|Hb~jlj%LWNJ^7r8rnB@)HP{)^D3sry9R@tw(zN^$OXtN_=ZlmDzSdl24!!@+1@i4kFHi{J)vRbH3 z;K%%-hZ;^jz*!s)^!Qr0EDF_a?wuL#eKhXs~$Mnnc-TahO&*yhh6> zwL+`uB`P&remFCD z=(CkY)8KJEny1;?DjdFi??-uVyy~`tDu)5?LNkboBqo`;v<>#CsUbh1m zBDj!b+znP|jS%{D!5omI$zjVH&Ntrn1JbRX`gt|+RaGvKFdY#gaj1nZg1 z32#%`6#y|^jgn^>7OgU?SL+~{238e@trh&{LmGAf_Uhakdxf6|>(YtaioA z(JN*j9|!6gTXeZN>Q*{yWZQXQIFr^)_nuzHo$=?q8hg=FAD zdb!=mWNiz{4l)mL>FzsqyKuKM3``jAUWS_vV*!3)WohRd(ku{2?A!gbcwl;S|0wgV@cxJ8-p*6 zUeu>>xW*2q$xw61><@9v+g-DtSxzRPkjQ9&(k@(YqYEi2B3S)lCUR%U(B z15Tk(-JA+9Yi6AzD3$B^5@c%<(o;|q8$iLeH`YMr5arr#_CTks3&spDYb*)zanSIF zBTq$o1Q%q@Q&uUhy!LZ7bh6R0FFG7qah>VFbyBU>xo)Qkz0dOFi27L;DG-%}ExCl` zqWtUPCHzW)qpAdlO#2e*r8Y``wF8I82rDg~2rpjuvf3G^AmzU{$KIQ4mH>kRVP!fmXCP5iAS2)a~S6Q;` z)*TXiqmMlcx2!Z^&jL@(wxOiDIfq8_T&Bd8g(&5bVY9_M<+-dor{gF#UxQ2C@&cX* z1^zSIA_koQJgC?^SIGLvPMW|U6d$m5inW>NAL^RD|o#tti$%opjNNk$$B2q0% zABP0YRB8Zp%WITmv+^1EcwF{dwQxFw=D8nSmR9wNQ&nY-OifV<+M0Zjls?b zxI<#__Ba%06Te||dNs|fk|b@*DNh?O_c|1_L1M2b#* zqXCK70grRNq+k+UYU&ftwJBY@Jyw1sx4<%Al;tfp%XzresM)~*z6^@8&O&D$E}dO& z$=YtcrNAqnAvjaiQ(<>}vtxZ?BJ<=IaK&@OHkwU$4Y*HpQDU>+3 z1a|OpZ5QXK+J@8SBi4(8jf}8hGC{ub*>jiLXq&d->;5%E-gQ*mb~XM+UrNv zozmr{@z?6b-t))Q%d9_y2^dB)5ge$hkeROW1$gTmf!ypSHJk{9UECsJ<~Rh`ytAeV z0SMu?zus`vPtP`cDeNp3h>*M07Tf8#S?1CdquAuvSBf2TN}JLgZzioXBX@$@Nr1$W zr*lV%Z8{mwojJWm7RAgF7?ht_N_yUDg>}xw)yzFSj?Ao}+Un$Ceo60F9lN)} z9H}5c*}^s7?9E}D;kG>A-kM*MtP_V5ilm22jPPWC4pU_w+~$x6?*dgw!!=ybgv(X- zla4XFvX872!`W%c9N}=HGoKSu@_}2HHV3+NV~E^aGKuLJ1B@eYk>=Dy*XML_=%DB^ z1U8pEmcTX_(2#J;=Z_OFp8)|(6=PXgT>yK@JQXIVYr9Iqxr!k50PR9m_>-=2|5=~x z<_n5WtiTEt>}j!Oz3edHtcM*&i;eY(dXvEzqMiqqkiYqiks^lAjI2ZS5|JoL^GotX zuS?)Hd{#&>zayeEza&{F9zf%XO`^`1ebtIP97}ov;(o&6yV@#B`%utS%a_^UlQf1n zW}}Ip{!G6K{z=BboipepGow`K?ozE5x5c;M+YMv{-Yihf>hde~p6%s|JAbPe19eTryZ05L3A*Oi~`4W@?UtuwBXN9x3E zfM*_a*U#RCb0X0IQ~B~Ezkf)f@%$VrA{mY$(4HrAjTvx|+#>B01IUR+fit;ibM`_N z=8SYXUeU}ca*4~bxGss;lbMB8J}2e6Y9pj5T)O{~daqsG=zUjG8|NS#Xm#xf3aP5y zU>SLVi<}ljBEoeE{TyDQg3wdd{P-+UO@d@=BNWwY zS>Oc@K0`zn_6m*)LkB%ans~AZ_Aa34tX$Ahw|ElHw;lSXc$yt**t2i5J)3;$0)tO} zj-H`hyTXe(+K`)5K{>nYgX(#&k?4wXkF71;TvjBt|_Q#N;Ij) zcc`blh*IILkvHMAa--*LGL5g^k{{ek7p&f>=OaGrm9FVl@aIsi-Be_$7K(myF$Irr zZo)p~XIGJst)NzQM@F5^jzEkTEat!1Y9OcO29r z>urFHjG^`Gn0G*iegixM8Z@%a&vg9+;>~n^UgImlnJmUpf-{NZy`Y)~o2`u)KGm{# zZiXO{#n>Zo&$+ao)SR9(hP~w=CnYy-ZH#yJnrzqINpPB4Ofgy5eu&S=uI2M^U1A>& zQ!apyyE{}YHPIbf3 zM&pl)yR86lcC2i(ZhxdREXiU*Ly9ulODCv!T~=;o*o<@ENpFU{244-lSCOgpFrr@W z&9+bAtABB)u9y_J6v*#Vaz-&#aOsMd5Hjr+Dzng#Di=-WmUvSC^XYTT6l%9mr}oe1 z?bkCT_7gie>A5ti&n5lyMW=OKML|EN)M7~IGsr1};fU7H;mz0wGg+5*)rKnqgaoG_ z{~!@1tZvX`?)&O~bbNE2BDzNvw!t;gL|P>M7zc@L9@P&LZKX8DgKS*o{O{E*Qi3!> zW!*E2QWuE`87~qopI6E)gd9wpb$|PsL73wg4Md zTO`UW0`~Va+u@}J=(V?>xm#SgnIKuy_pJz^%sHOoM$j6=1Q&Zvu(kL*huKmkmf5=qXq1NXwyDNCISPD*Qk#=-=~ zEea1#h6&>5r(|)G$kWXg=QdDS=BdDF3sFW{G95`B%HEqZ7I1_J> zcxoNs&PyG~L4;RWw5*dom+Qn&gF$<7$+OPFbtv1XQT8pQc4!`rFFUR)NFBI~%0|6a zBwNoo6HY2-t!FPM21l|J7i*~w=t^DLtuEoYSPm-bw<1 znxf}3EP?C>BjC1V4@L+1%3wDW%pJ7Q5#z!WkgnIuBJ3HuvO@ODlR#x)JoUpEckndk z%P2{~vne)_wz&k|M}ltv@VY9jcPs?`akkuqwq8aBVYo!vXrt*}8HgKUQb$2(T4MlQ zf{>@#$O@y4m{U||=?#!*U0m8h^Fv`Bcu~rj+(EI6v;-}mz?8{1eupzmvc|U-NZfG< ziqdq(G&dh5YkY`WqAjB(jxWxUxPn}#p;tv#eKW1}Vlsao;u^#i4Ot)_E0Lu|sX`B5 z2`V5-Ux{dqdO8shK~9~h?8=~dk`5hVFI$G|?h>r`-y}E=n}+W6G}*&TLRnUnL+tY_ zaRp4eNzVqr)Y)zejGtXvDOfcnZTDKBciVzKSM6sfFSx16B*klL+DR01xsNgVdU4`p zp2l0zs7XcTb2M>}@F)?i;hP?q)@MfbCVGbJh@obVg8mRq%|4JWwYlVm2BM_?IvTJ6 zw6xN^h6t1Kt$}%T-5xK@r7A6#*W9PTpw{cbq?cbY4vBQC{!r3};Hnr|z?Bo_8aq8Y zfg45fVhw^Pt|YZpibIvT09ta`xK>r_AFKn&jvb|f*GG>3aah_VXiQD%bd9bxvIKdNhvFM7SXL(pKjo zB{c(LEQ&FNmaA<3Sh_lkGn~zxUGUbMAn&qKbZcRf(h~8+YlZQHk-Elc8N4W=q}!v_ zp?Z5EnV?|~A>c(?hkF-AuyQvdAv`O<>|~!aVuNt2>LS{@ciMv~sJ~GbrM#mvmBC1K z>Xjr+t9o8vHMyHn7EgDlUaZIo!gY_lUK`JejuFb7;_Vqvt&Kb${n56IC)W1_C~OT_2a*YbtX2;VDGD-*RN%*=}!y>k+@vqe|T#Fq*71(+gZz zxEu`5zeJI{wFT0vs|)lhYijPa2t-mmQ!^NEA1Ma19Ld;mnY%%DxCglLeQw?uU7`$CvZ>6Tay>rQi&M>$R~2J5lI%?T2(=MIC{42k3Pwsj6m;wkiO z6I3sbycUhQHeQ}bOab?3hsvNWTcSgbHlz&JV@njzJKH%#MU$}MMO=k`V)lqdzEJ%F zOnVp&-|l7$WJG9r31~1>PhbN^N0YclkcC*sc9AC7agcBuS6;v|V^$R$l)C7bswGyV z(lwUBB4J7W_tHfZa1>OBd_I>x0Wp}J^m~!|70X)`1_3UpghcBu8o(WQBvSiL8KkJE z!%#hcUcFnYhw5fjc5d}fBL=5}rr=1;Nie<*7rJK_6)e_lpLwym)y^5);E+XY)vnX1 zFbSA|4ZNh*D9C~lc0_SYxlAZicaM;M_Wk4_I2)C-&^-mg2QNlxon**rFRjB3NiI&X zD+-pK+K7xN|Dt(p)t?`OBgD>5EtB&ln#Y?ExEsy~qi&Z|p9|_?dzmL+|Ebv>EtUbM zZaG=Hd$ou~kuzh5u-)galFN5w6txaner*fq6dkU_BPw^GU$b0VFs+ead5FY;^ZU3w zrBwOZvQ^O5J*Q@Wt4_#hd+J5V!LM{2q+&h={Gp7i@dt+(#&|FtBr*;$mU!_vB*fDk zhtOD9jwL(Vou_b@8VG! zj4sl)F?7w!vNi6-j-@kwapjk!fDRXG0iV{P??EVAH%&>P_MwDZfznpD2logVvh(uQ zoAaijq%A<^sSDRN^0=JsNBUB44b(eIQ8sB+_EZP1Il#RsBa`Bl(Y|~6BTe&fF6ls! z5$=+?{L}M%q^D%-pw(eQLh$Ss9CnR2w=SG;XA=Mo&nCl0yc&8pSwiD5QL+{ddCj3Y z2vIbbA!XlPExJ`pcoZ;+oIEw-A2Mkzn_V!*aumA;{~_fzC|l8Mt0`+n53 z5;EIN+nG{39b_Rpad%#CVxz#$9k+YW!9AWUsjdS8siC5MmoH4!&?tay1r;P0Tb-z| zl3~Rc`q-+*#akoP3_>-43?F_I&1>P&P0%pE5{pF;? z(d!4`4n(KPsr_D}?ywr~gh_xT+bkvG8m%Lm!`cWl>oqOQT|bwJ_|iJ!1}qK4H5L-K z53+(Vx1*GR=U@}SJH95dtc&8wpXC%z_D6~3$iKYwgbn5J6okx?bv8|&i3}aD4hLk` z$h@M#ZUbi0v6#QZCCOz!tBWPoChe+XwOPl*Rw=2rK;~5&>R>zta_ay=D_!g~(|A0< zT!T>O{A0D(Zc|%#0$ckBKxD<6$S$m_F~MG@P<5c#I+$VTLgTwCh>PINDt5l6qgH1} z9=KQ#Q*^I7-fu1qLD__L7AQ#YO5S)P3$SM=bu#jwjt&K|?TP$NLsx(r%A-)rH(k7e zYU<L4H6awvsMf z*E`B5$kU=cE=AGMs1^+s%FlMsiNR1``DFwf-h30ya*A)VvqL9}LiN1bfWeq(y3T>V zAX$xJ?+js3T+Rr8T)ofR`}=3r@Rp=IaY_0?@6|lM`W!14v_+RC`~0ZgPQ<3Wq-P&& zU3KAjJxaR40=S02zj?h%tJRiS*gtc~_XHH0fiwJzcW%AeM+C{--*M@4pmOk^os>yb zgf6BR)uUec3+Kj4_)SJqS zn!yK>PQ8`(VZUy=LK?Ks^z1qBKcgpy)iU}dB1nn?~$kLfsGjZg-R2{WTl&RTnZxu{z zWTcYiNtE$!0c%|vOi>bcBOm>3bw_nrh<1h)D}`1_r^36*WJ-6`7*{Oa(ZI=&g7S7z zc>(!6wYcnwGryt!)f&i8Gb;cX|8 zsrcA+EETk`=QbgeajhUmAUQ4PH43kr9!yt_Cp1-5=EWQOfYG;pf=gQ8Ekq@dPg3)B zwP2Q!_oPN^$IhOziE+W%7rvpg01wXFUj}oa0}qQ7;x$67w?P33oWRE0k-a5K&JDc(VVt%8Kla`|$kHUM4+8`{JJav?yXU<- zJNp2EEmjd0qPwPhr`hi5rmK5KS`cu%>g&4q>*{-pTU9+%3y59Bn*xUn7=ck(ivmdz zKv;4pf`AAf7AV56wPX_x5JFf&vOvN@mSs%Xn3c~fPoB*0WPabR>0LpD_n)1v`#bra zJbCiu$&)8fT7s&2$-4tfebb9~j7xi%UP8$Mfx**@scbi(-vg$XB!+v6Ul|~IDbS;; zO&8cBCKx_1 zbmw_>drCWSclPL(IyY8@4qs|ND63eri~Fa(*@L?=gQ+PZA{U=zSg*EDR{5r+fG@K8 z;e|(Z2yhA zZ5W!+CMuu8sXom3Nv_R6j{p_XwUZTI@C(CDC4Rw0SH3{8f@s<7p__HU_<8S|Su0V} zz8e{U$J@Z#tkG`5oZi~6P;>;azEvuHfx2L=jlWr;#uM0Ocp6CeB-dgdj%A6m>@%t` zHC+=$cP26do=J(W7d}UF&Zvhed?jj%PVK8(UPVQ_p1g{1%zJvkporDh!i=%Xn<*B#`N!css}O%XO(@KW+fqjl40 zUUgK%=a0tLvpppa=>mppO8gHTqTXzdt3KL(jG$Bzp@cx3mF^UKfukaIcgdxUdH=qN z@pp=HN!?Da@|f93!Sl?hCB&^91b*NJu-bN0`1B@9#g@xu2#>1f?MRR*OdMBm$G9iS z%yEHIY|WWE{qV#8+;G7XJ@Y1quM9%qP7e1APx6ijL>)f=`G^+m#3zVq^lgTJC$0rRU`$K?#poFg;{cLLm;;8#=0>|xrcl1u z{rw0e_+oJE&J3hX3CvLtm)m~;UB$F;rrT_J?eXOHSxjbS{5Q9wU;a>J6jFCNRP*Yld3Q1X&iJMF*QI04 zu9OY9zC9f+L9*L49t%b)5F_hJ`K$8#;IA^i=c{@KTm0_6a7xSl?TSO6lI!^tUbI!~ zB@-bEfZP2<`%h)Cpp+??uAV5 z?-WJ!r2I}p(-qKR{yBcd)vA-6h=nr+^RWJi%$1{f1aKKnZ?`vaeq#X|?OmJ$@a_uF znWgn@Ja>u)AYxc<584kV24K5=+RonbH@36n_m8)?liz>7-L#f;oRvFmU&g8a!Q?)| z7vrC{l0SVZ{^_qIHsUMrPrslxLXx_<=jHC?XTv|({;*}geF|H>rLBgChyMslsR@q* z2gTLkxJ>nYq;u!?%i7-pVZ^22&$NfFsK&V{vwXkt-)(;*!RN>DzuJD59nkSVY(Fpg zcg!AOP5ixfCCPO7Y#gdfmZbe0{L}X&sD1dSy9vrsx%VOJn-Wx#u1FlTA4b%VCMaj| z`Wi%?OX}`^6#w+y$)COr|MZLcv=ij6aq0~#Fq*x>F!gAxhG%7FS6a%ac0QG>Qx%F{ z!}9UCbSu}TZ*MmfE7GT1eY)Z(#xN~#Y|${qNQNAQb!|G?n!pj@N1JGJS+C+ATW|Q;Q3nfhYO6hVkv1i2vUt7?W@SMg>oJbxG^0%c0BL za3x@QduV=qR#VEPaV7jNf~Xk#58 z2SwL7XlJhOkwt+$#J>)Ts^SZ}%)(9;y#x^h!5#G4)Vk_3{iVK(ulRk&TKqidOf4JOa4AQoPX zEd)|x7((BV)6Qa!Ku8;}%wBCE4Ju9Wx3I%}v9v_q0L56~^gj+WX1A{&(6h z(WGkJ84M7s!j?f!cxm468-}Sk8NAGmeexYI;X)QCzR6H;7_a=6#}uiRy;qx)=lWh+cLSA5~SFb|wp6iz|e-^1rQKkRUPbV2&oz0m6 z_tScxEEAXCA!W3;>&Ik0BtG%pt}VlEdlZ1ZEgt2iv$sLP^|nV9DZ)6rvcbG3)6DG# zFKD?)uGDH43z@mGqdb)M@km@HgJyuk0G4RQ7dmFp1t54-c*NFK4CIwTZ%kI#aJ_~i z2riOxEfNJUF#A*OvNbbp^5|K!OZWGs*n*Lg-v`Q9v7PW?Qjik`=>v|Uh3HJ|t~hX+ zT$;g+x=kn-L@MLTVn4uC6fWW*AWZLE3_cD#R1nx6%c0B_lrj)Z;(A17f<1{sDrV&X zT#%u!z@lP`CfRn5(t6WBFh_6|=!c!tD>7H!g3T|)3E+4WpJK49Y>ac^gD`Mdb_)*6 zKwCSPy)5u3Zzcqq-q5f(Ia$WJ3PYcifr>EJm+S4nf>2C6UipT$Nj7MIzHRNl+vKrP zcmq+E3VMPA9<6;ip^#U81tNYGj{KNJIsv*QGcRJ8vwW+U7vM_#AR=iGVpQqB(hY#T z{?RKgSu2N(fGmxl0m;hEh4`gm($%#w5AtQ=P2ETqUlI@CKdqxge>>S?CxKB$eH>AL zqkD?fH^%vZ>B`zWWO_+G9kil0B1Z;&*o+qYk9Jg4AU&kT5Q4CsW=QF=IIYd0v*OTC8SAq!e$j*!mJxyV9RjfMtU8j85bf8HoeDp=k$Vt&~C9|2r zCC(Ka@r|N`wXsWsjTiJit`W`denUBkgl$>2hR`i+t2X72XG>|mRFbT0OV_4qqgh34 zzo|4A+?VVhgA|4$(|BT-l1$sd_9%XCc1>%zOG8y?AR5Tqa#vk3D1q_5HvOx^H!k748g#55r+O@ zHp>>N?@U!%Q_R)%#uh@b?HmqYgbZISp4!6~c5}W(67`XXLVJw#L6XcRpNtbFGRAuT z;r3o)`tG;?bMo)(=wdu=|CIGX$NzEr1IZ!4-)i4)_h{p6&o=%??Z2?Ux9>-H<=Yac zCUJcr8-X2dBNiS0*cm$N)sZIwxv1dq@<{wq7K?kKkwlVwR=P%OM2>HJ6|kybr^ugJJOw6e^6@eCz<9;+WttK2PdZ zlt!ii-6L`trDdhp8qj?Uivph|&MuxzmR*XhtL2b2G6I<3huTlbY=Yc8TqBvFr)jQp zj+Vb!H5IBU!r}{=m{Ai|U}nAj-;=2ynPX;uvi(In_1iDPsXIy%HL}eIFZM}si?lR#_lm4hu0h?j-0ZvD@=J3!R zRG9#WQGeJBuVe|FL=HwnF zowx%a_o~d8`!HS%IXxq6IP;YpoE(Nhm0e>Ww81#vAia>0tVQ^B4N`Sml^P;B_VRo* zT^YNr%M#Sfu0*Z|u*=At1^44{D}r)}HlT7n0L1vj&Qk~551E* zd0Y_uuZ@ShQ;-w{4G!uk-qp^5aPw~3+?|_cJa&Avi|5?S3&^F}#h8CqGN!kST({xw zW;2H~FF50lR?PB(SMA@J%^%!{s6cU;#`%^!PyhYv?WYnyG_Q;I4jkqm(QqUl<%f^n z-o`|XLT}=C;4BVTXrs%1f5=ft^<|lp4}CoCs#ClVq`R|pstKo`K7>~fd4VE^SKR5r z_1RMF4$o!ds@ahuFM`wtzwQfcz8AWnS2{wj zMEM1m6evUQ;t$h$8TvR_4dIAnwvm3tZT?IbSK~&G%?C6r$L5O|5Eg7^SP#Gf3*6bN zG4)=J`$4Ja`3x#%;o)|uiiS6Joi!^4e@44sNN&XNR&4CcwsVf!3CH3$n)w=)!o!LQ zQGYGLf5-$TWbO5UuaP-3)i$LYemFpUI4OKj;~FZEQdgunn#Ydnk^Q`6i}S}_gly4o zZLdQuzpqUmzOfC-qhbbTd2m}6lDpG+b0112!`s=bB72mCOLCT%ecN=<7Z(o)9q1-t z_aK81qH0_A`lsbzu76>jFNF{ap^=7U`d!oea<@P3bispyj|Fb5?!}wb zwnxugff=_A`%Lpm+m!!hd55DjU0v}%0W0z4_1Qi5N6I_ofpLFF7fA*V4mGl*4%-oQ zkpzglHTcgB+=}6!3TlL>L59Kzz~zK*&K6rkf8&+OebHfGY$%DMZejaZXX}v537?mx zH+GQC>&I~2`m1{Mhu`d*Ea=+~P(9B00GpR*zeg5ERHUmmOLR>ybaMUqEhS zIJjJO2_=G%ZNBVBX|s>&!8)7gWLYRVSVqb|TlB)N3W^WmJlNVO<((2Io&0xaKjxPNe`m5)@T25_Fe*kL zC-M|#B#7mEZABEzkL-3LNpVO+sS$oFK$hMc?f(IbZA{f#Za+H)@;Rgyo5sg>Kotg% zO2OOZo#{$`ygPt8lqVU^DZ9RV;#7ibd*c*comU65iB8I%cueR}sH&j7(yaIhn-nuy ztU0(F0L&l___`|(nt6PAp_dgn8WP$)=mb|@0a4?^GvHGcjY@?*kn#e$Igt|kN;hWV zk~^nb_kP`#Ift}zH5dFOJmh>f zn~)y71tEFkP`}pxo|voA?s*DKfJVWZ0O1R|8g?6=tjFuo(L1pe8d@+EKJs*EJC=9P z5*ci}98&>KBD&7{nQrs>3lz5fV_mqvi0irJ&1G9w&8`3*#q;upk+cHafd5UWDhql z?}VI8Q;pIRoHth@JG$zM1k)Ui&IK)Xu^0*%BoHhz8-ZnMpuvu~pqK-`%_`{vvCXcI zxll?`b2{&9FzpT_x2z97l=2KBv@$>GLPbOd5%|q!9Mt9|)w=9zS^C(b)f{4^D>t8! z7hwN!eR989*;!d{C_aYop7DcDVB*vT=+4%2;p(9yc6W@<27)ecF8T`Ipv z&n-WgY`ig=Y~6uJ5MX;42I3Cp_)pWjy;f`|cmokfu&nIIG(sL%aP7j)c2g2VAkB)M zTTK7K>Db_r9ZS-pG^M)-r;otn!#Eg!U8UY!ZJO14kXh)qt)&Vgr-6BUsl&2^S>U4( zfQe*o^HgmvvK>dnHb`fD3-BynmF*)oEFMv8nI*Z>zF-q=#lFGAGg1kWFlTid3{n60l1bG-!IyX zzB+$9wmjO!olLPiH0*&hX-t*QwnAPyHWSCf{(M#<5xgX`agT|=h5r6_`}um@eG0t9 z*JfLj`{M7(UHA9>@Ts9-IuoK5g^^Xa5`z@zJ@-2iCHJk?KTbP#=A=7NyRrU@6eCNNs|d;}OUc z_5^C%uO$=qIsDU!BopNu@lU5!Clh_*p7=w^5nz6U2p0_>Ma`x1^MMpSr152RjD5v! z!DTYA7%|B_PTh!MnSD>Yqp^}Rydoxo&g)F%^wAtbN(R#}_E~1*@3lXX%>S=MS-(T< zB@fb{!$1AkNpkW50JI0ysvd-#7&t*Prcq{N&h+1De@Qzno6J=5Y8N6U9I0Gdc`%*5 zx!%AH+AlL_WVhS@7g)pa(`fg!!Q2^7=5TXqzO_?A%Z_9TYJAQf@-^t>#WdTpJV148 zveCe_H91rf#G=g0Dtv4EAThQ7W&4GRsQ05>e=<@2EHwb8Hyz3jVpEL9zXvv-j$0Zi z#6JcSQP4q+5`&aG^hTgE3wWYySd0b=lO)$sE%pT8F&_fObFZEOMF=QHo{eQc zh-+!@fVas2a(pTfsiW=o{jg~sJ@IHsEE9NuoTze*W7^Mf$MxIMjM-+%wTmM1K@@N5 zbOy09vtx^FBr?sVyrn>2CL2HgIYTMNcpTXy2I7#8E#W=Kf)K(f7AYPq@lXQ8I@eJK zP*n-4gssD_1Qg3}?1TW45%r;XOP&X)v)8wuBv{A?q6bA?Wy6{?z?GEOfj|X3M_B_d zZ_`un6(C;1O1jbCc88?@D^I~!#GJV7UI^RH;WplafPx&v+9# zN#cmX?(Xh%bNe1&C5pq?LnOzIc~9IikxCBX{nA@A^TOrb_8sE3=?yc%+3Y%tl0pYV zTH`(ir1EB4J03C)1FFa!Y!u<8Oo2gqBC2<6n>DY)(r za!z9Uhwb7)1_xGLDa-xffGMiuF^D+GCSwb#=iJ-2`CYHK`*c_;B>ys0S8zh;dMuXsf|CwHsX!ZybuCGt^Q9sR~97v2C7XI6L>SxqCE$ zY(3ETV*7_JK@r9M;e9|61P{1?6yalQcxZtYT-|OTBpA%d3TA)6^n8rVmqRflsDa^r z>@+om2dKk^foI6fn=j*&(&naIkgkarf?gmKY!Q2cY|`TAH_Xg(Gy&llpD*hovjL7` zLv7!J;PRXBu)8Y;RtvzwBZOclkq-#Jt*LYXKyW(FplE|O-xXVEtzyrlYHd))0*n87`wx?lOJtcp`q$c@7L!Z=PHU&} zpZ+*TW&TVOe|(64dLxO6eG~raWw{)~gx`jLiVs&vh)I_Sv4o(!KJV?fBK^;4OijYY zFk?8;2I?>uthl^>j+()j!)??n4kEoD}|K9ou0jYiOHcNI}y1F9mg4z?n({rx`405gfD zF_)HKe-#Q>6-)u%XnlLrtaYZJafoBkBCm-F<3rB`<;_p)7b}=Z;jr$|!+(jPf zU%vPEdX-$^i(FjvUHm1bQ>Nx`wl60q5J_bzwwFm9m;dw;G_-$^7}})51*5(NQ9rHA z7u}L2Rjly0=yL7o=HoH>keG@da;~ox)_=ME9}-b!tGA>&*tOgDF)3V)ila-nkD~^I zg0WTdffUycKp#Tqq6mQ;$zUxUyAAdlQc=W5d}10-yuj`fa~-AkRajz%3F?iR9s*#3X5#1uDF9iMPd74 zl5wj$!s^B*yev!>r?RJ!OVL5A3U+DHc|UBD5Aex(vz1Ttq5H89yWPc&vxD`{gNb>ywPUzceRY!1kQ88pIv>GX?Bz9o z*)8Y&)6c8=%&OKf@OL-iRNWl}^_c^_0tKJOjA?%S9ES}~1pvA<13zqQmW7ur3bTpA zXHh+U|ISiS?JOAtT+9j@(>DrlQ-j(>V{&JqDsFPrFdxc3T3~y4FdSoMp%jZb3~R;! z=>lpo=^oW{s5`T$={yPT_fML`CkzSJuyXgW*5yz|VhhSZf`x+Q-RahKh(T>zosB~Z zC08jI5A+WRuE3RpEqjJwfhPZhLW22b-35E zHF`@YmWA<+5^j(W1e?`*!-tpFAu0y7Kf)VUZo5#69lkl4Zr++qs-2f&mvL}pvP35T zu$b4=9Eb|t7BSdOVdPoER#&F08?ygjym4{X|(%)DcQ&*+npxV0V6T* zpbCRU`>5HgI&58C-+6P~(2GzkUW<{tBttvO>*L$+=qc>z|C(f$_4S=4lpQ+DIsGtk zF!NP$Km~aY!sG(s`&Tw@X{89YOnkD!$iS;(0iPNdo}7@vQ~HudiTcMNQ?79=^vt5n zGz+f%Fi&9$_wu4qwn5@o8S(Pl+q(9&I$-Ks1k^RCH=ka$umhzR8KUj&WdxE3Sun=D z^%809Ex6+O8{UyCo|G|Bp8uV_soj1y&Y1Mg?Bs!LdRKz^RGZ$*<%^+fo#mjHREP7< zYy%!$>BWOU2Ct?NHqI8(f<67S?FsR-=>;YviUaGeRZ!^=QD!Jz7`WF6Q%SlYNcY*KH0mQV2YIYl64Rlk!GUTOg5-H`nlnbit-m&G@1LixC&wC{_H=X~OmA zk~6~>&*a6>xJLS4c$UtLGiQps%=XSFqi9hucE-Vu%=3K7tlfa6l*+?+#?EJi?oH>G zj&CNEkua{f{Hfx>T;Zu`aA(?2rAg_%hG6m+*q z=TtxbIpn%CdHU%{m|eT%_xmEHN#?3wU+oez^M}2StAoG%+RYnRURk=+O93u8dL*I8uYaLR z#`5tbqkv`iXE-W^UW)`=#6CiBFI>O@0tB{Q2(4a$Q578xO*H$KYr41ZjEj$^%PZNS z_()oNx+CI)$ps1hvcuuS*>3y2bfp5O3kFGeV zybKv+kdQcp=y@i(K6-d>b#!%mN+m^y5S|x8_|9~5(5IwVw_jPtxaBw~2X{Kpu2O0&pg=EEzuh<}xi%V4ZW| zv=Bg-4SBRm%2R_#K#p*`Gb)f}0UT5mhMjahf{_PbbBewW$P#?3p%N%TlEhtF72R?t z!wXjrn$?LU8+oNBERELQQkpAotu$21)E9xI)(+4%5f_LY9uQnV z`|@qbUUXqNx;U=f;PP~|u{oZ_B!1hymx5OkM2JO?p;#Si&t=JtM5Xro`u<*OGmd@} zdR!A2$S0jorh7S`T~{o+d$KfPoLq|MLjR%?$9a&gMpmlFx^#Xw-j@P9hGCftzEwV@ zZe^l9L6|dBSKBi9y!+i?T`He5ayK(45c-|eAx0LBLd0Qkg|F48GO`MHdYqM!_UbF~)Go7NVfhRO;)KZ);1bFT3cR&?_FjUJ>@Mm%=F?I?QR zu~$RIbJ3>?-y85H>3#1&;3hEgr&QrfNkJbw2G0{+LG;p1m(40+ZOm}1#1{`xQ}nF5 z>(Jr;bfGatWz6jX>mzprQjpS(nH2;oV@_<0$$%E{l1I`4RVwP~+G zF0)pAU;7u59M|7%zfclZ;YmegBS{0G@uJ98#pH;5B4+VqT=7Y?U)Jv)Ug*d%8R#l2C$T=z zBM8RCX5pUUL*?Tu+aK5L{4;uI{L*x^zO#jCM-Y;Qq+%C-D#ED-RkL~%UJIJwBa&>v zY}@-4eA#SvE#j;qQl zh!flLeL)Fs&%kM(u0h}u%8UXfkozRZm1G&zb3WPHfwOYApD!vBj-l^E^;B3dxta;A z6m-=GFNe6Kaaa*TOs!1d0Xw@4*?wH4h@gINkS0FMN#CZp@gTDJ0>BoG#kg6;w|pQx zKUxK?cRLeH!os)(9zg^yn4c>ITLBA_$%t7yvAZ+kQ-;0eA}j!4;Wx>q&~u{5JlO%c^~R0WqGBxRViE$rZIF{ zyru}EIn+LhX`CNUZuYF=pMF_=Cwaa38YBR&@fZ6BGCb=DN4*m)!(?s=#Ltg*pW~c% zsKL4kvjZeA0_$u3jL>1xs{2r1bXD(*xnnDJFlb&ES3XM4d~jb9_Di z=^1^^R-Ty%mQSo@S)ehyyV(`d+!<{?2ybt>9lppSEZIz#M(%Kcv0Q?qP!}SXC(xe0 z3RfmZ^Of;VxNq5ANkrg^YxaF#`?Q1qJ_M;f>!h%chJj7SmuWT7s6U<*I+ zIVydK1%gE!>X=9^Icxk}e&jSn=3O*FE?C0##;Uonv`w^;HRA`{46xGi%SB&HE>;XP ze%xxW@f-h2`}@gU8DI=U(}Rs6TnNA}!cpQdYsK(SzqJbAiGGqhoZ$dyfVJWl7qeDb zZru3OX1$oXy;(1&?YCZnD|mdac;$j`)p~KWL#-F17qecGrfR(eX15u`te2i3^*LA? z2*;CZJV)fT=eXE(&9z=U7Q+N$_L&-gqWz~@jZ3fT46}XOfj;Y(x7%a?^j6LQ3#i9I zgKVJQR_e8aZRYQ37XqbquL`35y7q%AS{=hc-@k`c@%H)kn!q~LoWji=xhn;ia5wxd z1OvltvWX_BJiW3? zapT`_=ZOdHl%&{3C+CU8>r6^gG`pHpJkb2>fEdtG`Bf1>;LLub#d=9I(^8}FH|6_TvB}O8T7&>*$B;u}u z_sqeEPzA|fiP|xmlrPnyT0}b}_5|OE?3#-OmSKDb1LO|tNb-oag`hBd9YuVe(9r<5 z^GEn{rO|&M#;0={zA8j8>CRwchfJ`=A5SWy1rOUnHO>&Q_JMSn&h^`TJUy5pIu+fY zer$B9d@R-KGDgBj(N#!6-R|a*+A-y$$maeC5(U-Xm%_0mh1eb3&x%j&^FiWG@jj4l zd|=;uM~u3bJBfayGzP)c_e^Z+S->Vfo!>TH@eFu`_%n9=9qpFggpcz&akp_s|Lt0Y zlJ460@kyUG5_-B+`9P{g!)ufepkj1pa&8t(2i@**oL0Y88kT`e7nKdvMXDg0RQ85Rhu`fvMhjkclFl`}I;Ji(4sPPUHThA1V-&6u(>lV@=*O!R>02)rmr4}v$r z+5>6V&pZH$25@n!#wZrWj1WGjhm%>ixns)>2J%frsW9lL87SZF^B$nyB{pl&{$L(V za6aY~V}>ooXBj+rjW`hS=#To}-_H`9v-5 zvxHGR;iK#5ejeV;!WJ1@pHK-DA|}lUs!8~BkMnhIZg1U#+}YLFXK;0_#xQpamx>yS zi?zbjWVi1u)o}a3_uQxfjvGL|%NAMh@hN_qb}z81oqFJUj(+sn(lA zun0~!;l_FmwEr%AsoKmDE{JN(WpQF8f%0=gH#ikplL-I_!>BVdC?y}p3 zw8LVZivgFk;bqM9DQ2%Q<5Aw_16)Sti=oS|3}$dAbm(2MXlMW@JJT8gK6fkWDfm(u zh|#9?+W>$lfbos0;}YNlPke=cTBQd`M{b=G!Hv;dGXDO0e2Aq^*rIlb_7Nx;^(B01 z2yISDC}L2EsNz_P=2;J;(x`SVp0X6Vewn+Lx*BBPEjpnQ1q1)O`mOwKAdL@%e0xDO+`9vT> z(;j^a+y03hMM=dvL>7wom!N9{dy4nr+-_#ejQs)yn*+~g&lfQRbO0V{<5)@@Z>EZ^ zKxJ(Rro1v4t>13G7V^Zm%AJNXawe{K@1`PBumMKCQeVSLqj9w@San{(XiReu4~&$>FuKqr+kdP+r=G zC*`vT6D*FNGX@+%SncEnlFk+je-gf_Pkt0eqiO$svW~RvZzlhaS)|P_wpS4-omxSSERQtVEZL5#2N)%vv* zufm}~29B3R_R6I5eY0DywF@XuZ3ASlw4=n4mORzNk;%eLj}CM-#f?;Ojp}Ez?fPy`(XU5 zZD3ka0CcZDSqV_-0Oq(3#3h9}TMaF{T|5$TwJzW#`aZFrN-=f*$fpgQMWwKP)hrem z9&OBr(n)XDT_4FQDN`JI3SlQ|Yrjs_LQR)@IG{Dn!2Pcu2e*0HxEv;=Y0w=#*<5Em zccJnWm_Tb|EqmX-Z=*`Ufu0oEjs1;f;J|kT44x1?1ltJrDB1psaf&!HZ|$4t2YyVG z+bB&WFFt3?M&xslKJ6wOmCNfz9>0%%Ink^28z)<=VLSayK8G8|XyV|}-_T8i1 zlW-Xbp6|)QIvf^z;L_@<%IJ=bcAw{E47M06aPb*@Zw(bkMzn29DAtsg6F&KI zVRF^bob)23Q=*qU}1KcFl9%SAhOL$khod&%mm*rqT95XO%_wC`PH$2MN=n#JY+0TN{!T-j#Z_fbD;(Aq}0&ib9b%l<0VlQKl zag`8V#H0Cjs{UjHLbgXw0s6*h3yyQ*+_b;r5<5(uSYf7vG=45SW43KvYnLYvAuJyR z;q!o@r=U*DTg`@#qCB2UD=S#J$;?e3Opi8HM{+T`&7Q%eWzLMI-#TYgu7Z~ihZM=! z`FLuzx381}Rva0>Wm-i#G(MKo9l1~f+Xf#;&ITskq5HC0Q@R5~n6 z_;csb@f=mQTAUqH{pRx_mbIuQ-_L}3Od-w%o*P*Vp-mnDU^~BqHt?igIXTTW8_0}< zzcATo^1FKRAx(H)QucQ&S8EY0N+Mg_oJ!Rq4skn-C^aKjuqnyX)IPE8Na)L)CCf?? zLEP(CvDZJX7prC6>hG{zq^$D5t5q)#-F}XUWWwr#{DFWYJtJO)xO`p z$36b3b~*WX{swHKqI5PQm)A%ci{Hr)7nH%;Q0dvlB7@cJQoM_$g{Gv?@hC2yi{I6L zT*XHQF44AVlcLz+%`0)IVH%ddly{vHRHfT~v$W+4eXcZ|X(S|g-dmq+jw=(HverZ= zmLUiN9{MGSHkd*MN%!bnVz}Prg5-ZkudmsKhYld$B4xm}I8WjEg?^bEvaCsd5hky| zKCwK?`2)7ulgIhk9A>*MivoP#WlpSnMb46N!H7~b;l)?XN-r5q<|pVvv7c?O3T&`Z zJY0uLCc#?!t2SueCILk)U_ow6rWcV?9~Oo8=qE{$3yYC+2e_tUhbJO=9GsXwnmvHh zbrtP`oaX83rHx4#jjnJZ>w3B>GI(vaHGw3F5LW4eCCndM^QCN*6O>ULyhSG%ll6_d zoEcO_a2REcCF*X$>XQU-$L$9+(gIhMM?-MB9_RkNZ49Eyyx50N#sRdjqml>Mz@sK} zUS!maS{&}jLrYGe#zOZ&?bmhKabdEM-xm7)B8Z;0_Cxa2e5ck=xCVhyEc_U(1n?}w zrr*&s5eJ^x6fTs&t%0q3%_yD^+fKmIMP63S{tT)7^hsDE?Ta85a`tt2E@#z@c~4gl zL+ng4A(wZcxK=!np0r=2I&y3YE90+YcdxvaS6s9}sc3c`N`IiwaOtDl^XBrpStKnu zoKy(-%Ka!q3(MNPy7|`i?M+BK*HN}$seKk00PsyXJl$}yQtruOAshQ61va|qHzw27 z8C}w0dtv!F%FAnL`_l&-&T0*;3oz*%Q7G2R=4`0(;6<=t-sJR_^ zlyq0?yo4tEwO7k#1d@XOc6cahFuV=I>r%h1)5ic3C`R74=POvnGI?amDwNg~aVdS{ zrZq(tnq1nQ0T%f~4JDrhhv`4jaa$$T?Dd~%ggKJN-jsDiepOjMP zMYATEzhQXlMom)o0=PKY!#Sob)upK}R}?~X{S!TFw={hvW4nK3BwGh%URKMf77%V= zsrC=j5jaMG2aw=MXmvvIkf@e%i#pmp!!Wo7hW3Us5G4Urhu*?Z!-itE0q5y3ip7YmelG69nSy#>ifaSsPx_$rica(gF-vj$fEwD_B5K(auG zWkI|`1D42;%ZWs{VVm->*eSt$Xnh75Nah^|`&IES1LjU+3XglZ`D}Zm##iu~AxjXB zJ>g#IbghBChT8`b4>X5i;~NbY-QFBeqKj=A5U^Fi55YA>zhxOG3$D%`)+GzSGlDZB zzj4tOy%0fDOxCT)F}t!(dCx4UE!k6j>JEEmJWcP>Vg?!Y^ySVlqiGeje0K+Yd40Rt zm`|v9GF;Wlb6ukdM!VpVBG=|iHQfX= z7^0&5U8_bF743qnD%yotR1|!5lmOmDTW;o%Z$m0EgE8~hnseYYj22lxFr3(@vik$3 zY@h(qr>uB9j~IT*t+YI0B7xDeiaujLoI$EGyTi|~qUv(0U`W*J(R@q_H=h9mLkI68 zla6U83Wlc)yf#ZPN?V;G2nf3G_6jS81we6f5U9`Equ8&F9^Qfg)@c3u3^HBr&STQ1 z?I0@5b;$Z0tu?RBR(+=gW7xIHh`A@jJr?9`*i^))VJ%YTVgAaL?GBEYC8^9sD!ta7 zi0;Usl+tyj>xoeS+{`_HM$_hpP{Bj-aa1MB%)vUEQu>nFh%@aGWZ#(tA1fzfBO&nF zki0^jX*_{_?7L7$%w+LrrB6%wqaLjfr>q|XmwD-fsSifS^9Qr6TqqaE)#Xat9Y?8x zwaQ70kdLL|w^-4AA5G%0XjXe4Oc4MJMp(g3`bv!Wy+gB1iL1ej*Wp{U&0t&Tu*02N zrx7){%@-iVZ^qqMG50%1Za3&E^zQB$X5~{`{^67Wfj{kl}wp{vDJWtCz=Vs8Br zl$$TOeER^o`C=?q3AY7j6^R*ex#x?A?zC|6vgbF((1l4H!;ELeKad;D23%W=Pw6*u7e7TpURXt26KGdw%XvF1juwOf*!wuuZZyt2T_C>SjOF%%|x|9(i` zDuEk6P+4TFnTJ<1q`?R62W{YO{BPTrGGZ!+WX1B|7!a;6Z$wB?6-WXJ!SFjQ)uUJ$$;?=`6c|**#z}6{^^5y z{-XuRwbwMZm2O2gpz=eA*DLiPLT2u(kdsfUg6WZlQL#a-HM?i7O&8Izr*1d*X0xrt zRkkB6DFI9CaDGVvvcdpww@+KqjX2e)k3rOe9t1gt)>N6ax4@Syo#U2@GET9 zpu|MHWp1$;v(2!WiEbjE7L8m0nm*mvvV%qYtoF>dD_q!iD_k52m*+uPS16U-ZS>I6 zYy%G#Iw|`0%W@iA98Gxu>N*sAn{GlBX>|ib81DBZAy|r6M!kxkBW)gkfBWr;#rpB~ zWRh+4$8g4fujYYCIFn>!TmqK`Iawzx4S&!@oAMa>frUiEj{M+Z?q*n|WKCCwMb5j+ zqU4olh$v-!12+uctOmA{aJXel0%(t2=*;Udp&RTlq016HKV9$LCH=P+9yG}DvPxPE^n`{6)Yr{M<{j%6BJ3}<;fPy-@rNy@FChq zSBAI*ZMor|S&OWNPRqg=)t2o*d~6R+?gG4ThGPI~`1HoerJwg4nHlZk^({edq6d$` z=#@?L%CkFgI5K!y$^8-Ucgg{0&8T3AGKIlf6~x9;vW(&<2R$Px+)J-u7){|++^zQf0M2{ zr?%AxQo5{|RzwsOdBFvZu5Z&I8)f$ypcs{_?mnKRN%)$iID5lh2k=`XM#IDa#jI!- zN19+ZKv*6}{zm)9tY2q}0n+bnwyTl-RIew~v+zi6cQB#_UhUeWUFp^$@)X*1U#y9Jj?x<(`d ze%Su5y=gN3k@mNeUGG0`f6ppId!4rR+FyRp8dfj}qiu}Ycww-T^Yr3$h@TnlUI^PD zv2~EP&_^PvG4zo*7O_$8^1o|8rn`@@so~6sGELUsynk{%W|0rG>l^L(FhS=Zem2Bj zZqC<6)5%9^*z&W}VNH*JtUY4KaNIMdPow7yf zzN6iXT<7D{&J%i`Fi#qACD+1KVePWqkf;4NRg2ENqT=5gtvrAeUVqBavoWNE_~>VKTv#b`S!9AuLzAOG=hv`;2ibu(i^uZWaJc<-Oh zrxsMA!DFV_qq0aN!>|jZB|4E85O#HgD^AIB2+1*Q30$Zg77SDZK_?ubrw}s&?C46t zWGl|kjQNmD53j(zu$AW4%v7S9wtq?jNTXeePlZaEakC}L9kDFoJly`H?eEGu)r4Ts zYt7c15CV)%F=cEC5<@v`gek_55C)6lCcJY**K2irEL@np1J>X}^9jatP;p$S9H0cK z&E>EyN;E2~LS_+?toQn@r7QV(b^8N1ukk#kPu7fjJhJAthRB+cLuLJ>!0pxfC>QE_ zn-rR9b|C?uQndyQ4`lcwv?g~(43j&C30UYssIF3AoxGm0Ih#PB`rHPrI$R9hZ4zTa z;hRy@3zNo95@dP6s3Gj;eW-y2mJ5x`c3czUXux3sz=n?OI}p3Tuwn>md3$2)n*B1x zQeec3(g{A=~VXNU|DNQ9{sgxuf8W+)Y)g_=SZ2yb)MeARf z(jd()ebEuV&)+*SECEVe{#b5|DBQ3Nm7MwQx~{DzACC0e|0E4jP)bo=A4$=rfE4xR zvB02e8Z23@O}V2N*3tgGZ6bq3x2@=3Tt zZR-c%^@(W{6KZGer?l#-oV`KUCu?I|NJKdzhQy!m8}z=P?1SMJ`TEvaxrx-H`0%?x zZ^9K=Tf}Z`!Fs3v(scF^9^Z6Z)GMTvm%8JZ-G5&aa zTm5b2#cnPt(m3zMGO8kZpGOgHe=b3d0dT`%wWxganb5*QRN6C0{m1Q#DdFbI551)& zNRGS$;ev;0xWJAuraX7~a9iYJd87!C7z_dKI5KP#Y`bI>S!-t7n~+4V**#q>EkByn z)cu{#Oypx|R{c4V=mC9RHoDDf=wP(TmQ`AQoe5e&TkSH8by?pBfqbLp1GC>bi^7rFL zc>|u&n8U0ClYJy*b6n zHDBRBD4Uk*bf9__rWjQ>7FrkmbU@#lQ@xefC-bfC(K>wvJe?RNv7fl8L|Bt+We|;i zi2T;c)*zy@K~zQrZ%nqv*GKX8lx6IU;s%3$?yS_k1RA^9JHy9 z7ej`RR~g?sUV}*Miwl+o&g)03aK7lW_XPp%4YnD)Fapu{h^&uxUmDnoPYri|CEeUxF1O$b znK2qhpID2ucpdObw9ZYwO!#n32)7zm8gA7L3-;U93M!zHK0@oTmcFJ{%R=D_za4mr zjRkxg-gj!`&CC2EwH;6Gvdji?UHsNnocYx8;TP-)W>js=WGqche41sXN!iuK`e_V8 z<&5CoI=>!gyL+6DsV=W=T!m*fp^(ZP3Eb%duIOxI2v7nFv47B1*&M1PVNZ>vvI`07s6%DpwI}KG4;cm7M*N6s#27Ex5PNoRSVgrZhSKH4j-~53j zy3kWrkI*;|w-w3zt?@~?CY43IWd_Yv+T+8!3{emG-U=F_9e%zf#^8)F?jM2W+Auu9z;+7w}|1 z?!_JD{bprn zWgXH0U~`RwFM~R7_a!DLdtsz~C)$&%dU;F^daq9gHunu}ldM8n^W%TrKA%jd>~nam zmwwCM(T}{3Bs2f9a|K7+ywIgw>|KBta8~c!g4A71NEuD=0bnd9%BMzgT%ya)wj;mP zm|sfF6!8q%Xj0=+p}Y8n(e86x7|UE8#hC|7Dx_pk2`+OKV(Da4iy8^8{G-XL*{WMy zs3AomG*b)gG4+?1zGO9Q!c%>mW4a2Zk@4k7idUN}v6e}>CUs-72K50U5&%6)l1waC zI%EXKb_+Vh<7L5`>!MBw1Twd9)Lppj*G=2$Wy9_VWRe?-#Bf?(#3AS(F}B8h#_^dN zG5+T85tGNDEdh4y9vnDE!)|*oyYXQ|%-k5E)`qwPt`Bhs-aEt{isBGI=R*4@vwB`~ zRF-($vRp)AvKHnZY<9QWyDNPbSCmO&^%LXePG;TAi&F&W*O*fY76$^$v1|UQ!(Bk$ zs4g{l){dE&q#uPoWu`V?f(+hjUr*NUnC;KQ^R+mL|4%ZAbNtgEsHdPl|CK3JR0@yo zR6c1hOKYTn?9UR6*V+s#fR!1}25nqjA6*sG6UUCt`?P*m<8fEs0#^XF3G@$2PC0(II{b4=Xmk7R0B5X)gxA=S(-u$+vi0v3Y58 zIeoL&a#^LkGt26jSW4XwC*R~9iO;2){d`sQ;V%YDc5@Ei9^OtcD`Xe~pHx-7bh06gtrjYsM$>iNMq7@OKAQ0yZ9U2iP@U8rU}<%QhU% zCRB*=zp5Horsj0PuGQ3RmW*_`FQL{ZYr$t#Jr9HSk(&P_TI9-2)pLlDFR-CW>_GIC z-1aZFc`)F>jtf8Eft!iyQb;Gw2a21AV>cj3I0Oh!xnOZ01FbR*ZxmT0>9Tq1cC$Vk zLHd%~EYfp-!t7^o1#uQqL9G+f=eBU5u;hlh978+~YQilt#T<$_-P~_H)BOg|?5YP* zq^f!kr@P-`*TLnC54eUbl~B?jWh6N~MqAm)pS#mdv(x<*HUrj_CZJ&31>pfWK5%NJyuJ^U=kYG5jG z$)-yDa4<%Doy%~eYR!*DJ&W%l(MuoKpw<9xp5YjCW*dp!UC>24hsBj%xh&PcN~O3K zZ1hCZ=<;;5u{oZZ#Nq{;ld}8D<(@k$88zC_W9qJiA+_1J-QjR$@Uk)HqF)P&PM$V+ z<#M_>y{*Qe`sBUQdZ%98f}DY?MOXEL>g|0{vxXkmpk~d!{Xq>4zWqTBEj^Y&<;EV* zpz01?=R!If=tS&I_cS2UZF%pF>dsCZ4WsNuT^Cev-DY+g-#f!vfl}Mtdt17PdgXE{ zIJjVs_1OItHC%gVPa~I!?b>^L?+UwQ?yOWl&DXuXx3ByrGimSb>8@TQJW;AQ*>TwO zWI7UZp;V3b+|-HCRMPD|H+P74Rd1g!*+gqYsU6xgf6rw6b?n8STRt3Ge*4QqXz8&m z58T+}8C|F}fxFeH$a|I+Y*O)6G)&NWJgmKo8_d0I{J>-Jv(Lj-6iELG-p*rEM{E`9 znKari@%wiD?F)IUcV@S+TI#ha7QNr|m>s0upt8E>b`DVbO_1cCn>n@o<^=McFdJ)_ z3gt(T$8eV;3rMg7k(;bsa+)Ow2`zjbV9(_2we6K0>-7jNaLf_f$S~*ebldLY=w5UN z#3iqbr0aBoG>c;VD3jzxrQTQeK^6}uememB1fl_9xUCTn#teQ?B`)sKzAOm$CuRG{TNoiag=13nwXBSm769hJsE`%T6N%Sr# zw~OcVo(!+k*d^JIt83uaMNo$JLT#SC1J9LCrjX+lxD8X$AG&vOLUQ233{7HrT<+uv zojuQ(XC)b$R>$OXW_k~pHy62`3hx6N5kZt1S&X9-e6@agJc8!}_!iX6yf*u0`{jCB zXdj%3T-%^aZ*hby;7I`7Zq^`oO9#RJd_rC!ri3UxZgioCLB@E{)58#ZN|)9rBV-I;nEj2{TY@_vn!>1%Y6pdW)AZQN=?j7%C0-QkYN*`be4H%wlP_8k?K?~ ztxw=6^V0U#7*8}I85q>ElVfX#K=UTOLGVq0`o3Z2+IuUuZFM+bfr>*D%p~5L<1GbK zDb~WZ-Pl>50rd{AzjEo;ae z#PIxfKL`(gLXenVKOAjtLh)XF>#NYAzu104GIZdcrv$LF$dyl-u8=Sb$tb8gxQubi zPc|$r^6=g1Cd#Ycp4@ZyEV1;=uVax-Bp^e6ZdJ)@&Rvv5su zAopW(ZyQc_U=xZp8GQoffnz;of&NnaGivtJ0COb(rP^f;B76n>Lb#NiH9 z|G9Y|R%Z$=x^j2|Vt~b-eI7jjL-7w^Pse|!{iVb*CKm#m-IL-MkdVn|SQtHt z`fw{BZNEs|`Vq4)$sCuVUTCaYY*BP5f#sdonpHx@iDY49kHTiIf`ek=y5}o@ln(+z zb2@G82PKvZQP?@uub}A;&nPG~MlnXaFL2lwED#=OuWID|%3Dyj9QyU;+weSzOZoXs zNv%&p71kSYX$>Mun_KMthoko77O(yMi(VsG@Dk5&v_zfz{bxPW*Ed#r$j&nv0K-)* zuvv1KAP%49@vKq^W0AmR?C7OcIQoI>f6e8c*<@pBvWlV?RgMj_(*1Dz*VIKi6e_^V zz`3m-2q3Jo=QDgfK(1|!9^$h%!UBIWaxmI`FBM~fBCXr7QQ6^=&39%TE_r=p%GYcq zLBi$c7>w{_ZF+6v(Vq~eP2maqC48P5fKs;e+D09HF+smJ3hGr8%;BJHO%3(5q88|7 zd}I8T@ybR_0qqL(Ic@&RbY*??s4?N&Yomww;I}Ksn3MvjFbVU!cvhb0^+N$o5tTP4 z)79CV?k8}l`2BTM3sB9`X7nO#(Undj{cB?Le z<81$y)<-bmR<2K`4}iq&@CioJm#6S-BE&_Yj+OAEZF!ZKxKhG;X zv5Z&B6c>Do+D8sv-kz>v5tq%h5R#S2VLm8jWO5VP?i>1pGw!Jkc#gk$?IAoa$S3f(@Z>*z^vw25J{|U$wNKk}au@kK z9iIkMNNjw-e(Ds{EN{&CIrQ5Nyc1JeV9!Qy7{4n1BzO#1lpH3&>NY<2R~hpInA&=E zV{JrYa%baygct{~FdF%J(66+=pZNdr__o=7L2_cdI&^tw8B#*10FTSe#UdqXpG$K* z(9EHx&Ac{q<$j;8)xl@0HcQ8wFvrOPyGT8_SNE*Dlp9R)bfDFmkV*{34U5g!dJ?V! zHup#1BlBYB@)m9&GO%R4qLZG~#?$6+!CF1(H6`=rjvS;Z$w4Xb)w~hX_(?;G)0f z4)uHG!|*$vxXmFsB)D6=eDc}+o!NuNchzL1f}ihx%PX@D-(i*K z3E**uTcb&3e-n+4K90$z-()9s=brGKB0(fr9m?&b*a@uHy)Lt`Jo}}-U5!iMiPKEr)fuaVDF*dZ)x*7i%`Y1EAF*2uhYWjT_vl?sQ6RCJ_s zmIT}5hsb($YaQNh!D^s1!mbPhlAfA?UGaZVTslO>@%RE-y$yG>UMJVLXm@0xUC65u z(EtrzRU$iFY+sb5XWJ6)5!jG{D+#e8!AyeJG=NV=|x5=2s`{phhmQ@g0#IetWwR36Olj=8LzmIM zZT`DGMx)(@>OPQ%XDiy;UdBO!L*=m3E-z3rEz-XAOiWo&N;H?I(;3|837HSwC0A}O zrS#x^@&+5No;wV(mZrP?A9UkfJnG+i3Nq{_ z%@}rB4b28{nN2vJ<8Kuy8^hfx2sX?ZEsg?alqb==?CuZ z7~HqF6I9RME=N`Nb_o>bB<0HW?CtPDGaO3Ys&%M3WtHv9BL*O+&MPrn5-|W7NqH-o z@mJkS`q;j$BvTF9N(!=nD=Bh;gr#kI`!+qvcV{77UciCq@hKr;5mCu0h}#?#<0jxD z`#4!SYP&OOb^G2FY9_$JU+?aW3BZTVw_7gPA~y$K-kmvLsP4`jd~J88aI;Tvc^A{4 zy6dtex&)9li9_TPly_$y(V*R#M>ueI=91E@J!K;f71z7H8@xO7h|3sT+}gV8HH7K2$#*9>?7o`RMs^ z@srX@;Y3`PC$lSx8xOms^8FO4PGP9MJ(W)@RW}T+kCV%gfZOZlD@efXsoZgb8g)3M zhHkzT-Lv`Hz#IS!<1d&S1+KZ0_T;aCznDPLeJ*A|!5X08Tgg&eB!TOKbGiQ%PPV!m z!uK1H9QV@h!>SQX-Z|biD5Ol8CQ@6Y2AJmMjRx=A`06sS3BIoHH=(r(C98^9fmoN1 z*87V#zsrvav|oWCj$hC_HCnV$NmV|sg6hr6OKXsru?E>I*H+#1sx6sMiy8TKG79 z^(16_P%hNX`!FDo+2GUYs}q67xKWjTyFQ`dkoh0d3!_U(bj1h9=3}N(SU7wNy|1F# z03qv!;`QjW`Gn?Ec=0(gX+DMOqJAnYnm#`EJP82#V77}>iNJG&9LmItfH?% zs~mP93LmYyg6ME~1rr8On|um=;)3Q<{vNXBTZ0x;nNhso zCoLw7Nyf%p)D4YFI-~Nx0bwS-5LQlP*21yfq*!x4?l-RjtgZ^mGFNxs+Nq*-07-%B zqK+-oJIOstsyF^P*DWX>9@hHZ(-8`4l!ZfmDlGw_R8Z_?vbDU?tnjmN;F$T8?+v3B zC>Ql+bYdk9aK5tt$tZ1cpz!K$pGgRE)w8QSPPM8i|?vjj8XpSnQiMHKp-C z@TbGL?k!KrZxpr=!a*K?r^p5DM0*EBmB8oA|1LyKy5Duf@jksil4U=d)BB`4*Da|J zReuYh4f%@{_?N%q%%>SP5 zQ}gfna2@^hM8OB^pvK6k4^$@LzSeYQvN2k(`bCk6B!c3%t!B_oee-FQANEDfJ;mbE znr2WW4i>{30;r`3&6hqmr(=rF~gvi||KaabN^752Y+fAf+F za=3;-txSj1|CG+QZstcZ!e12+#U4yF3n z3*{!puY`Lf)(7lH38Pd4Uha|9wvn;t8Idn2HF?;Rz)tnLih7+$0Ty7Tsj;J57my}Y zasP|}PjCpuInHrmvOtyu1THw>EId*1L7%$N zOOw3m)2SDmel;3cdjjV-Q5culgioeh?)1y(H}{OF&XbhV|BMlgy-hmYH%|}b>fugR z>Xe9%BYgJ0D2#ea5zG->Gs23^pse>NA^Fg6NG+z{kWZvzkP;=Qjy|Q10jGu5*BFfi zGPtt3s8wSme!CDL$PYBL06`oHIvCyTcS<6Yq=9?@<+=#E8iTHy>%=ZrEpUo`WxBb% zy|FQy`)4w`cL1uOaf_U|s_k;={Sj6BV`zq-)2heSrH+X|{)cs)?2`HfcBF!IDw|@orzc5t4ZB;=)4Z*ue)bp>iyR)ZHcnvxJ}%Qa;+}n1oAm zN|+@Q%n4}t?xhQUAq8_b08}}G==rTV)XQ1H|8gmx50nscyg6SRO(!2EvP-K_cn{cv zI~u;J#0M*Iyunn#a|>W3H=&eBr9aOEye(qXD}|SZD)l&`ivzve7{O6{se6IT*Vo=8 zqCWfy`5w$?GrL{MJ?%n!7MNp56bgvQRB>CUvVQ^SpUb**FWL_*R`TVdCG8$?xt}8; z1U7^7)R)yxrb3B8@hY`no=uo?V)ms26(i>uTC7Pt;VNpF+ifxtp$GA@Ubzm;du%sS zWzIG!?)Q>WP07tp!zCrt5Lit~dpb#TCsJB3-u=>P+ee&0)yg<0xIX2O<>LJUeH*LA zd2V8X#o{ni^sE(9&wKDvA@#h=D}^w+z(R4786C7vJfC0}SSFqm!bMjJ8M@mIcq@|P zM8WruLP2J;+F)V6QX!Nx{OO_V%+ms2XqkBr6P2b%->M-j4qapp^4EB)b?v4*nT`vl ze;vB$A$yjg4{)N2t}FZ>rZgBl76hkjxU3fM@IU9bVRy`K&-R%l85eMt0nT9f-uSzr zvSSD z4)xS80zBjy;ofPDec5V>j;Dc!13r*+=?L8A>scVml7nt-&%su0HgF{MXwvwl>ySU+ zL}QG_A6C?Z;2}Y}=4cfZMnvbU+A%|LXXD|O2jDHcop{LtA#r3A%ln2;VPG9f<(K2M z_A_^(YDZC#akw!vL6+=V2p)$Uq2LivjXtWnhFb~0r_XfYvN7O#p@pMU8ieE{A)f<1 z5C2Q}Ivp~GlU#~Yf{pXBI1dB1+#=2}MD>3g<(Fnn6h(~!>72F)8&_l)HLOF0H|9!t zE$;^cjW-mpzFmb9j>TNw7phIbwr~p$2wbf_1eCX6FN9B96a|6vvvXM4UkdgVkfjGd zU=vC)8Z_yScTtI!B3I#*ud|ykTL4r(I>8EMTOb!`UQlkFL2+ZjBB1ry*Hv5t(*-jI z^Nshi`F-F2*`Ea&UGP88O24nuO13%aRY5dGDIaEk(o2nxP8O+aiu_`iPzw;d4A+Pgv3t_S1uKoKHAcd`1@CvMJE6wrGFPnF$>)^ zmv)0aaNFI_?Xt)3B2J>8P0Vjzn?hmpot5?G^2CqXok48)UO#+4rh)~y|8Y8{*^!04 zex`IzuqK}aFRhRkx{aS7ts`*3h*27ZXozQcrmM1n_E;}sfKI{E~H%v z8;ROyvLVvEJh$UdN&Dc@y1xfa6Cc{e1Wn7lVizms#Cp(S)8I#T4@Cyc6+=XSQ*Yb- zz6i%A*3$Z419yCE7yL9cs{Zw$EKL!IRHcU~&TFHGRUH!kJ3g|@l31O@GcKniNz-BF z(5dM*eUy6}z?jVD;zT#l4JPX-d0#H}gtSTJ7V4jWiJ{;;=IK@qY2&5BGahZ?2h z@^;qwV6EU1h!l>jlY>Xmep1Cxf(^q z8x_NveyQv{$Ru*$m2Zf-Pl!BEnC_jaTjS5zqTB6Uukn z7c;E;v|8KHwdn+|n}+$|+fYC~(&UdB44nI25Wu7O3eBAvuKO+!JSuprq+9*H&cspJ zbnmH}!!ad9jw+ zw11cEY#oRa}TNp*GK5U?cPci?^THG6BfPMsSTvdpvJOjxv5pD7}P+mM#bmz zYr%!@4}FTtYowEw><-kdks@A^0^^qcY5*N2YQ}iT5hJRuS`a3Q>e!=S)u&q`Syl?(sz(zZFP~nn=m>XiieTz2%UF{Yh^g!PZVaM z3)_qNJ!_p@Y-kq-ZoiiN0JAF80FY%tc8>=9+WP)NOhO_-C(|fZrVP76VUYo#2 z-k5BSSLdTQwe}TgUEw;6eJ=AbPKxE_SJ~Z%LnKw!uW!FbUb-6XULcgsn(iezcX_l0 z*A2$a%7g2;|IIGb3TxlcHpz2Zf4*(?CC|gu0@uU~M`Q!Y4iFg}=mi?~=;D_~yYCJV zbP?b!f!TZqpM#!4#(=-gx5UT7+qcxMyBq7X5#|pK;7p3X-`ZYZzcZfCw%5i>;Mq1w zyv}|}-dTxEQq6ZrrRd}oSt__bi!}!ae+4dvAf7ic^PMb4Wm|^(NsJ6a8^jOX5lN}^ z3j;)^my>8P(eIPX;$RDH85_%{UhO3s9N11ah=uE&9#3M?;8_x_K@o&j9RKzj)}r~j zCqL`6zV-V){(bOzQ@H%2gUWd>14N<45R`N$m%q>L5ts{dD?%Rr);VrFg_$0&%-#fx zI9k0sUx(xqQg9cGnI6|C_xZ?|$1_hovrQuhK~y^W-D!y#L;<$Id?c!%lG^+Li5|NK zF`@|=?S*IZQ}N`xIP~M5C)tj`L)D#ODRBLb?M%($JAp2kwgTK*(F*}ST%r)u;V;}7 zt=-t(k{oDP5sm^hQ-rcW{&oAj7G=WThq2Z%b5z2>hIil`QK_vMVAEk@#A&7c3=psQ ziMY)|{#+rQBgZWhQ*FMSWoN)MNbU!oVSMaCZWvsuL%ep8^524ka*70+T%C$^yvebOjfANDk> zn0OC;Xn8^`#7HT~x@t}vclhP4_=hZi5OzJWQtElB(VN_;9Nkq4p0KmBvHl6n((K)>|E zqB%Z$TCWXJKIC{vW&k_59*)plQ69YV7E~&Mo#O1_!_hQm(Odc9+K=A?ozL%dTG%mM zQB#HXxjRU}ZqaAk7$v3+9IsQ9mNr*?D66S}==0zZ0Vv#b3`u3i87V%14e3aI4TPjYre9!YG|(3hksJaM?S- zc;uiDdXiV>T-<)Mg5?nsSKU{a;{pxCa%l|`~7h%8xud6O9?1H3GMYX(Vaz@+H+$J!goV{Tu9*7AFG$&$y*;3>&wc?Z-M zPicKh^Ta{3D_^{UcRjLaMdAlXyZUr!k6&qN!1bZ!cQ2h`(nUKX%XTDFXO^7QReZ82h_-7^Y} zw+vZ%bXHmT(+Urjw!==MLX;TP^kCPvEziuu$(yOVVFB_C@0ACem!>;!j$w|%GKiII zD7NkLEAQl99l^C}*f7p#_rWx~NO|b^wb?4%o1ATLxFC7O^ww-ObU(-gwi_-*Vt*@r z7c*V%I|CN#yV&mmeP?7(-x;)6-@~p*oIY1XYWLVumxZJ|hq8)9TaJ?St)NvYtr1L3 z$}wy-OctuezG7d*G)EM#aNp5xB}WQB-)=s^;aA>beK9%>TjDhEpS~3T^jDINSoW@p zR!mz&1$6f|U;^kO5>$K0%nrfiy6R}t3n&k~H_An-*krnzlhn=#>wK~r$DtYRCdFYE z>7;B7!5c)~v^y=^{Lu&_dz*ySBal_(r`qL2C9+3>DY{E4>FeTo`c|kivTC|s;*KZ^ zs^foyc8;mu_`q|>j4w~0fHu&oF+PSG_0`2E*oR!OuJ`~*w3DqQZ*N?UI>IgVS)zRc zE1K(L3bmshJ6(Cl;?p&wSf~*Zn7|jZsRAIL`7X$qh*_@&>AP_xmka7Tt-gA zGP(T_-l<$~;8e#DD*Bt+H`FK5YKS-t>kU*fn9W1ZWgOeT(0-*-sH1Ht(z})xtq#0F z`|Um#o+g5%S~w?7Jkk6t{F~d6@^g}ocQ0*htnaX_E~T}^EcOIpF`&>3JrWLnZ%m}N zgUZjgcy4L7v4flEW)5#DZ8Gap*~KL|O3`}mV_emz2Xvn*ANyO2- zyIB(*l+;|AW6CE^Na7T3tS-=9P;sW04lgAY>FglEp$7&CXppL7Z*W7#-oTS-@=IPPqnASIL`#ajzD!v8P^gpL- zXh!9ub!UNSIJn@o3$cX(__ys#dZ?rtZdW}2J9;LobH>Ld&b>c~{v_tISy`W}llbCQ zczF`;*~}OC%*)rH+Unb}=YhW0o)nf2#Awg;g2307n{GT?P0m zR%#2%E2&(VB6#nb&P^{MRB1h*eeO8RRo5JgCG=MPFo6|MxKo?@RPKl5DmbdE`@7u4 zR&{nk&xK1wT~iUJzDxHmxJ)!!#~bv^>$9!JT!y235vf-NP9slHn+@!>8s4%7K*~F1 zm@ni$b4rVKatlV~e{6qJw$V`4`VJ)HhpFjG=KvyXt6dNGBOK zP}J^EF&Ee_0Q^&as=E%$`nOXl!G&D}FVUQrNl= zFYw0ib-NKV> zq#4pt^_aQ?;g4JN;A|K2o2XVmnqHbP-Z|a1B*X z{g$2+m56)fV93BS(aVHBg?h4j0$u5nf`<%ZQfS!lGhk*@oJ(dk92&!Ic2smY8ecKF zF;-m1aeWU=U7mxj9~=L%i0teNED}xs_GBHZfU4bd#~IQq=mMI-U7OyYxmdW;wu!}J zK(E=ka#3*I3L~nI?EES=d1Cp^(T4f<(rcG)T=B>HxLyb`c$b6c#0QP$BWRJH>$!BN zJ9IUBCK(OM46K?R3Eg3?B1FsrOD-oDLOx{DdUby9XVVQQ-LRBHqhj#CwS8bWN#lRn zzWmORHx92LkWE{Fgd*Z!HoFH&O}8c+%{pvOwFB`}qus-((u~8L5c4*yMNxp4ImR&` z!15DNKzS9~bw3K%NJpzxww@wH=r1oNmZ5i|JCQ8e_E#D4@P*fATh7zGmOvuGfuJIZ zXUXwMch`4j*CA{EGCP0K^Zd4UL;aO=oRDO3cV_IB$4O)fjt7Nm*ie!aqQbPur&*tX zucg`S!K7J++ieeExG{ewIghbKlFoAd@&YsxskD8Fx)2-Z!TR=gGg0SXX@6gH*u{CV z4WSk`km>0QL;NL6Q0bdQ)hv3CeDbuj4fXKZC8+QTF@|tNtE(bjFty)vM_l*+fh=1~ z<5)o6Qg9D!t6uhCB+3V{*;T*CHewFim?E|?iZPuI#ZG!QV2Qw4!mK?@g7T@713nZB!?=mJ_-u8c65B;82JqA>u^ zC@IlxX{6X=7;<-%BubcEytQpep)oTcZZlH&(7<|zS_*DONx02wssuNT5*;=}pdiLW zh>VA>TOqq2I>-3PPL!}ejBr^T>c)eG2Df|@SMefiW9#i50Wy>|+~uFm;ck%GF(*<7 zSn5{0Tt9^{I-xEw3H*iiG%aQuGsD3885E!J>c=eX5Wrv$gYP;lZ#5h7ix#wR31hzN zWq`Q5p>x5p4^cH4-Pj}>iSKN`K5?M`a{FWIK+6_#d24%hf)N0}iS~1G#raFRi!W+kx+^BNoC^vn4zPI@61B+v3b+frn(3gknpmFkz9S^f%tHJH`j%}!EE=tH z=P7_(IlCXha_(|UC1V3#*eXQp;Y~eX^7yoYL?q#oEJPyKDqPV!2&69qMxNqvm!X2& z=GJ_)!Lfgz2MUKIc3}XnVj1X;Sx1xnHjki5wrb?V=ysW_Lx}gM_}D5D7g={?=YNl4 zI;>6Q4|Hq@_C?;KZPd`FTRN*`Qa&<~ysO`#ez#O1s)+tL-Zua(@7E=|RyTY;;N=7? z2qQ1K(5;>R`2ckd7tn5AbBD1n3-s_AEgrz&x%L#?!V{b6j>$z>Yd_pH8-lsCK6%u< zF>a=>&B}`%RDasC_NROA)2hcgK;NB?>2zZUPt5U*7_MI?Wq&QfaUgho0(Df^n>#aj z4hN5Nef2&GLA?1^XNtNVJ%Jg3!=Oh(=bGwjv!5f>YOMX~+nHUKYJmOQ&#nh>0wWoX zL^KL)zQ3#exVRE}-3>@g2wb?!>3*U;XA`B9khB~Uimr9;3CE^jDdlbY>y!D`HoUu< zWdK{GY)aogbL>G3VveB$nA7dfg6uWBdY(0eAr0;gU>rfCtN;EIJ%@o$=Nk z4QC=SSzxQyJlrskdqp}p6Cu-qfqprik5encxcUFK{mtaM$$!`WZ^^%Zz5N|Kh1z@Q zlE0`yphIYQJ`*203OgD0y6|ixwXeJFx9T~?F-nN$IdOM7ffGmbJe~!f;z@mNRdx_> z%;pd7!#+vvs|8Dd?b>She!YcBp70GU9R^-vwaH`reXL3NaPdUh7Q&KjuqE3Pw!jFG!61-9=ED}YBx70NUwmX_+ zwAlal;>w1{D~h~l)D&2ZW%KFOxq?aqhEQVE1#+nqDv2eAO8x;A4Bs3tR4_4;OcyX8*dftR(&dv~RK0zG z)cnoW#T*wkQr!irT?(&%r||;)H&6c+Gm@uMG!;pB8}B1W3Z3ZolVQ9+OrIiq|1IWy zJqKrg3sJDoja?y6VrGw~XBdr3;`B*LZuvfu@)mqq;86iubr&t7_BQ{Nerw zI7yJ1G~wl8;nuQP-lDZI6T&|waJ=~XWWv^hNPUvtYPcz-eRr^w0PPH5+G^&nue;3yYH^<91CD%ur zl2gffTf;hKe?zpNvLu$3){Eq>6hcaqTSfuBq&PBJw5bY~IZPS|7KPcts>scq&0BJR zW#7&^h#FihOK2QrIWjJ`_2*qT0wFiR7m9NMn@V6(hvX3&g7ZiFbeW<&o-ft-ujRz&8&#V|a91e{*`u zZk1^&4zqd9C6VSfNbV6&5EobWk?GerB78TG1`eK}$*&Gvch3WoD9EE4A_YbEN#Wm| z{+PP=fmDP>T`GbZC>4ti4j6$IP02GUEP8?19lsvInP*TPjKxU%X4mdx4ji`~e^xa7 zxaI;Di5-5dPfNaE=kHR5vREM5+3Kg>b8dr2~>f%&Br(=#qw||5V?)!Vmryj zo%06V78^`5-7}BdaGA??n;6UogCge`z#A+{kwZq;P#tjbh*R0y>cz1a&1AWD*eUsA zI7H)tQ-|utWXUku>3RBk`z!kOCV#p}fBISbpgRpJevmQuPXX1*$kX^SC*HKjgU7%q z$)*N5&ask;o}Q-Mbsp{A5yH@mT)Zj|8K)+)^$b|&y4U4Z9t*Iv*Se{yLWX|EUUakzhV`kgr-q9K6~ahneB6Z@i#875O!6k8gna>q5|n(QJk;~Jxx zuGLIl8$YV|-BcH~&QHL#w5)P1kQSeENkLnh<|z7BA?x^FhNzHR!)EH*{);@k$G zaR)PJO}xA*7F-)Uoy$}PPR*AdHJeWxkuwvSFNwBzO&OF z!S+u_V2|5Yh$%Eiu+dt~r-(P21(cEaE^$x^8)UQEIjCvVYDmkP4(b7Xh0V)C1o0=5Jr$A1f`r5Cp1$+k z>KWEMA8IXOx8Z&3^beUS)Cx0zHO{#`NVCS@`H*^jfJ!rF);dv-C*yp!=23bO_RfdV zWjhbl4}f<*)Wn&T=q_sGk!o?@`H-?JLe|vXf9FGa;5Wp;{UlerE(th{1&DG$LTd0o z1y~oEM|F!Vp?XD@fc+v%$ZnA(L`7tDh}NfI&j_ZXU37vDDt3mkhFhL4#naC;?Grs( zm?53=q*zqH{!y@Nfb!AiQPI5P{BuQy9KLZv%vRSAoX^KhHOo3M9>?YK3>VWnMH@zz zVlRl~7=x56rFyIj6*9O(*n5>eyi@d@Mn$*whBPEw zRv#2C_v(nqnIyoT+mHQ-uj2RXAJSLWXqE^`^pY+cPF%{eZ^a1+1B67>`2cH#Copi9 zDwOdx)JlkMQm2D#SmjoA4Uc2vWAe!>$76T_r-8)~$r>m}6C*hVxiN9LKwj)=zH`RY zuXM}2?7&u#$K%bRO?*n)kV25&z8^wQT^eXA`)%0h#_nM#186;g5^o~#a0-XuIotWdJuR4j7l-Z7O@GE`noUt_g1L-J)1TO%G#8U5gNWF?9;`kX&@ zQtG3zlfN8M#!Vs`66ev-TDyCv<&MoUw2k=*7Rm za{w$&fF$o(Vnmxg`ef~&Df%9Nl4<%l+O_4K)E{nlg)@@Ndy_fpv;o)jQN4tv*rb~( z%I3I(w>EYrpDPb{eJl1vc<%N%Zy!ErZOyWKndUKD8BdYj%RG~nq4DSK?eX>lya(Mp zt@0E(0_!f?$7^Hjd#1mjUq~{)%?!Zfw07c-)e>(HUsqh0cRH#8b#7kr&oFb%u zjnN!|ND!=ULd8R9F7J=?me4Ye$w)epv^ARE*6Cfi#|czhmGlVa(8Z>a>mF+-j;QSS zV(QwwYYdZax}nI_=41oaKgcvazMAonmmN0a?KEk!|Z7iim? z+q)<>DVfZ}+Uv&#cGDRirEETnn>!yJ=|$@jiT^@};i&Y>9m{vTft1`3qxUTmMyUBj zY|nrOq3g0#7uP^k0cL-UlSyW@i?mANvs-)~B+oXQiakrnI?6ls__u{gs3 z^~$Npg(_AAVd*+aeM0eCO@xH%RWgz8?KvL#?J&_+cE{8#?D~~7QwAHRjV87Mde*?E zL5BR8hzIK$#R%cXAPBy3c$+F|Zp3zwQ((N3Ie#FppK#0KgXX4&4R6k#S)pFO_)6{@ zcRI-ND3FJu3h|Kms#FrE=}e4e4>Dn62$NHt((DHKzA=`+n!9x-Zp499YZPPH7xYTy z4*y%Cij*WK%OS!N9s^Ipz|u248X5_e^cjl2RS{?9Bi^f9Na@5^9^jxmO^ZSP!1SBV z*E+|LlJywKpWYHMdvCOuP(3~ae&_V}8^zNMr0<&=>b%@eVyySlWPkSb^d=M|b#|dQ z-gEJ2^+yKLMI3p5Jdm!Qso+x$U!C)XX10d{cv^&1L>xSe>fWLl|9DoTqYh(TjIwnO z8LmV2i&0&dG+c~R_q#k0nW;ZA{kxW1q=!kVQEhB`(0?5TW%jvNWGKnw!ZT;ggaCDj z7*K>6Gn*s2m{0D7d!gKO6Jycn#|GRaM8FhcsW^ll&*AD;8ll=gae%jhz=f?VY7&#yE=ow3lIwrL(w> z`E0}_nw+Fz*3^=YDibd5pMphMYh%pE2tzrSq2iEK_}9D&L)>$192}6*CXPWvJZA$^ zujLILF9u4^C^S3fpMq2G++NFvMDL9AZ39nFQUFmhE2cwHMs_)7T#6tDQH+j2d)2`a zDX(y$1SlG*t0L};CWexN<6mj=(~5}^;%bqM^W4Qq3c(~r3VA&NK-QxW@(e-JgoX%; z%@p$)qUfpRLDTs$0Fymy@57WdU9&lQqMJrtTW0#uVgp)L04?_2Xi{17DesU3SQ(Uf zQr$LLe(zO1J%sVN3UBJakt$XPt*4-btBF$+Qj+e5NRU#ATySo;Aou;@}5)LXsNV$}*$W znCd{;^@N{KZ?9;*oZBdh?d$jZfofD!MA1VW53P}VP>!!pDC$VnhnY%64rbdv|2vt9 z)ygh^m+feh;s$oV$p#SPawr7zxpugR8aXxfwi3HicNrH1tM*MF~zuQdfsa1rdx8K4Guxn+tb`Lr(pPJX`X_=6U zz5oHWMH(FLWAb4N9(9}`wC4?^C#s*&Tvs<`u4TRTNc||zOs2CM*;zbP6YB-aNr@r9 zYUmsc@ri@{k>g3~_ijA37cXAHo~~{Xt_^L#{#47O^;8S*1szhgp6;Qk zSPNh*)>Qa+V=>uvSp&IO5w9)ixuf zlOLb{ysD)9_n)7BUs3Ap6sh%Bsai8+`c3qwPucOS_EXVOVPrWSl{Q0kRFFe-6p>`w zIR`19)`@XSGC;9}zH3ucEPW1WEop(sKhF=|D;IjpOgiHBsWv|y<{#|GOAE&)36=#H zE-)*spY*8Bra`(@0PcsJIn_Ggt#=Q$SO|kx zN4uj9gtJ09*_~;I@@HMV`0btCLbrPuvFg-Cqbfrt&7#Tu{mHDESmH-)(&Scman=pH zh<0;=$Mf8pHdDn-|xkb2djA*S+zWFPXtf4Pu_{(z-NYgO1P6B8%LM#Lko)9!9=V z-qyD;A-Wd4&BY91G0Ng_@EB=I=1feejaDql0So&T(>GbVW@^IX26i?#VAnG_qHyOv z?iUyQgrh)|Qv_47k*4MyRx;+BzLwP#k)OtOtJ;PwCAYzf(ssL!>M8k?(@!{Ex^#rRLM_B=y#&eGrSK6hrD$w#7{6D#p49! z?(Omd5I7>ty>{ESvuj+{PoQk5;_*uTN4CK^-MX#Mm-(B|QLCG=v+{{ZbzCXd@o?f2 zeEpE6(5^EXp%{Uh@Ak5IiBN+%>z#mJSih*5`j7|8m{POM86fg z0eY6p0NEcKuZ;Mi>qQ*~`*hd5bUfNVQ@VSngGvcW@{_uwS*3G2O+u0aPE7EX6-u4F zf?)%Zld0M))197y<2-(oVm}Dw`4kE-h>Ho()&|)#JorokbMKv6$UA(49!4DG;zT{#|m^MmGDEZ`Jiskr7}Z!ffxq{ z-90~xkF243cOF=#)Vn+`B;sKVLbbmPgS3j*=jPk@=6H&G z=TUlAB1<*DxN_2AZ0&gy|vWG zHonzpyI4jz+F~i;1a10%|UT$`Nr;Q)x)VB0T%81Wo<$!+Eta@qH+DyE%&KPkhcXVpx8ih+Ud2! z`DAx>vgw{F^7D0t`ibW%CVI8mAmxLCow5L+?}ih6eK)C9+=+atcdW>`2`A! zZo~xyibhH7F}}?geWB>{A90gUvAq0He3NU3(Bf7#R4+Ns3mz~O&U5zMs{@m1NzkP6 z_VDwv(-$&Gy~P7Uh=NKG)dCPqHf)lmlA^c`}*f(qU)Mb&BP)PKFeTyT3nkQjCXOY=t>28oTYrPE=C`~smdztQF3`3 z)MUl z^3E=z)_C(HdtjbD6Vdu&^5{{Qh^*uE0!qx^$C*Fm<2lh#b_O;XRHxb5nbW_!^e4~A zDQ-()Gz=e+kBZjG`mD>~7_x;{9r;}7>TiP4cJiP zk=B7o?L(yDg(xk{tqUoHXpTg46@^%r?3R`grUxPp(V9^~TBY?0)$--Rio6yiuU|2F z=_i$2c_Dd)VbT_(oxO+i#dZiw_gbqaG-XpH(9NCA@rNNf9h5}#IdQ3DP&a8%KV-o* zor#>%(b4P`YIZ@JiFk@Io&4!vB-AA%uIvvm9Lk28jc|l#WzXTXPQ)G%h|j0`dFJd* zA1dX|mQKFtAaSH!x~@vLAR-snkByfUFb7G*%Vin}li&c9lOm@6?>YVj0 z>#d)h{(w((+WL#rAGSL%+S`EDtJkU-S@X1%tLiEj(e$tKgd4WXDYjf+cXao{5M6sM zotS%2EPM)}uVQzZ%^%67KiZq!LlWLv(S4Sl&5|zTWJuiQON41JYSYC~`LD!{x-9`+ z)@k-qb4{WS6dw5VEkO11v|-Y~7fy2bz5g~kDT$|6|-ySXU z!K9y3(oFdTiD^6~zsEn)i0jTjnGZw6M2O4Y{jJ2rglH(u*J=GwiQBrtA^kQ4Z;$SP z-;<(Y88GIC1c%Ix_{)wKvfEwRIocFSeuWNwfSV>{C*An>BozV;D3Ss zbj997rWGMOH3jBcx#*r7(%WKF5A`+^)IE0Z&B?=uA(WnTTN|UE1HR?r1OeYYS(6YQ zOP=Q0e{Hsp}-O*%J{4=R;&Fdh2(cfbNF9L9X2j3j1zB}e} ze;oH}-D~q=r9C-l)4l@g8_^b<8}X2;osbNBoh5QehJFuSjIps;NYB!j6MQComZ4hb zzOT&T$40z5bWHM`xJP%hMQIgzM`?4IX{J&#=cHN@z`ij4+Trbuz zhVy|{@8--Xx9`QYSc!C}5Vh^t0vcfxK1nG|Hqjr}misgVT4~pa*Ri&+MX(?&)?7R; zR^zHg<5E7}a>RtLYW#IMNA{&MYOCRzo|x?=G%7?|HGG72#-zveJcpFJ#o=)3NKcit z*rG{jhj2%c&aet#mGjeZJo+8SGvuXjkKGW%-k*UAm@dT#NuRCkj*>X*ziUH6sF#g6 zc!~)PDzkdd z0jiVAlyZ=&?MX7A#ylrK_bn2=d}|sW&b6lh=15D3dPR?O8$Jc9PucfM*iI97n_@;g zUYKDUeGIQAY0XAfNLAKayrHpzunfY!^>~=j%N>%4eBAaNlGmJMd>K2?T}RCUaQk^kRwueX6*@HceK>62R-OxHiF*tpWJX^-EO+b>sS{n(b1kOfH|&s< zz63VZ+gDkn1q70@EVinr9pj_Zsq_M#h2I$OxL3gb@boX)+b?>PntI#AEkb&_))(FJ zP`&DFP`Vxt%9TDi2B>8rrlDFw!(&wgj7 zG&t(6$2Cc;Q}4rJ?3PrusELbR5TwW6DFJ#(SyMb@N$CeGU;x;LMV z@#(vmyBae8_VlN1A-!No6{s}Gt3y|DZX;XkcTNAgP0VFY{grS#UZ!uysB&|AMFgYR z;o5*1-aPT~titS_Gtw{2<261`8fq}}BM&wh=g_-(0}HO1%S$)REyq{S5NZUSo@&=JT$ zm47~j)`(M!Y6t{~TNLV~?yDEC)G`MP@#q+^5LY=H`suoa5@$AX@;v`+4OkUxYCo1AS;~*NYaX^ z93k}kGeaaYyH1hWFD`B<{7(AQPudk1b`wJ>#2xeMOjEGwmPGD)bi}z5q`Nzt^eGM9 zi;1z%Sqdx$N&-a^S&TW64)A&U!}&RhrAzIM2h%hD9zpZHLM@4N^Dd69JBducj>d3Bugl1=zMlScwfNIF z(4StnOEqdMZ;xgaB!pGd2UCmO(9G`WNE_Tecrcsn(RW~UYuYzZySX-^>OoGR@l0&} zjp;Ahv4J*S-=a$BH+QheMF1i+-wCczY|K!*IL|d{Qg0XG8+bD z#8|Zs#uC#n!&+kcrNMwXM5I@$NGLaJ8@K=%t}3hhFr1Coyz9xqPkJslnTS>-H0ze= zTGq-?Wy;#@aAt4g@lJc%_K7vII9rt0a=X=f+OT01iRD*dzfglDZ(x<`x{n@>c_*Ta z#PI}CLU7eCQo^w+)|X$zb)M}KLkgL9}3H3ui(c;@GRWy&Cy{@opav@ z*n3cA*Rklm*WQnhPVk)KMHPB_diY{e7g3bITMhC)j-Dv;<3-uQoqsorT~%2Z-Mnq( zUc?wRUN`0pYhDuFG@i^^VE;DM<9IR|AKK^`&n|4aQ^dejEtH)W+9$$o;#nvMO2Wts zrO>`S>4C+ec-JuKnfp7V1)gDf?7f*b#gqbu+?e7KYrlH6R-{?N4i(80K0eYoQgWnj zjK`Y~P*^n-fEEq?)i94*StV44kQa^h4Yg~UnK#5BDJ!9#g{-8^vVJr3iR6;3aCBt6 zsg45O6!$jJy7{BiKW>k6;3Qc5nbi0?J!I&^;vU`VDxsI^iW?oSJbJVkKPrMOVOLG` zkglw*I+v&K>B(F=2E!N@1=2lFuO?x6J zk6HEut#sOwPdZmcl9H-#Q{o-2dhxDg27H>%}0nKNVjl+C8(Mx}P zq%Q^+nKzlA(UHnxZRPf@surcZFv$qMHQ9L|=kPwA#}n1evt;^q#~YMohuP8k^`_sx zksiXNC+lvk$SF~Xh4n$8fAScc)$J-!tH1QqLq1vgwr)o=@k%c+ud)j8|fD~V-Z@8iX`oOtYAqY(7S zz3S%YTo2Q@3F~$^+bWA*9Zk5kx7n-P=&}=9>6_GZyl^?7VdoBvQ_4QY7ZEI{_?r8- zHhDxJyM6qql-XV`C*>X!tLch^)*qCZ>J|4XF{YPbJ=3eG9`;U&UsGT1h?wy8ER+vU zwnmbL!l`{Zp}kYSC$^%}45c)r6P@N;aFe9bc(duOdS$1g?1F=0QX`BwKdtAHbn#s+ zisr?9j!zei$9@OhTC#_L_xSGFvC=Xqtgac z8bQx;BkA;-QU>+eQBvq(MwIku$<^}7Y7@$LNT0gl+NOFpsYi2G*gE4VR6fSSb|gTo z%bG9BC!w~Gv-9uunLj$x61K;b+TH-)!WTbzyqWddf9`v-oO6-jHqV35HmEK%=Z7Sj zVx?EscuvM+P#47vBg;Vygd)$WAv+s%miQ1w2I<9w5X>2FjSm^3-nk-}3EMR~>GGr+ zEbg0&M~4=nlPt$F!zOVTob{T7ifT%T{m`}JbPdlx7v}Wh;Ef~ZU#6^+8CFWcsN+XW z#D4Bp{z$<^LkddwsvAeU3DwB}Kej=Lc<}s^hc``~gm_XtGA1Z6XU1X}FG3P>;SF|n z&LtsQ@R&4bit7BWwZ4PmHoP9%-e3&osp(zYJT-*s92BqqrtcSbc@jk+vZ;z4hv$G{ zg&F9go=EuauK*&+16_io2`1+96Iwf6qbhX38?M#R61^aeX_VBkVUxlp`YH=?MNL?U zA@D`YlG*Rno3IVay6+wCY?uuTK^Ji>h$~w3tW#kK03KimUP0?rvx={gDu;I$%HFN#e^Kp(=sCs=m(p>@SLAez0iBYY^kB;bDv_ZQANl8Rx0y=&^eG|0;Db3^N z4%5p!TG1VO#Et$4IYdC?BmR(Rw#G+{bZollJ3J>T?l-rQM#aY*_>47Ru97}Fx+K6` z6C~yzZp_BlKSW8&q7b`BtA24dE)pRZsO>b_%{!NeYEU1ill%NkC>W3o-2Mu#dv@v(l1Qb15p<+kd_ zYnY12$$&WRtBjMPvr8)=+6-Kh#0b?d;Y%ph#Y@wZkZb$n-TG2LCCAIAAEC@o2Zo zB6erfk4YYS4~+xL8X|d8`quSe5(^th`NpJ46qwhVGa*`?{!SYaHqzJG3Z_Ef%kpWo ztZk=#Y7JibE|iZeMH=z%o&GbL=rKAXx08nFo}}qUlILrtvkk=L+?BL$=d zS61Q%Rxqx31(7rSiqCT2NlsX_sR<>B!*pS$u0RP@%w5c_rP}eE5qFzAr)QE*ah-g* zOh?Ei`8dOLjF9WdE7`^iB8ROCzrJ(cZ(Q#OJ1cYvH6E0l&)NFvR0ZCbGSQkEH2P24 z828V63g?viY7H^=BzRxapphMR>rW{cdEA?e!btkC98a z_F$f0ru$>lXKZ^o1?J})6G~{~=z}qqv-s2Fp0vp1soutZ;-Tlbasr^M^Ud+v4t6KE zKX{AX2fnGejt6&V zF~zeO?GW|4Aa4=8cQB*0#Rn6Vyzm)2zTflosYgP_b_V9;3Q8zU93*?Bxo9C6_2ThW znCXU~leC5RV6+;zszLwy(@(!9uw&W$@O01TQNFK^Bu;xV>I*JB^c7V$#leo{nhD5EZTFKPH1=cU8c0W`sPdYzK`sLukDJT#U*R#zCU*k!K zNRgxR%A4mUmDAD3el}T$LoW0V+uA;KoyMj1I1>|<_;b^r@regp|NZn2>OM0*(!xWm z8(|;%UVV@9=)f37H*`Z%36$1w2-2AtywLFs1l?p(7no($zc5nll5A|BbMQ1~#zAmBd3pf|eu&AyNuW)cM3V-f^3z$84U zNVTsp8G zi=d?H7NG&^+xk(GfGom$pFwIKZVf-WHGVwKk0xc~I68vL!ZDyHCF*RxoI>!+Ss5^* zhYe(B^m{VYf>#P;b7^#YeYpU|5KCq?A?scuq>|X)-0+YS?(Y| zISmc%a3ucA>0eMsVtizBZy5ZJrvLHub@Mr3hJ~-&qa9>!Y=T1744alW7e=_2@Zg3} zkDRlxh0S!-qGsrArt9Y0&NBxXaGQ#5`)liyA>9_(>Y*^s??#;;h&G_QJ;ZvZyTbMz z+ek?8I;hY=pU6U$yEf9{gq_yVxH*X-N^c?8K@=>$Hr^UNo*<XjVrHR2dy1nwKxPcyfHkn#wb?G`l*cP<r`kV<5I6PhI z8>{Qh`3CaYT@1-%s2-c{l=N(_6rb%_#eZu0XUzc6rQEt>MtQ*Mm4%X_Si1h+KNMnf zfQv^XMjRYZ?Cs-fVuxJ>#xI9X=Zxd;h^!%+?du?V>5&xb8b+fu1Z-0^Y(b~;=D6hP zXn)#srXB@_9*^If%#kCa6W4{@rv;0L*(u4WOSkx7ntg+G!Vyjzp9?UoX^;0T;1dg; zbJOCMWRl`Wa;)F*Z(=B)zMFFsV>n!&qfIf0qoIYf6655Jk@g2)jF#-mjNZc`%1S6= zo(b>J#02HZ^Ef=$GZmb@Sn+J)4q3qF*2EZ=N#N7OXpT<1Ps-X}e&-r#jwSp^OIncQ^xlR@Mab))c~-cH5naN5f;5FaGyNvx5#R#d_ab zVPWq)EoO`3PI+4H9PB@u)6U_PFr>(;D%-kk#W7uG(=b*0!o?pA%EF9kvT|0OvA7ja zS=1_qzseV-$CO@&SBf+{78>CVU?>~5Am;IP%pQ-m^!H*m_F;!LtrsH>P z+ASoxAa2D6zG15v&kKn0WqCor({s^P#>6yufywVTcX~gG`S?1a`Jdg6JS%qOLd1;7 zU{XjW#<2X&(clP44VXL({_>20FCj-6-%9dCC!7&Vix}V8G3O*yg9|dL<3+L8hEU|K zX|7wgU382$N3wOiiP{SXEZ&gl<$L2P&bFL6te#F@k!$2kkB*3XXBqUjQBT_%^h`8f zctX1G1`^)%lq+QVxa0_=Z}q_A%NNJ9qZ5)k17yqhZxbX~sVpJ2a~@W6USHxIgKkGK zq<)nfRa};^l~f46gSSq%@h*+|h9UMj?t$m1X>3CLWjYHV4nMOF=RZwI_sQkCq=HT> z!|pY~ez}-r%7Se8(_sfGBcu1^LtmCI8SdDP4#+8c%Bm%%2g35A<{jJ@$n-{7uP1xg zIAmR~hnuS!g0%0zR}JBM<$2g?s?r`Bs#6I%M>u3bFSHL20O)i#-N=x*ChQFUBG)3z z13tUNwFYz!9iXyvH&N`_b$UfACo}(j z(Sob^eBh_H8ctjaXvI4#UDo zj!YqG4*qHJ4QK+$;q1}T^So`miz0e=+fBk2YQ;S15OeC$_o~P!AI}bd^69bN1eO5gKQ@_jeX?K;(>?x`wA0^COuD(KeL5c!*BJd z6ZdyENUj1538~Tu0+a3TV|hx?Tb7w1$ved(OB=Wk<3g|h$@J4UWttGt1Cr%h2U+00 z4tvRGhzpnG$JQIsQ!RqsySY<`@~4ME@;n!V7Mo}&6L|sRD+&0d$d*U7`AHo%IfW{?YV~C8FYt zj4bUC`VOge+y~INS@(R2SUmG~{?u~yJNo$k>2E1y{GU(P^uJMGE}5AmWz1hpW?ER4 z@exJv2Z(|{O-o!>BtI-xHXAu%Vm`L~l$?Q1J*gP->4(VJi{%_{J0*#5Sl%D?c&^XL zGij?^Bb0;3D>?fkTL2!0ZwsVhCyKqAoc!I^I{==h77$rGz&naK1IIBJof=s8|LOEM znq|VV9ck_mD7^%AeP?qQaYgrhQYI#Q$3@yZe%4k7ITu5TGtOTo%d|(VR0$vvp>RCx zQZzcuTIC7yqdtjZ2ZZM)VZ#2Np-;mqLTfiQcnXPW3f{_WwAkh|N<-y^Iy%xDzVd;r zhnXQTy*0SAd*CK4lM?0rd(&TMvxPNJ+R(><+AtvLIh<}zj|=eTa0cwiyN3l!H0hUu zosJjK6?imtPUBe~RL)2%58F;>pA3>$+u zf>;c+N|TSj3V2LbigeI4-a{A;yKD@T=c))szpIhX<8AAk1Y!yXlGs9x){n6s<qebo6kkH>1_`OPo%#H8y#o*@F3B?F%$7@ckE|K|bJiR$tPAE+zc=2Rph6Z0NUXQK zA&-vE@(9JDQ*Z?*?ALI*S>n}EWQ8nnw1*;Hd-)cQ?^-P{8@wSvDrq;Gtzxg9&GYey zr+A`&9J)gf;!o`D0R99xfIk7&`j}lDpCu7bzF0I+>J-lg=bG220zVRa^nrP(EoQa zzcE$b{0eN0W15BFdT18dm?mF))q%taIk*#Jh|lqEdq^di^L6HarPs#{`xjyH7G=0D z)wONRVQ7tYPcFn=0~Rt2fhYP+`|;J;d~dvdKp$X6<3F4JnAPmDP+A4UDJk-Mo+(e{ zX{z{IA#m{?t_vd!%WS}aAWp$`nnR9q$qDR;NIEo7{stW#y<)rNI&P7n+A0#g*5}*+ zhbp@`V#(QuzTFkxI;}CVbSPTs=tXYe4Sau_m$(h=SHVw8o^)%`bM{==h?8bQ3B25m ze;Y}BbN6&gyBjQ+g17E&&dT;Ilh#bX$b@BaG|Hc)k2#t~R{L-uFKQ2l;=*Hl_;BxT zqi)ExgW3CH)qQSL=4y0g!K}Tl2Xkbmf&<-fGfH@Gl&G~g(-v@Dx?BBT`quv;ixcDH zjlUjQ#g~Fn-&DIw@5bY7U@KrE@gp;|l}TX}XyM`og| zz+1-@HiB^3DJPM3&f(TSnEqz_{I`n-ygUZTHh-?y=k02WB{E^w>;cs6BfnU!p*#)R zzbITRbyvPt~TpN5hL6H9UdwUiJyUyJVVlTG2o4CnWh5mqLpGr1m! zL@O`t`rJ`0XHGw4aIpO1CVZHin`kFKsXR*&n+>H|W^_!Fy;{$57d45D(b(=$<|ZY* zqncw(RDuhHrQbVNRkm}oj!O)`C|l(Nx3hPYTO%tHWX$i#y!uqapv2*bL{5e zzkeL;Y06^l9>}z@>2heU-z$?`LGuY4Oy_Zu^=avMZ50RAdMb9Fi@Q?)T-j8NZ#1c3uImA3c zU4ln}vC(2N*wmz{T{9X0^ay`#;8Q? z3$=Kle&-9rHYAVGLPSG*)+6UNT+7T!gr1%rc&y=3v`+5Dd=B7VfEDib0J_3XGS%Je zaG6VTp2g2YWym*MQI(1A%*;kPZNrO^m@4!poIK8M)PNv=8Z+(u?R_8AUM zE4U$|)VgEt*|9_+%G!mspf`L&dKxFLPlvkpC6^(&S0tvbr-q!+fzmQ@Xvy=NJ|6I7 zTMxsEEcGJfMNjt=vibexyVtF*0qs=mS7np|Jn#G=OK`b4S+tlvwQ`vs(Jo(MQ|pt- z@~KXzbkH-M0va-%f?R$&r5yrq2X?U_+sZsrq~{7!pAD=HO_53HB8yPUc+Y{PTq5PT zq;pC&)gra2DoaUTp`DabdB$_&;62KMk~j;!51H7i4EhZpOwC4~Q@{I-d=ZK*6@w_$ z^{HrFvfAJHOFiH1#w9P|biZ1s&~1D_6*biPA;$Y`{%bw$yL%=-ug`>&pVzQ+g6{~D?lKRT5b*xA*rq|9N2z~PhKVbHeh4JKp)xzCZB==C9Jl4 zQl{q8#*8?*+rNY;$Z=N#zM-BHs|nCAAF2|Q(ATlqZ~H4LejZ-{FwJ&b6GBdic50F) zxEGk134xsJEt|K?6K}go96ssaVg0NRo&=S^T<=YJ?BGf4T4MX9t%u3B39wJ>FB7@ThBOXWj0!LZdg}iY|n0`H?hnujaRj1 z9Xls4#(kVlP{jIC-dAE>{?aqaT%@AC65@{di_pwqjNy~3(#XU>elW2Cymn~R!?}E! zMq0B;aaYV!X_aCir{<-#TbP{~&oeZ@abpHbw=E{fr;l9|7&l?sWs?~A3JEU(L(eL1 z$47pRb}^D3`U|BOPUbnF2N_lrjlh}Z%52s|$6TO>1|Nq?1)HkLzC;f}(3PY0d8vsZ z!w#LqQSmW911qyYO}yFrt*>2aMPJGTdi@PXC-R_#6ggl6NS$O-^(7`(4N77)FX2_D zxrQLzY`Kq!7r0?5$R1;LmI_9cEy9V?$0*unZHQl{N0)Ue>EUToF5E>o9NNMUTWLLp)T-Kp@;g8Q zbb3l;k8){}Zfa9=VC3=$c7DCIXe0YG(}xxclA5LBk+Cr0IwQCe)M%fa7!o@htfBCUP3W=G<8npH#_QO`W71Tst-ZK z0f`rk^zLYq--__`v^af&4rE(`0?V*1E$VEOyP6oO@k|b6!VP2iGFmLbt-P+PPZqRi zjlMnWBcZQfK{PG3m;D7<0BI<-A@i2s0Orls6Xgn)3E7m6KQ-`;u8=1Z3n=b|9Z!=% zjA37_kV1?*4!0wnfrJZJ%2zD27@y|Lt*OL(dj_>IQJ0u<3XK<%zWadON!-lnhzkD~ z(}~#|eOZL8InKm(M>x(zQHcCaJkJ9i(hX*S z&jzz3Y@R(qN+W>XeT0hv{9srJqFx6NLtm-q8Y~;$;QMa*+s_R)YpF+Z`a(3^7%oL8 z^MHBxjiKDX(~=k^$8t_OF-*??2p0d&!-(V;!%y=*=!6r_$te)r?FGqRhaI}H4t{95 zV`~P<-xO|c)zrusUd+?qK`Bl@T4X()Ksx{Qq~-LVgmb!{x=S{qFM^!4#z&9BngEsIq=&W-zV?|sJR)`Ki1qkO7V+0IBYvi^}fy+=Y7199$(H0>Jw z_jW1jO0(t7O(u_P$)Xy{ZNsUGfMM-%57ZkEeGE#!{R~=0T3w*_R)E5A32RZhm7Yg? ziJ*z@m@d0d(4o+oc6-9C;6TwfrG8~yt=lgs6 zi_z}21$Ny}I>#2=Y{sQ!$su3wyf zjV64_GB`ut!H*YzI!k~0$>L9qDx{jnE@egU&JN(S80Wb|O1R`#B`Kq=>l?+xHTC@e zw^8UG4u^wkx^<62a6?Cd21PlT{7j1~dq#?K zH(oS&LH2HT>TlqE`hx|RvZuO2YtAdpRPewqH^Ii7-h{y?h_6}Uu~H8Pb~j-$f?rZr$3!BYZ89-GyLP>rnxY{6oFt`RwpJmI6Kw9;QKu}#Guu@!` z;|~YUu4v+~C!4xvZ)@n2hgQu0_Gow4?g(`^5N2&jC88FYM`g;j7uGVsF&nt6LjA z0>F_C4g<$_Ne*We+0*~8rhnOPwrWU!Fy1OA3~s@(;s~9!+B5W&LQQD?NBv5SsD=2G z;us-jUtY}lv(vw35DImIE>a<(JS$M~KI5;A?JgEEv?AA7cZ-fimpT zJX<5*#ARJeBWUH`!M2Nkd$g5Z9)PugonYnh$$U`e$xqZZRY<01y#F`TZ?&;fe#OlVD!cZ0tmoH;`_bvQ8SYt1!dsb*KOEh` zi?0i$;^EeuGa(*(_A5|T8r7^o<9Pe;o#LWf%G>DL1Hx zcd+@{(f-Dko}hADt^{HEJW*T7D{MiCaEev0rxl z90VA^%K4JuorC?|16Sj!#>>lP(|cn~4Mlmf^EONxZ^*9sg%a+kTjW^2pO=(S2!g{6 zdH$CX)N8HQPb{l*e29$I!egR`e730={(y;ov79YAZ3sd}Sx>Rwi_ha`=I&y=hkCQ) z&HIa72humuaIb5!reB7(stR~ZG&T6a8%OvkZ_LD?k(jUP7;B$%dd@5`>sr)!JF;Me zYoKR$@zBYxI;^piEve^6N=Rh|OuWQs{>XI1-65jtN~SIL>90@!q1s0N@9Db~t@$XAHBBRet)g;Lv=qBvGk-FBG02t>UAT_o z;W1$k)^OV(!k1GBXBcU$Z*d@aV|@z+8zc~Cgqvq93MtSQ<9S~r*cbkJ8$lUlt2Ef<=Pl}GSN3ZC;4@ICOMce9HaD*1e^ z$repEE%(Ha!RZ#4EByKR{T*aHRRDVENn*0EsF0FoZZEA_8c0}9J+#qs%Axt$O-YBN zIV1P;@v+KF*JsxJ`1I%PJNg2qmku%Im6fYgF2Z!M7b2373==#yw#K*cW)^o<3Iad|a7t<#8~eVtz^%@*?qIi^dO>$N1=J{p^+o5u5T^ zj^DYkbC)_$UHpx~8gWd%-n%+IV?S5>d`iDB!-<21V;j$|ZdXSLpBJDafgX_0N4}%&Z{v?fVk2X5hdU~4F_(!cQ43cG~(LKBfJKqk4LZ<+b zMI+P15A?hslH41|s2)l8E0KVT`g7273T}wzck|m=c4q_+cB<=y<1lOxVwLA;e;SiN zCh@#-LJ&Y{STQL^AqvMQWfZXqWypaaYzUJDg{vx@J%jj6Vl2nJJX^=^A>7q%dk_(n zZ&`{~hP)hSK7@%b06i+03~GCQpJ9fv zFZ+Cjdj))6+y^vzxxe#(G7tCe?1%>!ik93Ct*}y6++UyEjzWOTP6Cpp#d&dkb*R%f zkve_K(%+G|EiFr3Y|rzV>mTC7b+ZpfVay@3OZy+;XxMg^8cLFm-V?HQ_TzQyMF6iO zCW>YE*e_J5_TAC`R!E#;vfUbX0>8>hB0aSc5}@%K1Yc#o=7?vG%1)%o5sMa*qwhsN zm{J`JK~$K9N?O9yEe_wB9gO*Y3LQRSJ@fJ%&0T5+~r1ZJ_$nwM(ig)My z7|H8NxVZn*gA6Y3{6mz8eU^$eIr391EMD6&Z=9UX^3V_hMX|T>KCw$r_oHPm#w`b; z8n3ynzcoE+?jt1+3{vOQ=~yYLO{fc(`aSkMVjc7^>|8+|Zm@+^7(m{T8h0m1*ED2dmbaE_4kNI0AbNyY2&COAmESD2xV zb7eLeIjffkDB9Hc4y7G`m@w(3i-D6xx6PA4h7qUF>D3lo!f((NLUfvaoD~s&5`=QZ{cw{56;h|IqZE0;H zLEwpI?6+e)nuybCI%}^8;-j)y!bv+Wzpd<{N^S_aBmnw6#Ab;K%qfQ_L~}-DOSQ5K z;m6MYMq6`ZFy|(w*=*@c+=iJ+ZA=4V>{ZUZRPDva+!{kTT+X8LQymi*&x1tvmx-L0+nuvc^+O_>TQ^yo-JC`?TDw|n(2@q zdVVqSGsH5y1bj{0ot-SR>Yi6$bGR>p()FA8avCO4O(|`!pOE=fXWz`TOEB_lQr)d2 z%ziVj2S2I7u=&z~rbf0jL)Iznk-n3tsy_hgpZyQhKT({X{4V-X|HF1=o(uD7BRs`{ z_dUo);=>kEnryT)5ge(I0mx7uNM3dx$)+fAlv^nAxQ$++xi-g(vb=BQk^GO-KjlZV z^%tgpM*kb>KH}xkTT{3WYAq$+kQy@bvJjrwzbq(32 zMtLypInyEEC!M4e2hyAYL#FlMCZkQ4;xGx|N@rv-j*iGx8$LIbC>Wx%u6yEC9|dq7 zdW@k&MquI}>1z7514L>kxcW+(55csDNim`V6%(U?n8MPe#6Zq(tan$e!*~inUjTj&_0%35%n)I`xj)XQoY^Ffj--(oA31Is+(`X`p*@`>ecyz{ zZ-FcK+<@msFC{dmMC`Ld5hBnLYpezufBHEm~D^Hyo?l?YkWL; zq$U}bNIDzl52CR9r)APeo5J5!EG=#G-Rg6}R4)<{cFHhVj8PJ611Eh5GRJiFa(a&h zZ4azTP}9+xDijVUZI2ZFjv2$n4B8EslA$!|l%W6|_jeRUDo9U7WuyWthe3A!&`)-=LKywtL?nblT(;!nvan6c}# zZYx1w55>4vQ1|X3PN?_rsS=J+a%>+x!YL*lsOJqcMDw=>c;FpT*LvQ4iz%hAl7w{5 zT!ay8AJgGb0qdz({}{`;D~x5`k3oF?!=R?($N0p6YJGLkiGh~q;z!MQblI`Hsa|5- zvB;Xd$(uV#qhloB5o|6v+RkG*$9`m`N!i^P#)ApgX0bI~P&#AD!xdy6HA9STRO z-P6Im6)j@QhQr=XLh?81yqM);v+VIPC0+uy()*>ynrQf%{I1A}V)|wtOCAB|9L222 z)#=vy4kuBHaED7b#LGEkA=wZoJP;0&?J>lBReKn_TH^@d^83Nhm0n5)rsDBx&}mq& z`kJpSop89CI957NA@XT)-$~iS-%}MQ{R|5+TE$7F8$(E&ame_&aq*#(zgtE}XANR% zXrw&u3?Dt?gX%FtPNk#dVaMsN{4O4$le}p8PReP}qob3nTjP!QZ|>Z}Q(`zbu)aLz z0%MS|dTqW~%s;3>PbFv#bXGikW~%98`F`~g%o-`Z#;%Y!7V1fUT)NJ95+7Vadf)?T zHAz)MG3ZNX+E$Kr(1QGST}$&^bkcXKpG(@XHexM5(>-14xGSlUO}(j~hZx)g^W5p1 zTgwmYk|a>&j*zfeW?cGiDC8uC8_K%G9z$oa-D9V?O>N+Gy)`q_GinO2Ww6tNP^Oe- zr06B)ozE;~UESY<%Mg;LTqJ!O#3h4igd+<_R_rRBjgUjSU|mY$!s#>u#PdbY9P!-n!7H1DSQuA{ z2VOoMiWaUa36QF)2y-JY3a8VVN=n7klFMW_Iw-C zxQ?o#c{ldt^t|S+`!`d>iR=2kIff8j3chcm+$L45a-rov6E;@XEAowIpk5j1^ewr{ zG@19_c*d^MXqK|WL;1ZXOtsNNxofwqN=m;3S8!gL&nmsxH6NDih}9HC&zHUcOJ9$^ z*oM@XzUFW6%BU_-W{hhyjND$P141sUW- zHc64IiTTloBfxPQHLC8E&!9`LxVO1^C+C&Z2z8!r1sXB+YO<#i z6J|Li!rG>1ekh}?+0ETK9w>}^pw#;&JVweOaU$J(sM|}7lFvF#1yP_Mt80wZ)q`SY zDB?({_*c4GfnUwA)fAtu33%)+e4`-a3D$`+H`wczuNgl~60&d9GbcJG<0s~tR?eHe zXT`r$=(99Zwe>)WY{@7?A3MGr?$Kw$_giHY>LN7~vqFZt%Y&rL7zy1Y0S~3m9kTot zOO&WqHHB2S35+F2@;790gVs+fCF~_sOowKuy2<3E)VJvE!hfO?C1_OHkEs+_yG0{)-VcG)pxpqQQKd~h7POHf zS+V3l5q2cSKPirN7ZI`#uuU%^y&esZ+03PA!k%dBY|Eo+P+}3LCb2YVt7&%=V`=px zNOrLMHufeTSNHSC3COh=8Rr!%Anxb*%{|m#ygQ}{YbcwGSUJe-85V)5i6k*#iyin-iz`nbk{lbuwBO`e0kB+DinGEAMt1lo?tS=O&4#JBhNcViVfg$)?T z-xe+kJ3qmoL=2E&GlEz$Xb+r_lN5OF;o}*?EX@XzX6CyCxj07IAKiL= z#KGk-Ivk`jK5%NZ)?ypoWB)Iu^OP~_H{k1+N?DgR?sJvE(a}}+0@a)2$2j~?Pue$a zwQuDgy?Sdre@KOvZO9%dm7A_V9?cHm_a2JeFM;NAc?~-=o*A&@%IeFDp?MhiJZzV%Qc*fE3kHU06Sz|Ka1^hyn({>Y3vPe*y{pC zkOnLd`tm4YH4{PZTCcu{hgiw4p2 zgnsN=NvUX4B(r+3?$C+rJbfWM$7E;m=gFR!{*1W`TkGo_nXwR+UT!TXFNCIi-ijGH z&we-RTKbJXBzDTYhdCEr(}y5E-NFY3JsaG(7U+A_9923TK(Yw2 zhK;;}*L&Vo$>5?9?XL*~^M%XFWfq)gK`Ryo@CqM#UYQz64X98Qy4yougJwW@ug`#C zB&rg2t4Es>2x(N(^rP21@LakKr(P2j!t<_F0JTcyCLaBY%UFrQHR>^{AbbK-g)iIc zy$<eUfgIaLQMXwchM?F^OR^4MAdM~iur^y z(qU{mw74iHH$EcwE9pYG@c5`m4Y$*S5KB?qM9g7rVab<>=U-e(*%m5Bhuk*^Vu-~d zcd;%r>Jtdw5^hY!GxwaCXH5yo5mGo@_R55(F71%ImMzX>&5$~kD50FV3wu3H5yjq( z=ps;`mts(8GMLMdC90YfU~BX4T2$s81))H=zTcfMF_liK{CHh8P66ROoF(A_>Sf*{KDLAPpno;P>&A@;ABKd>qA3|qisAj`O%CDfVekYwK4 z>!LMM^v)p7k#19&J=rAw+Vtm&3qik~4mG~Z4mHjvj+l&IO9>gX{duS;Psl3_WISklw!K=22rtvhB}f9vyMqz^0?I<9i_QC>i)6icyxK=WeiRam)hW7SC!KgS4C= zmQVaU-%R&{iD68JVf6cx=>T6N2xc|zks}s3U)rNLNAdMk76A7`llMAl{(BdaoaESS zn1DmAkqZX_!y%eaX9iN{%LzjXtNmT!;_H5ldJHh^kb=l-qwQV9he0ufu+d^k zJEN8Pk&Wx;r(arQbmTbr%l&%(1upUDY7LaH&8?L*U%n>f5Jo1iC z_8d?=OcYMK_XG7HzLl!mJstcCPWPk?lpxTj$q)H`=7;drz7{W8Sh+d96HUkd47RcP z-=&B}i&%&~MrZfkq_jZ96AtR4yq3a;qL8c^3gaZdx@pDWsVi7Lag2)atp{sjzI$eq zQtYqqJf1A(J5(h#?1GlLYtr=gdE$x!zJ!$r#it#ni>O5LL zH52uCt|ZjY7(BbXIVq2_|Cj00_OYhV2ypNGz!>Smy*Jm4t_MG_M+h`y89d(H+nPMg zTH{-!%VtH3wJ#5;r^5%yKkxx#Pf^dpZ{I2xo`-0s_r@bs5Z%UGW}CYt{IF9ERSyxB zelu!*L??gp%~<6xeO3iLYe}Oim${{e1CZ=e`;B-JSimo6u7!0LN z`+M#&gz(94WpbZVg$^6*PB(-V^^$wI(NIclHecZ0FfgecMk`6~96y}+>kNNm`b*{? zzs^m>0jFy=xRyHJL_~UHPA793SIm<3P1VDtd&(b>Z(w5Zp!Dp>3|R=4x#zcM57r_# z5%(c+tv7RHva^Xzzxkk=r>wU^QK4Z%AA3GE%fs~g@a^&T1CH(Jrk^3=)BVi!nE7;Y zDJU>gmx3Mx4j-5h9WXFE?`U2z$~?m}Olxs<&@`lrWO(mylCzmSLA@N-4WM9oT_mp% zqNQ;11<-t}!*`wUT?L=*9!PI(mE)DWApKp?5l7i_ZDYP0OMlG6Pcdt86V{9-nu zE;xpL%wJY_HuTLJmxJPoQ}#TCX`%)zj}hS>J($qPUMO%XXOtQPgDg#yx}oJykFZg~ zUf7CmEpj22_4&SkkIl2iUqhzhv4ND2xR#DrjVk+grSidA#`M`)E3vGoG1`@sELAmz za{#hb!5GNjky0mSa|mHBr`S-dl~mI-oR1f1cWJbc54lc5&SjCRGanD;HJr1Q5*=zZ zUPa3!uH}}ZBqycu5nu7PGyP}jTl1eUs#bg>{ppX|J)lbz=^DJt=ohwWaVY%PyL^NG zwVvJ(8MNtW5U><}x82n*+9dHtriEXr`rJsbwbPe!W$&+<#gtm5&mbeOr_(a(izI1o z722b_77iU|QMF)GRoTOt)e0lH;XSvzEe8CX(^Jh4GY0xY$Pj#1KFkm*U!XWEe`6AA^N*JtB@#2CtX7Z0X-lH@=2&M{>McTX^1v8>E4as6ad z%K^+{Zu4g62f@BZYj$I-(Dd^cC($1!#mV*ER&6RVAg5zLVn zWD(EJEQLlw3J&XdDtcSsDoTcGw)5S7avoNprdwODB5GK^TNgF9P_?z<2Y5loc3YEd z;pJihyYr#^(L6d*+y3r&9~WQg{6$T6#>i~ENGAJ>%w%glsdteoU`seRvAkkzv%B-X zQO@`%Fk&>T>}g${&VK0ds7?TYWd}!{NI^gHoqIHhkAa9QqX6$_ksWJ z^hE>7DdF0|E)`=UU}Kl^*wfhTuPr8A3`1>Qh$n&5j(MId>kCjJ5;n}En7L?C3EDxJQC z8TepC!6UBe8p2JlDkLq-j>IM-#o=~YOvy8885ctsu)~o%50M!;;rH29wKGkzxPNc@ z>nwV0r!@FPMcv4Bi)28{3tV!Th7#&=?sRm-uY`zTvZ8wcCp!0%;p~ZqEGqQehzK=c zNN44hRKG&P$u4j*ksn}Ucpx!&TX>S=e0-UF%4A=$KasOZiP@ilq0IO8Ihq7sxlDm zwxPRWx99EtoU8d}$p3BnDKpa-{k@SsFgYlQBe%ldo*)(C!-*ywqOd=mjXo->CKW=L z$#vev=QDO7xlzbI3Cx!Dn@tp~hq?fC$`%5iW&m%{;qS-v_j$fg>FG;N0-48@uo=?1 zEDpwCz7fitY|8*cdX~x#Ijqqe@;IR=bK5+dDW4D_i3yU@a_s;Q(sYB{2?S615!{fl zdkxf~D#Sj#3?5a8vuyTZY|74R9E2pbkf?!bY$BCV65sv))ZEBMM|SlM%0+iF-w zL(vjLHr}eSl5NDfo?*Wi0dU%Mce0BMl{+2ktd+{Sy6a~t>5t1UwRZA6tD~b=dcPEqzR39rp?^9zw-V8XAprcvb zf;*}WAa2GTbu^1xaCV3dI>tQD23*~xIqDM37rDA|DE^cXJ_LK7sjF98Ut|L65M;=R zmKf`3(ya18$bAU-}TiB-huM||}E+IZVaCfKn3)J-X@O*j23 zlx$_Vf_7FAKUh`}YruqKpiWi^wMFkq>Zhq{PRVPYL#~U$o4sgQH%?PMlwLDWBvWkr z!`Prxx1 zofyi>!5c^Aw?OxFs_zKhloyXo4FCJ|RXZ0^FRKpHD;EZ8NXpaGD;KNt1(KR(lZ|}% z?NKn(q>*xIt%9>Je(O3MMDdg$Mm{|Rw)u{KkL4pf9F+SJ|Q+f;y1+0 z7r}Fj(a|wl38}+j6fZ)^{E{hqFeq`S3(z>up@z}X>rF7q!yVGy*7WysTk}m*2@E7| zHA2`PrIAI1bsvuL4cZEndaP!5G(VI*M4c$i!KGv=FNg+FZh(VcQjv+Ju;kFn~KVUOoJgQIfr|sxDL? zbhx_G{f|KtBIPSgBeF5`Q^LvXVdpWPW0lSBV<`JYr8Hw88(kA+jJX8+5waOtgH(3G z1|Om{>8qv{>avxi2#IMZ&4v+zo>`sm9&-J7pUv=(rgzNp!aCCutzyUzdh_P$o!fG` z*ng*aQWSTfe26`KSm%QykDy(Xd%8iX8pg4QP^MRv8B)gtXr1PJN~{hg+j*)zkdL_& zNxj<~BTq>RO~S^Ya}s9gmzTyl6SeAK&hn933LN12lRLZnKc{b)6Q&5 zw!41Di+=K{13(^}Ct!sECE_~Xq|`Bj>K2=^vT1*cQk8y_^<3;&$`~Nllw9VqvJ-?qw@9cJyz|#@(tMFy4=sv#4-w6aJ~d#Vbk1oFt6pw+3v3QCE4F8xO<9BC7|F?zg(PAe zUt)`9For+-?tFjp@UY2ks85#!TQE_XK5WQI+wAp*n2EER@9nrS27Yn+HMaL1SKMbu z?h1tmTriDyq;j7g`m8}^@1}P#{E@E5K1WU(rA!j!*5u)M^>71sZVJuyc+JA@WK*Lp z?`Sf)9-NZUYAjc_erWXY| zyJz)vWrUCinFsw#(+#t^TAt$VIn5V6cSn(}#XMgf`M;;X(L4?$uO$io4_zkftLf3f0bk4J01NXS@xuGWo zo&8yzZ|}}`;6OBMa{WKkZPWLcJKDgvhwXdhFO2wR6i1Nl6}N3*BHa@c9>%rCK?m=h z;quPj9+f4#nRhY|rK1;mto0!d2<99mTF?tud8#~^16c_yrTX%jf(ObgiyQN8^t{O% zBupe-_Gr8`>)X%~lkAf<6tN(v9#M(U569;4bM(Tvw26l$RS0)&Gq)Lx;dEo-?G!VZ+UOU@Iqgg3`SV8o=+X45Whc&8gLk zKmID2?}6U{?BU|7?5xpRk~4vj_q1>Gw?GAFS8Q;3Ap5w#JYCqLMDT_-%WI2Hgq_4x zFjl!pFxPirv&ZBXWNq@DB8y8RMgmU`PxCcC-$XB`5SKAcRQZ~32g7Rl4`&(pa z-K6+dFU!X`Zc_0xD5F{KWe+DzZ{J+EQTk>Y#_Rf7&wKE5;jnoMdfo#FaacpDM^=og zldP8Yk`Z5<&0Q^5-}^Cj+&YG*uFb$Mg;rmeulg^{f--$UR;GxrPf8AZ^YW@661j_c z^1@l8J%Dz$`?!)g+Pr@+$6!67j{?;-v9$FSyrUkAi=}%qMWfBsnhjO zrWZf@k&pb|Z~xu$!0Gf2`j~o7|1?GpB@P1Lyw*Cu7}h!!icQ_7Y(b297=cs@_K6vg zJ;27jJOhcq6H35>3oRB%Lx%~$T&w0h3@3qv2649>iQ%nZORe8Sz;yG?Ysp(fHC?L| zI-J@?ZU&`NU0i-n5`ib2lPl^cqSi0Yq4@j9m>E1NxEVq*Nm^IrCoE3@7kY6p=;kAR zY7bKiwGg=5y}PxxgV#g1<~g?bPp98#rbG|Z7hF`nTY};W`}woolalNL`cUr_3dakEN`x5aXQo2N z*-I!mr=`=DD`&;zxr#x0%GRE$82&WtT(?jWXy+shM(_bjO|!IEQ8&4>au4^d#dsUH zrV5cvKSKM=?=daanjrMl`gq|IMVdIh+I#yJC5&JlQ|Th;bfv{5VM41hF#SNi5(RPW;=Pf> zM{E<|1;K%IK0%Yb=FF|_juu}#Bd4hjKP|5bjBg-Mht())ZIkYE3e;(b4~Og3_1SMU z!9b``V^xah%3iA27ZjT(Xb4l!wpvv?AyXq!kJ15ciibYc{EDcfDX-WTMIB1TeV14l zWcB)0i?|)WbcBV$>xS|P^PI4B>qDe#aLa0Wy9u5&wu|=7*%3qU*HO7~H1S+{`OO<_ z0~?{?CX{2i0~Z@9Q(Blcc6Bo``|2X-&F(pVYpP`XLlefA%mX#*Y{2_eR-{fGpK7;N zsS~e^w3Ml{0f#F}b*hNt1v!adR-2?%nwmbFDObNu=v@|5-WLmm-qk=}FKY|43!ge@ z3A3xgMGIQqm@E*^n9a((|LL!x?R&*yp{EUxx~n_m9R6tn_2V~Dwf-CQ?7e^?oonJb z-GDBA=(2M;RQNh#{EF56A#u1Yj^{ni>jeY7xnn6F>E7)rqUkS&kh4_YR#p*Z%fY{+JMY;%h2nYHDI zd(MZBdla8D?26HPP)(30n=qw6~xi$iWOXmM)Jd}>`+!PiRW1T6V@{Ug(y zdz-5-{8T+J?1>T%WlK*UyJxt|JfD`MGNeRE3BX zw0^AKW9tWE>!Q>3Au5rOy()p*5JJs~IuyQh7KE~~-8HvD*ASVW|QboVc22}c}(SR!5lZS`bAam|zQ=5qEM!U{F z=v*LS$@X^w9np0dnr%wQ>{1XS$~(ylW!CLYI*F1dExFZ^n5ju8%{Kt(T(m>*at`wkn9-Go;s)IoI1^}kz`$6 z7}CNe!-gb7fB{>OAV9DeHVhety|5PcLK|)F=E8tBGGH&Pg}jg#79hi4JTfvf&L97u ze{~yfV5Y09A|fLrBjb;ZjELk^k76@dAJY&^$=j9=0Y9Kq)VpCSq&UUBd zL$B8^;OQ&@zXYJG6%KF_iIH3EN2-jT!~5W8x~dmA0#C^zgP#7odUdqL$beJwAq23B z#Usa>h&$;x(X=zQEn&f4L{u{c8{8!C6A81GgAfg(brEBIBJInS44histq_OPA{}h3 zkEHKxHEl|LBnn62$CiKj#UCP{@vqon@I`g#`e4W!_7f2y_ru8X^wtLNnb`t-o0$3Q z#*8}qcP5*~?o&{s3#SM+^}slyP(0tD7tbAi;RtKWij=DY*ga6@!L_@U2bcej7`h}! zot>l2-fU@k+Yz+->P=Hc{EVFL3buX+#7_XJC4^xU;Ij0Hm2~HrBsm})?fTO=Pc4uU z=zgZ@2^Uh2h)?<$RWgVG$q%dmT^7HW7JXVP_}l0ah*oR*X^4iQi?s@^e|nUf+O{Bk zDb=_dZ}&YxMT#cvo`vwv&OLaN+~kQ}IHZkDW>!w&)&3&2U2Yvl zzp~YYBbGEmUcvUOcIpe0ws@~~)=XNrq7gF|5G+EkRm^a5V!@dhge-9tyKp4v?>l95 z*ZrN+80hcrMD3th`Q*$n-K!6^4wCoxFa!;m71Ggoi4<~>rz_VUBjrrGRl^0QY(yj( zOAUa@{5#~e{7v&(UK9&O9Zj;g;$WoZi+q)pXhcp^ovtkw)BqI6aLG3-Q=J^~7bMl` z!DbQBd`%!eo2+IN)Zd{ha8-6_{lXD^hujx(AjSzoJ)q1vwp=U{b+R0^Kbbhr6lok^ z>MA}!^EIw0>}5+Y!y(LcZ!NJ_$B4SBJ5<_$Pg5^R^~3uWM7&2`pRxk*(h4o{~^(;^V%d@h3(yY0@ET|a_O?92KNcMJsG@r(a7s^ zpaZJTFPw_~iKI`Cu38gn=pYYJ^%x3rEEQh*BDBFa$wBFE9ebT(QC~6BmS|p-;ATLB zqf=blpohjMFWi88SB#pM4Oszsg?3SrmthXlh$ZWmAN0}}?j4HK6g#WH^A3Kr4 ztQU01ti%Mf_7msnf zwx>55B&{!8`O&~QZ!T&Qa$#TE=2J1xXhL$BJela z){ffdYQ4SjV^nO=s^xODwdm`{vfy4tR6&Yr{VL7qFPUnk z`zL*R(yd5^Z;zj(w+#EIY;Pr;imh~bmKQMx*s6j1*`gx_ee~Au1&1%(TB!7gEm4^cHSmP~mtx-#gbW?~HO+|D%8}SgJYq9r z?aSkQ#R1;mGh4|6W<%uQsv?KJiK9$`u9}5^X~Y2UZ#8?2B=AUBcn@Pu_r^A9k&I`7 zmAwd5MC3Gc)TlL!FjeodC>-yhWL}oA;HTucJe@qtlYEgh;*n{DwpJO0I}6u$O&up^nS*!W-pge_t6b>@7f-6NpXaPJG9K_kxDAXHp!A!t*QV;eE&X) z?+=^!EC490pcLG;tjJ7G;J!=H(1IuKVCUeiGJ80;tH7yDBi{1R+{z-7L z7L^EozC(N0a`PiKJ5R0-;Du`0=u!7I$pG;vq5krnxcts$GIk<9r z@g=HDbnzc9OW{St&)=Eioro4lu5IvQL$AgbT)s~>eII1tif8L?88=Wff^|$Sk1_UlksSsME%56hSK-nR~0+wF; zyawUidC6Gk5OBkk<+X}vH8_>-!E#xI<(y1jyi5I)`@cBvB)`~X-efr6mi~N7oO+p+ zLWx*@UGBe1(0&V}pM0|~v2p&2jPrRj&Xh@Ve|9*f&(-z$W_pOy=B)f@3HZOWv_Hz| zNx)XzUtcjf48AQ%C`LaotOJ*41`@nU$3gK+14BTsaFa-U;D!LTJKT-Q;beDqbX+C2 zR7K>IlNYtY+G?)|xoyF$U^8L0dSJBwH7>*sarU%nI!ao2JOD*8>K5L)1{Jrl^X2|+ z1U#ZaDJ3WIu1Zv~B{t<9(u+T6PhC(>&;Xm1x1}KIz(XIbtO?a9las3%erIz?9(BG1 z>_wDB-NM&)gh>umfrQKH>TcA5an3|3*N{^wz{`>i0XeUOWlJEd0}HPOZw(^XHXsD~0LS(FPf_8D*dO9o)K0K$ zQ{xzULqE0o4T+>;HpQxw00Pm1fm}d*J$$XQgH`Hu<|Ff+UUVt*2pvXx`WTZyf!1~w ziUTit0LWAK>DoHt-W=-Ut%ji)TYI4n9DgP4_%q*H1l2Qfi*{EBWJk`pA0bf-+p2(u zWct^>O?^G_&xv@3+nRB6iY}RZ$YU<`%5nN3-K=%Uix^Mx&O6c}OPb;Kz##(H6RR#l z#vvRN?vP!GAl{Nw#t`~tix*qvsn~osKP%vE)7dxF584(;q~#y);HC}_PHbZ|$zLxK zWfEH{vTiajNYLgd(_DK0GJWFyq(v_%O*%)Bg;LkD1Xc5_zH>S@0&OHqV~D!gF%AL zf-YL7H}LkYdSv*d^r66^5tRG0UN{8J8W?zm>&wyH0e)aFxhuHJqnAH7M^E*Afg9As z3mH^Jmrf^v*9E72ZZ8SK5G(R^x`~#DyIAsLIeAFe16z#qv>5-u7DL->>p2k3tzp>? z(SxaYr}_(r@{tUaK4@I~fpZG79q{d^H|=iW|MP}$*f8T%E;Eiv!5PJ*pq!FPO1)P{ zW=XLvb-Jplh;_R_;`}`o=k$bnRg9V%ZTKeUR<(vH4lUF?Mz z9}z}BgXNdgji(Lq`td{6(ngv%VIm!9v7e1*wMZP4Bo!Ql<1Rpn*w&}O5v_pl<6`Lf zGQZmWg#+eKtBpTITHCmBde`lRR%Vs%)T^X~f7-q|8suj9o#{R`xEqEh)5?1WX0``9 zy=VdGHB!|0&Y}lgBk0_W2o`m>crWS;$FFT3AMmTtFL1(X;6-R3asdZ{Q zNXx0tB@QfgB~UP!+^QRygVqpy)6B#cwa-l8TV^JZLo+i^{Mvkfxx!;Sbc|Pyk&Dvm@ByIX2Oza?`^?*FEc$IIH(fE57i?L*)A!V|~N`ONfg}!jQr)1Lk zR7Dmk@yPrKD27S$HOJWf(8fj|ahMoqO z)cV`>;{1feTv-?wX-EAp=31XI=n&+)0HLUAF978`g#hGm3QNX6o2iU&iMaZQ#uXn( zL2xOr$L~FuES{BVat{1marTjd!xj+r+o^|~ybWtYeT|0MGauw#`7({hvy8QyNDV{t z)a3$4&O8}63QeX2^bsis{uaPTb0n3((jO2yHQ6hVwr&7^P9b--DxWm_!h!NV+_f(! zvpL&X^2zfz z)wk_5qz-xMCF=L68MlnsHRCGZw-*Qk6?_Yi)zGLm69xZc5dWeG<2WewWAAU)PlpJ; zBLr#0AT`cBTfa2MMNB~E$+$?BKgVT%)a1G z!rb5^2hovXTRkA^4G7(zVuWzP=)SpSe{vz_1cjK0;*(>8?4L4V0nE~3}Ca~%d8`ruMZ!+>q3jin$h zDoYVx1(#0d@QHO}9DBFVsW9z&k2-)tO^_7@mEdCse7hTaU`#TndCYJPXm7N9(5?V( z?AMG=1`$rk5QTg-zB;@s3gBVAlqyb++zwbNZYL=*UWE+VCHtf7hA^~khCk_?3p4Df z((Y>dnA6I(e94Jm(`TG>UsR>SxpMp??dL*x@iF5gvN0c;jd}I{QxseNbaU`{x=LAN z(bp6YUCZ0y(pND8L%|L`%bUXAu|YaZ50VdAt#br)@8Nxgiv~SUV6;%{A%a?p+i;xF zmL|halBO)S`$<}|PY4aJQtFm6%3IPtm4(Apct5dvJSO${$le&yHUuqBkAk**`L*Qc z)E|G%Z!ia&8FVo-J2nr!MTFp)@f7&a$*MOZ~Wuzw&m z-8!$+KKXxs>096Wi+}Ym$ji}Ryg|SEKdM;LoAj%h1{B4|+o50mfeNaMI0@9BAgF(0 zL8y-<9^={Jcs;5`aHvW5I-}S-6)$?lcyqdk3`NRJJ-okMQcNZux|-nhM59YMDWfsS zYVvqCr`%&q!@{x6q{`^tZKSjvfzlXOv>Cb=Y%hubFV|V32cDh(f7V%e`5Nn7#-2{I zICOfwKF5Xo9Fbqq4RVCql>_)*lw#NW888}-khwTK3*bu?H)hqIEoq_V7`eBPVW^-TcVm;ifW9J4uzbA7JZ7LQ+Szu zyveb!#c5Ppl4i;_=J=}g3-(k-n$HD=CXk8lAI50|S3cDu&lY+VrT6IGrf&&HQImH9 zDDbV~m%v)HP56i}G`YUE5YOk6)xqYlI83gx`r=#;e=y+?T3vk@B0f+Qt zdh%h%2+;oxZHDEdWelez?{yGV8JvPzmjF253L-rPIeL<*SmN7@(QZmj;OpoJ8q+yz z=xpm4GxiRM@zE(6pqtNT2dCG$iqt35>E2^}alavQDA=D<;mGO8y+FFe?DdLgSn)rW z6Nukk{#)KVO2?;sXyDGT`ztglKCI-3hT58rAvz8af1*Abyp`HLpzLn8uLpso8JLkU zQPG*4ylqIRc%UbtE73$)dMN4;TjwC!Zx34+Wm5DJi0JAo7P>z>X=#f07;g2tyEB2@ ziz8ikQ==KV;!Xl8b95Y*K2A^$06q7wF*_u!s49^(Y!^vKvsd`!I-Bkl$32PKMR?!G z$AhlTrL_0v^vPs1r+2-Yms>B%6;rjfs>CvlIo3tVnnKV^W zvSZ;xne<&8PAAIo4i!6bo7Q_cZLVfT-xi0g*TKUIdr?LK;B*hV-Mx!2awSC1c zleMyc>XLUFBIt-@r>Y{7^V$iB!><5lbqZlE9kte|5FYv`w2R<<2#k%RzVbQknSifLA(2<4w(GauO7s+U3Q4yjIJ5x76G`i%!ZJmmAjzOZBtIem?A;XQ z*2_wYn)QC&Io2Y(;DEs4isa zAt}8F&~lo2YY(|SSAki~yB(2+Jmq(HXY&|+^Gg*R*r%__5w6u+)aQ14oi3p`X7Ros zqqNxzZF@MhmY3Pq9&hP={662or%=2ZKH!Zni-H*-*KW z&~y@gDf?)AmkJ{K0MhF9FssLbJJYXbyUW~+>Mfe}|828Amp=KdrkQ02MhQ;?lqlf| zJ7c>0bmw?+^mK{`=&Noyjv^;-D9LbG?q<+IG(@0pzyP1}icJSmTXo0jD``HzmLo zi5Q@3uZGd3tZhmi0`r1pm2iUPdl1J!2+UPy1q&hIx6#C4KbnU;_Tk2q4@ZQexfI3F z5u_##6_EGmlWhLn?B6HVJ3P9D_JgbCf@-MMae;3721oDG7GB#H)@MoWVRQ71(XQ*p zn#0h?jEcdAao(*SKuUdO#^TYZgnGCQ58?~3J#;A`rMr3hMsoJu`G4@^Lt#wO@to^Z(IDwWeH>K1Am zHkuL|!Mec`#=x`?rXj?3NmuY`kccgfxQ&#w43TILOPcP}3mtA&fAW6{yfk1m8A_t= z03Q%AeOc0HV(t6yyGlAtIFV3TdUqhBBq@b&qU8b1s$B?;2Z z)bLUiGZ+#lWiE_U3@SlMREJsd(EXy}kE(qcs0ZGr@ZFpo`TF00;pxeP3{wPYq+B#y z0mdwpXiHZ^Os>H%Fb7!VK4}H`7$&&{w!lp?S4|IDWZ%C7FGS$ZH%^NO=Jh>tza7e$ zx$!zX{3ia=2YCd431tj_+&Z{>TZC+ub4qI!0)P0^C7PF^2bC5!n-3)WgX)`#4n0os z-zA*h`n8g{0ox(vwywB zBJz}L(kB2qV;0n-sZkwhAGbt?{=S2AeVHKO7U!H4N!?uT;wiPFv$O-_V`srtHb)MS zV{bHW2=U_m6{ThOGB5p28rbLlCJk_UdM#!U$yMWeL^A?WVhzK78|F8McHX0X@rvz> z@6sP>$Jsaz;H(_Z?$0;KnTde3_rn8f=QaJZ*jJ92W}$yReT*79+)JRfYK{vzSiR>a z{$Qb~)gZMTd@|ejy%ijuUibqRjI2|-zue_IP<=*bU~aN0hvz16O>-lkdGy4n>-KG= zihPAt&_t+eJ~G9Bk=A#|%GO>0Qvdv@Hg|Axl*`*RGiixk$UbkuidppHkaSns@&pyn z=}*y|{(**ErY9UT)=Qy~yuRM93K@d&lk3&YLQv}8!~HL#UGT1aDn~DD>dW81#f13a zN@1?fhdCY9Jp5@e%CX;k3F;j;KLp-I7>5l6Z0_Ij0qQ&*Vow`Uh-#SQ+lr*aWS>sa+ zm)pE{38}Eb)y|O#xb4s8)u@a@m4|P(M3wDsHTp!%y$Wm8K+K~la6f95u-`dj{!&y@ z@2BVl_I5fV@8bPdBv7zpf~p!^yy5BkF}v&B&or~;=_z)~MZrA1C{gK7L+hbp)GAy+Kw_14-Xq-9OvZ_x(M_dB$xEGs z@gF)ZWBw)<1b8t-#J|I8gZ98D-`QZ-THRtoT2a>N- zKagbxjv3xEMipu2N7B6mvzA@%II%>CeqPR}9w2BWH<>?N?))esNM1X~#3KOm&~MJ@ zQP~LMee#sMCQZ*;_4CP5JU4aPFr9PfBs~v2I0V1NC0vA*UlScs+7FkiIT0i35cf|_ ztK|g_(7iwMg44Iq=n&|cjG<^i~He#mrOT&(Qa=|6X?#2dBKOD?{QJ;7nN_quHP>@_k z*~MJ6YHQ&zw4Zir1R8 z@*eVBqmeEOZx5F9>F`7}6q3Ha*XFZH-aZv)lM^ivdWbg9(_X+A3l#;I@gn>-&GS39 z2&ze{qfL`djVbTnfD7PrBpmtV~^5T&5r# zRrQ{8Q_xV+DfC;$Yu0Z8AEDnGCzBkXqhp%N1aG!-wl9UtB5l-J1Fe%c2GdF*rT>Xf!8P>O?jZ0`gN>~Bl2f0+s7QA zbT8Io>CN#`Bq)1o=@^@ZpOabmXJ-EF6|O|1;d^0qKSN&(Xq1$;B*0u85@}TCM>ly^ z!f?N5;jP#3#5Rk)`7}9ikE4OQkV>y#J{E3CjXNt@9-fg6WH@7{WJo)U$>I9x5>8?` zB`Z(qLP9tUHII=r5DJo&QllBc*@`M$!&H!BRFQ7B$5w{LV{tp}hT+V+H#^?Bl2+{| zU=4)dOY;y6x(J`+Sl@tYS~J4rTLIIci)e*Wx6yzfCA+xtD(~T(2Aj=5{Y+VM5G}aD zHyTOP_A#FkGbriX5OHI3O@O_wk|}F~9^S0BR0*YFc@eP{1!w0iVjXu@1ynEJ_8bZm z9L0ft20K+glO?DIviQ0y%W@uKr!ktJE&Lb{)7G=;BZ;?QCw(-3sIDRiaX*yOiE%iYnq0QL?B6Qd<{MX%J*CHRV=DU9NQ_3yY)>A zzl0vcK4>9^zKo*HjWmlRgyyTi%Fm5t!65^I45@M6SBFgu5jerD3jvDIw~G8+Lg+n* z!*Oug4wVEJyH(YxQnud%^0fZ0d0HH&aaJ@Onp^wSZgoFwvR7qFOtp{hS|S<43krXll`Uy`Ny2ow9+vhfRdHHWm(jaLE7Q^Y$@Cx~9`7vX|Ve^EzppR9;?Rj)B_e|qTs z{_PvL(u4GfG+}jt!A)^w3r4 zU0>g51eIO&y^CPqhLuCj936ExtKBK-4IVtl3m)|a#K+|5{J^dQUxUeB?#@t^Ss~gW z4*Y62cwVRoQH#AYptnkHAPr|XF{QD-cG?n7Y@+=}nW@`^@j-}@8H}WK_11zOB-1@l zS<%;?ukWq)Ckua%x((i2 zM~)HJ6?_`CF-e~1kaZF3Uhx;vr<{7~rh~t$*YZ6wp}(dkRKCW)pkMuEy}y%R{Tcez zm2a1g*8eR1>Z<-~dh*>nD9uySf>TjO?~6wZs^04ZdG*@f9(~3iE^l5>HG(#i@6&j1 zen(iCoVej3FSz^c=JgwMTy_*U z>~4s2H%AMHF>$euj zlHcmt-;I+6_+UCgmEYkUU3#J!XrAoN>)0n>4Vq-Q)}gCw0BA&8y#tr@6mF8Xyxq1|*gufuFjC#z47jt+;6@as31&*67XFlF7G_m{{;Dw=gUW-iF7t%deQWJ97b zo7*wL`*Nweo~{7jpiC(47#LfLcNBE9TA^&mj93cPM4RnOLZej0MsMjQ!O{I6QNC0` z4T`*iF6>Z5{LIQ8_l?-&?OZkQeG&>S;ab7Tr?L7clY}5!+6{_9_5%2{& z)D8N#AKXU}=_!1=j{TqZax@I?M;W9;Z=Dp-OIFnc3zjd3OX3kQ8(MJ zh!xQ1D;?W8gjOVxyPf&2fu}Q<43oYN(J+@g@#1k%AEIofaUL%3(-GqK;^FKd^taza zI`s0{$Wz{{a)TqU{tj3sR9K^IV_2*|ba383(nD6Saossu9=6%ba|Alf)DLQ~Q}Fcm z&hy#P?o(9mtUBD&Xz?#)tcC;TE7$P_T;Lgz#hUu&6hX($ij_x!H{jdh_%ZGn!Zvbfg&2>NN4!8+xy|KaIJzI zmeNf0(~_MbH-_NI=b{zotkeR*Vj#d1O;*DrG-db++JzWjg(ojn5 z(O|~b`Zp8M>3+ICxG_f)g`>h{i>9k}7KGAyi&k+xIk7i8_E3%Sla(>jSgGNWurnkSZe^r^y9*zU(LNjWvuji|FqJ7cRt!+~pH>JdHv%`aVyS~YYO zO(tS+m*YIc1>d6-zuL5oieKSWCM(heoOp?~v{xDwUlBO%TplPpt>O!%uc7!tV3gu# zlorL;Fx$ZQ1;=`>ZJ3VP9(r$>lyz9SWsn44D0jS{_UTjkk1VfDUurp>!Xv;bv6oKs z9ezwKRG=Dm^D$M+QU9M1#YXBh`XQP{dpuf*VQJ$@`^zsyh-ERNgf;j*O_ECpd6R{M zSU^H?;5+RW2|Lb`SwKRex2_i*yd~Oeh}(a@Lbh_~kSeCgkqSXgxMm%xKy}FP>c)bP zfKJ!3w`TLvt2h;d8O@MM3n*?GA^B80xfyo`uAhM3520ao8LE&qQ)mB?4lyihc6;#{ z?=0*UNBt1T^bEl!c>Mb~GL_vu-TY0?M0y!HuRa-@SgQaKLLnUET~Zi?pjZkKS^7&(F9biwa(Px$@WPGiJv!?vI) zZ3p>>kzjsVa$~kseavP?aRueqY|$9h{mXKuy!P|Wq#7?`Rs+PH4o8HeW2kw_-bMtk zp<;!p5b zncrP>3#l>xgqg;R*m^|<%GNIeJbQn#S*NdI1oW1Vhtu~rZMO*NB?)k=W=Bv6mw$oB z+{{KsaBtF%KT=|CdJG0A8Z^KF5vK16#jq7L2|3~=1;F`pGy;zpLeOu3q4y+yop>qF z1Osuui-Osv?FyXc+8@S43>A*q^wwf`bt^6{b z9siu39VaJTv6g^HjzxvA1APa^9p5~BBX;TAcg%MN=!)a7w5m8562v5;mcVFlc7G)t zp0@O$Ovn8yV&~vA)n+_>Id~7!r}nm8pKZs+n*=ssMI8zyv<6D$AgNcLm7zgFG&E2_ ziD7glXVgTlS})VC3Rm@DE2Jkh9Vop~l%ySz;}-g{SLrBHqH%{wbxKJAsDVg z&A3;>i1YzjxQt+?y$LG~V^~Qn;V_bd8?1kXrA1V23h=TFQSQdhU7{60iQ}#HkC@Tg zle|_DpfX~M6~UcLVch8_NGH!r`m6LI_)GR7I7^2Z1b%vfF3>a(wISC+gHblDqo-N6 zMD7lnAEJre+BXPy75#BadEfjYH*Q@dzZzN(157vByOPr%FvVtL?5tdYgv$1zdG(Qv(_bco$&h>=F)6V;vRKIzIJ2P81km_fiZ+~mDiXSgIs zwWVLV`6+~6THNHpizZwb?meJJg$-ekqRJC8wQUOtzk=%dl5RfixYfZaiZvO>Ox*-M zETG{d)spuL0B;8y%=P2j2W42)jjfiU1Dy%-h>;4p&K`qz&5!O!bR&AyvUSWzg!I5v}3Upc&E3CVTgEn z*vLV838>ydk%+bT<4)b<^4G|fyIs|e{TBV|Z`c(vt^`u4#I?PH;*F0A74J2ql*nEv z?y${~+9E}3Y@6pFCVqcv*ipU5X(F3RwUL(=@BjFh;HCT1x&C1a^H zJwnn17|@kq%U@^T!kda{7My}%uY(=qJOn*B-|HrVNFY2MO5 zuBC0X?v5X(QIk}Ssv5;A@ zX#LSf7l47jFN`+%2 zzEqV<4nrIc)&bI~|Io*VpSjpZ3y0x2T!s(fcnY;>(ryZII3(zHLFqL+CFXZNlTLK+ zf&QU(mS{J*+wwK%Mc>)%Q>$}+>a_ff^{~j&eF&t+akz*6BV>Rd9-_DkjoY`@Gn~#A(gyrX&tUgW)^`0Y;?t&Bea? z(A5qFoxHz4ZU+?#VKFqn*JHyvsB;XGW|B9a1LJS+7N|O>6|8uo2gJ+g*5LmV zEK@t@f9T;PhrBA{c#Gf>ns=Z|Vvys1;nCu#kIdm7?w^^bUinzQCTh1}x%tMpj5huT zP4vX>DkO%P{Gj}{mPo|`(^FrVDsI-Z+#LwrECS@S30i`LKoU1;WAH4fZOhJ$dX8l` z2zI_G^}%J&d@pnOBNEHRbn#7vl#|WQ>|itJ$aonAbttb&eNS$IG7Q^9_jRFL!7_KY zLmVR)1%W5H-MQTXD|H6e#=g*&Ue<9MsnAXOB2gQEUuSr3oJCLt@R-PWImmSifd54Bj+=9qsT00 zdX&&?f*Gqv=PbZbLH8OJn=P+Pw08FKVW>glV|*}pllifUHt7d_-73KA%f0M^I{jyt zp;D0W^T1A+w?QE^cU)%Z5Jdd6E>jbtZ~$a`0gJD0*ZeAkkT!=1>NN~u`DD4hfFV4K zxmuJElm*A2A&5oUdv_u3z}9qGRLwq8;>&yxt3Z3V#(8=UywaP9ayk<=ngZ=Jn>lH* zo)WFEmfqMCz^zko|JM_fdA?$0e&OFkKcReW^VF&(#fZtf=gh0|9KdV*<_$rQC)|zENu`UCa--Xq} z>aDKJj?*`=?~(4g)c!mad7s*|PIAO?7%ynrTaW4rM1!a=aa#;H%Q!IY;MF}`wUtT9 zze#)SneDOMf&uM>sYd0~C58C@BreTt$sNi?Je`id4>x9es2bc#adN<~3vfClQ5^z_El>2B`8>X^_9^kG@3&)p%pLHg@x#d6&8eXD=avp zRv4}7=u3L4<5dQHAq8L)qlm+{d_LDSj6EZhwA zXs?))h+V3lU`sGvp=Cdrwa_r}3&6c|r*Gkpr%Uwj?cD(c)}W!3wN`-Lpb&91jdyF< zJCili@@DhR3Pqt4WXMH$bW$nCa2K_1=Tf9JuTVsiB$}amxvoJ|rw{o+y0Iyy+rF({ z2lS{tMcI#?2)#_vFr&614bzFxD<)3qC;P__REPS>r>ELXhOnHONU%v)Lr8Y03Z?D` zEOaRZq>J*N^ z>^q-_fc5I2+6}Tn{wVoz$LhyTPk78u-3rJ2m^|)oloqc9OJw;h_9-`fwVabJ&%5Q1khc8CcK^b*qJ}TDtpXNp`P22t z69ekVRZ9>uAi%>6!1tA_pcWP^3fd+G%VQg6LvonXO*tMGKoP`m{;1e2c?q2ae0V62e{3lX5{b60=r?wTVOX7%~SD)ibJ<(|Oj$$`)I}?2XlOy;e81`)K6w z^il>c+a8`LSg-3NsspyCaHN)(uV{^D2df9)beJxWj}8(wGpzD`vr~}Kv(qioHoFAe zFuQkP`>vsUU`EwG?DBT!G`Xo#S_BgR!&TI}oVhKBzovHdx9rJkRAWaQOH2Bz!6R7pF>I-e7><+K4X9Ar?4YA=8zciba^;(|iI3N@G7a%gqMHoWK5>_yp zIt32|PBTLh0?a$l5v8;Ky+f_^?5E53Z)nU^);c?C>{m1?z?u7-_0#?}IG0Lh3`|d5 z)u7@B&uO3iW2?&ffi`KWcFU-p`XcM2jVz;715Zw<-}iK}M^2zy$XjS{8?F!feG{5V zz1s(IW1;0am9-*UReBF1qRD2rHr#SFE-)B9IR(|)((o+f=j(DU8h zJUSHa%a*+M0oZp-GnPgmMIOMTxSWEdnZOFzY#m1w|5E5QMgtZ{Yqkf;Yve}ZWc7HM z5KrGSjWMHX3QJo>+}j)- z;uazaEMg?3_Bpvm;8xEk%g`oEoor&IZ?f9QIZd`fkaF}%2z|71mqw?x60~2C0^ce~ zfliZ53K|G<^ZgTRw>(Oe^(cMQt?rhZC;gE(ZKHPE`(4;=NKBb1k_GbS z0_C&2{qS_nmKJ9~#ZTTSVB2)y!w{oEq!K6S+D!d&B?mrDsZZZ%$x65emgsw5qVzLz zNwR*DT8O5GA2vSoCsG79m1ap4|3`pe?>v?vDsK-CC*ImK!8O zN!hke(<5*dQMp_ZsPx5<>xT!MgS&K4M+Wedne>awn)k2MS>r#jqod%dn>7OOEzUnC zgFgaEFV~PWe0K@HmN~iCaiwow3l7PDoKjVWDU(x?p{P-liQdsm zRo^=d#>^Wk@%R-96qdY^a+XUV+Hasw+*yp!z2x-g}N~ z0z4;$>TFIV$eQbh)9Y~&rF~;Gc3QzoRP)~?CHX6sL!gIOL9d_uYo?)B0YU>oIeIAr zOL;o{&!>;qxI>IoLB|r!&tu8ls_CK&lL%B`LjkG;xN*TQ-TAE7oZ>ZR26S~N(Qf%@&&teNy}(qVl%saqquIxk9^JV%sZFMC4@N}F zNz-aF-v!{3pU#dbl3IK%^L++cRC}Tq(HIOOY)2p*u@EZV{!{n~`XPn%1#`fzh9ru? zyDe*o$)N-pyvrhmc-*i=@In}lnB2s5%q}@w1Hha?A|loe6Vm0e1vG@=2Ji~=*>6OP zb`?Io@HO_!T_A>=j$#K&ecYGZ@Or=82G#K8HrN3Tiv*RY(yd<+-US=jKF1Mg9>PGq z@o6@`)FM|T@87F(CjM;$?9%>-RSND1`KIr2C$yhi_xIRP+rP(#GX8sf(Qr@ug)Im) zePIp$G+$UlKIIqI@Q1#z02EWw_k}f};V*0kZT`X<%J3J~K(kCcm5WyUYkfHB$sl^2 zNMsXjmREOhB-&L0LhGq%dIXEyK#Ee(^E?sAxeP)h)~R=fE*Oo_hS@_|87>hK2-0x>lkxT+U1yIq^-e;Ld0g%Wk);S zKHeHg;2RgW3X+8%;m(mY_J__h-sm_MH9ujT0-=d7tuq>eCk5*Mdb+Vb>PH*kRHKx| zpuJlAy;LXCJvGY!jCA=!WGZMM#7du=>LyX$MbadYu z$OTp09{E9x(kK(E$rfig(gun|(#?*r)(16gKPdg9dxu4IDI(a7(Z3L+!+m)Da*apj z(ye@Mb?b2jqZTHoQqpLK6!431281n>9Y z3~b8x(P0T4HM%+NHy@fTQ7(GgN)e-469wrZPamygiaH@@o3uYLAp32fre72i>$NKq z`I8*PHMKw5`H}JZZ_$0_U$;wb-e5aV@xtC-N_)!6?_kd%hdUkF%ACpSF^b8_PB$s6r13RxT>47a!FMy)aw^lx(EPAWDWS> z5T$A)q#hn#k||-{N9CJxZv1xyikyz0Z6;Lv29h0V6%I`&cymhazv~vD4pLL|Z|&_D z{c!KkC*k_)OO7=Hwo$A$dTJ&NQ7~K}^$13#n6^c~Vx&R=^z+@UTrRSksnY#qzI;9` zz!yaBpKd}OQaJ#EmvDh3ITTjnIi7I)=%ReOIhZV{3;v^7q)6H!_*1klUm=8qP9+G* zJlxRDTaVyW7QCG_kQfc1tB#ihh2>#mA@VQ;^hHI^N_FSx#H~%e41>yAi>ID?=Am-p zH_7^EtTCp$^CwtTRE6=webn^62w*w3Qrw5k-SydI-L#$5uFwN*^Ba>@zHxNKRjdZL z-96SJd@N#R1OU$`NJWzp*CxJn#Tk9Tc59hT30i;49B2AYH^YyyJSGCp*TeGd>^`0D zmARU3Ahiy-f0#aAE}tb2JJQ^bL}DJ?1$iRuRQ!Y2)ep3_T#q8^To55xQ@Qh=B&a?r z`YO7)=yDoDmtyvyYrAek_mk{F)dmhIH>1R#{#}zgi63qDX3Ivw>7K+MLxO0F6_Ogw z)z-IZv?)M~f6G{qzFPnHOPMnu(}K!@39M|dze0>jI*5|!=scNA#Q-%+?W ze@B4_{GAkF)_9wEWqP+*siuTF{}lP4|G?^vQ!~Ui-$pp14@HA-lYsku8-W__+bCLt zZzGVktJ3S+bRqQmHY&qLXGO?=(&DTrXq&Slz&*~2MzyoE5~(~MMkbZdfWux*!Wi~y z6mq0jBUnvdje@p&H4b6etIufrEu!AQc<{!rr)FN?YjZo&ixXqM&-(C1 z|EE-@-1sRKVERP%e@Zi4-=`E`s`TQl97=Pv!)btZ9&3vXz_P7(@s#@X+VHnh0n@2J zT5IHHJIFXbV6Zpl8??G!{S>=AqH8m2izXD{E#F+cBO+W6L==$MFK}bOyEfH^sDizU z(f;kVS4LXn1SX&6kFU&o*vMO_yp}IDuC|4)0weuuvR79BMEczph``1c^73_$ox3~i z+*v_{b>z#R@<=ajVH+Y7u+PfQxUaaqy;8rm?qp+vZ~5Arj#-FdFp2@oD%=~&urhqG zh0`9%Fv&K=GHmX5(5?^BwBC;^d}e7MypR^CeXVX(R?gb!y`2MC1$%C zJrj7FB8~gA!znruA!M8iMsdA$>#ON5KMftRYvNDngBbQyQd3~VjLgpABtHD-*Qv%h zShk$F(h?c$4TTSE_cugMOpROOCN|GrchA0$Z4RGt`)G}zvug(B?6jGLLHE$yvms%9 zkNTUSS>6sz-@)YBbfs^XqWk@1zTCeLGx7w_+%F^Ddb8}0(l+Cdr2*b)N2Fw!!WE+H zp(5GSNsJ1FO2giN3ZnFv(w?!2>PMmUm!X-s%TC84rHSn9;*N>CKId}M`g0e_pQN!f zouG4SSN}dY)GN};W7sv}ZuNGR7@isUHnl!4gs_rtkt1dV7<*MNWG4Ki14K9mS8k*C z<=zaQ;FJ=L2mivbpyL$ z49W6yS0Eb#@B$R>eF(^XOM-jH5Q$|~y6hn&udg}Z=C6nF{6L_=XJhv3452n% zbl10YG(8NF`CWh>DtY7( zTKZhoG1m~71-o@F=`|PpW=TS^G!6RNo7f_6i8IBjGfl`C&m!eH-Vkp%1ZG3T7ghsN zR-W!o*8wP`%`3fq`U&Bgm*VIX0&>}K2?z-WpP)#dyrIjZQ#crZt?Tna2+O^r88!sr zNfsxj5RmPf9hX8duJ%n|gb<3_f3fW-jZ**l;H>oglGsut#+R*mHD9iV~(1x=e!A0Ueuj}dn^T-FsKzK+eZI;uU+-V0wrFef3q zB+&?O@CaRBmpVSMPCQ2YPs zNqlGd^o{Uns_jHKO;T$g20YNZBh7o)Nj%m0WXX$VU;v z13a9Gyx0D$WbK1zHf8uLq2&%^tb`UZupiKuBBEj{{e?(Ox{CSJ!-s_kT7{aShYXdL zQ?!~YT>}r^dbc4fxaCbmv(bImV8m26*)fc$sbQ!c%?Mam(!9e6PEA;H4<`XJePHK^*o|<35CU78LJ55D395TAkKSSOZFrvZI$?(6 zok#PY&1zAM7EMD76OwCCCtG|^1(c`w2A660`O z1(|2=X-fSGZkzduz@pITHcjU5*<>{3dKj>n^})o~Z8@TLNlzz^>oZ(17Oz1$Sjzw# zX5p-S6G#FFxnqu_M>o91ushsh#zgOcsT|+N-RmdJw_FBXbX#^b&%u@y3`9f272s=V zo=jdxM+_%cn7%w}L=8U8@^BbTTVxLdUec;tE-X1ZLgp&ozs+3!F;b%c*_7x)vZ2=o zLC;Bb($>+-QRHUkgT%fgt*%Tcj>xOYkJ_2;uJCo9%s2Un{1ckO-?qo8F31~@50> z^CF|;fF1h!rH(2OcXFcpU6Y%Rplqlg%@TId7kL29OBw>coOU5aa~oLsUV@k4g?5XV z8K>R>)$%*$UQhUt?}pkdP_hUfhYx4-p&_KHC`QX@Vq?;qKm4T&2a1An71guI}9 zA|KL_ya-NUp-!@dBc^ged2HWUS?m^(1VkIbbb8?^w;nxf$W z=#)NURp09dFAPI8Y4R+Et34BN1WPV!v*M65puj$(!U>ZgbxYv8fpowdv< z(-GHvMO|BfZ~zWoY|Fh2>O1B?pmdOJjN?M|;b@#4rovAf25Kr=6-`IqF>@9t9D@^M zLYakVoV#TTp*yZ+P9FeiH6V~^Y9B|qv@vsvX@stk`O;8Dpn9Xwv2GEDK1STE{(74< zVCwaTz^zZj!#n!T%;uw-K!j#(m+DV#1fD*x&)4XY?T)2)=`iwpcAnDy4>wS&w>%hx z);0kY#=L`+bQ|9h{Xjzk3U=$O-6@iemJ7|5;h!Vi@71{XR{N8MHm2wnl^a{znOIs5 z3J}pE34U~SZ|acJ_qs)3^7U;Mm_QE-tOgq=uo`!~zyyDsz)+M~O0vA>7Awh&}yE=^b0*I+DZ(V%ZXy$wC?I-Scm$RZN9M zO?y^bLfx1zH+%Tx%5m?;)5&5Vr}ylc|CH?A@0-0-^H5`LC2ct*@|uuSL8o!DBJN1+ zfT@k?IP#oxT+qKcJ;dD|8fTSGiyMX8FSfFD;n8AEo%M<_Y#ANhx08neA+D%b+RrwK zQ*8<89!d4Tu#ncTstqYnZZ8lBo2(Im;nxBv0y9MAYT+b|8lFUc?eiTtk`wrzCF3?t z$1s)lq?uwxp+6B=N!BIEk67CeD6qV`}NwyPCCi*BW zFIU&J{29QZ|0>zG9;qn$^<&nkaPCBA+aI@A2UE$i2!!%pu50?YfPF{m+sPQs`>s2m|W)1MD z-%OkObWahg>tM&Pb|N$++{+DVK)M+fgDNtQR8_@csuIEJrDKD9s$IuaF;D#~7P{Ys~cJTICK+1(c5sP*?>C(rXI#?jPGV&2oYot*@TgAmNSEhY~o8WB? zJHrPpkECnq)g3YTPWhpckGtGc%3C!N+WRG%ec$=plypBrtsHFCwkE`1FUPFpIrf|Q zUP;LW+cNLBB+NQa#&64dPfdxNZ0pA%RyFrX%=AD+H=F(Th+^a0jNU{0^i2^^cbno( zi8WtaZ(~OM$>D%~P>3TbO~38qK!+$P)$^s>;oc$gCxK9Gpa_TK8Fwa|#qQJllcT4h z8Rlmn%6(>(`}Z5)A~eEs2*zMTRygl5gV8MeKlM^nnVj+_vG8hbFD}{ckT1RjF`UA1 zhZsMgb5|!uJ{$LYWPI!r1MW|XPGu#(RG;J`8JHc2R729yO24IlBGoo!DUWd5zsE<= zBlONrt(PkbW z2zIFk8}QcNFlbq}VK%PxtWP8t&7R8^Gy>F02@_xhr{xz0*DGkf`5IU|%`MtqkcbB@ zyxK;SXXs@QLHV$L<+b)%&ZQ6)teo@5I59>7ApHcAUf3;}y zd5cRFA5877Q-Ml4+5usIe&$=>`ip<{FZ|W%NlJZ5$%(x~>AAFxuc|jx8_LDfaXZay zsC>o5u~$%#xmV)Avzi3mG;waglL^;IdDA=kT)UsK5<4Nhq##rbg|Pf&x8nm}xvuED zt10oJbHhp-|*)eMdB$V-4nRFP-Q# zM%X51AzTP=fQ16GN6d(E(X>Qs@ERCt3?BSi;TeyJz6Go3Gf!FHEgEhGNPa=1Q7lkb z(2rm(23WxM%$ZCQPOnSUQd8X~+#5|Qi|EtCkYZcDk=;c%s7a4*KARob*DDk`J!YCP z?qY=~gd+tP!?={|m~hMVoMBALbxfG7$mJEeQ{{4$Qb@z2oW&@#4E0T=BTpU*0nw)e zXXn{&C@SvoG_Y8N+-wWEsG?O2e0EkX6tfvJ%1%zLupul*GP?F?QM=O&OP?MwE79({Lg2xQjajI(MCZZeIoq&k zunPp##@A(-kTE6>(>96IwjMAl9~R)BOs9K~@vVpBihdsr7U}y1GW;E^9`5SP-MtmA9j-=%^LoeXj=)-L#wN&Lw|FAjW(Vr7AcbWD{O#{ z<-0=)9syn~0h0}ifG-Gei{tTX0Xj%F75~VRQl^}7P&vDklb5ALP>N8?W(N+gcrtUs zkAUhYqyiIn`cIQP{d;4ogc?J(Y}jmqSOyZQSNZmM|)?CRJg@oF~961cqV8CneAjcLr@Ny zo4$u&(xtJB*%0MCybd+VH}Mk5tfyWG_S+rU7zcMUAD$nj$yLhdnI9%WQ?5vmvEIQOk`%~I z@u_eLUk7#)P}9EU<~P^j?(vwB;$P06_DV@3c>OLFnjb-GET@`rzs5YbA*dTqr@PN0svj6I;_Ub;;3SRvBYJ)SEqDCh zaRX@{8xW6hpHrGt2dD)n8L*COJ7}$B=}aPbpqlBG2}je^x5qMIKL_%mY2m=qSfJ7H zoTKqA?jT?t=SMd)OGJTcz&@365C>^bS=~CAqNsMU%N5C3N>~_t>-O^wbBLD4m8BA~ zpR=ZHt$PKCiwQ~1Y7yc0>6QV7pGBy6z`S`^sGG9yXY`f$CwBXIQBD#1nN>8~1rFFOfP$B&4hKp>INY%;r5eUvoj6$X zLlXn@30|yO#%4`VCv9dHXS&!o{jzrp`Evpm3#SDkfz<_vPBy#sB$MfYRKkV)ZO1B`L zRFAIcS9xc4uz^HNW&NC#^`Dxu7Wy{?s_U7+ve+HT??N?KRJdi*a{y-;0GDFL$J&9U zd8WZ4Qolr9k$Q%Df&Sh*68BXamXqCBSvguM{Xb4Z{U6Ow)XZ}5C3$s~<8D)NNNAl# z$9EtV?P!`X;eIHlc9Fq)Hz8{BLNQ*?r1Xl4a=fOi8;GLJm;2FNE|s5Ou(IGrk`Xj4 z&|-?R)#-K_p_36BsR?8{*hVC|G;NglskrPEvVU99Mhc#qRA8`-CE2FYY^W&Kq?q@e z6Asz@dMR5$(Mvc4rC5xtc?hDfXkkJFYVoQh%{Hmf3$k@UG?L02k~lX)j&IR3F+<(7~{8>x5 z1AIhJlVgg(drZz5@uVJ3*7;QTC&|?QbNhZ-Wrnoqy|w!rDFq~}t3$DKofRd3S&fxe zpc27-LIeIowe9k&4gKnuzMT@r*e6LXf~ATe9Q-AjlD2g>DgE0-@qe-phz6Mb^}B=8 zGyR?AVxL5h6qa3h84*nlC~L#ar?bOD0Q0K>5r!4s4DeyE*^Z!1v0AMBU38gvf`D+u z0(H&=*rNqG3)FM54{&F(eExVcpFg7TYXr!iv3Ap!13+FYCtr4Wbfv&V2*(aAy-@*A zi@PUb_z;+trE$p^_Wo@5+2-($y8ZohH31v9BXmFsVYROb=ClJnpfD4xNpbpga?oQ{ zBhznjusywkvW8o%O##ikIZ`uUCY3(JED)8}&c_9Wsl5zj8KSf|2~L4?fA-xQNPtY4dk-kQ&%6cQ@p1Ml;l|u*gEIXBffjF-Zj-LFudjwbhf{%}SHq zQ3kt<@7#KdC&=?T!ojKp(%lH96;W|W#DoT9hu1)}%Wq~;sX?m77b*V^eJ%c$eHw6H zBWsK|Cm`eUA=V0Ya-vU0w2N8j4vE>IhQzFVS}TA7nHgO6dfbFGG}*Rp!e;Bb!LLsp z_i$Fl!EP_b7b31d%or6z$mrz%t}o8qOg_rU54k@x<##tP)CVQ@`q?O4%d*cZ+9z+h zTQ2s0Bo6Sj1#1>d<`|A+Z-Nx#7;cD_s}igo`7n}Q;pcw0J9zGM7%`_6e2F9{x2e11L`Uve?cyfg^Ns_h}5)1|R{Vi;rN>be+oA`br z&^0QB7>Z+|k<~^(6T%TVWbZ2Mv0TNUo1V0e!EHQiyN+J?13Dwbz5TaMPPOkt*2s z-NGC&G1fY#19AN<p zr)Kt%$ncG{arf0))j zrKU>~fDHN5bUh5Poei{gm5aOFBxyy>m5yXcifll>pNu0&!y0U81}cgal0}c9Xmr|~ zO?!eAqylXEbW7qSRYZ4C^BF}3DB2LhGQSyC^rkFoaQt#Bq+~>rG7cyQ2gef5kU$vN49P7# z=F`IsH$2jfbBp|={&^~7@g)i2`M$4Vzi}t5bbuToN1kTF8wwb_j(3u0Pf%keA(LU> z{{nIjGBdpjfLMOE+mTiiknMm72=9GqlXGbTU2Vz;>HUOMK{svT4-%@lb=0YL%jibK zzL#;=&qiUsibcakf;7S!Fdg(Z8G>9pNEh#kpIxMIYH`%0M8aH!z~WW!&IT9#7|{Py;4aYd(rON2{vOixiOk z?pNxoe~S2;_RCLOQk}Sgndn`6LQ3XyU^>l8{kG&VY{AR^h(^(p*$%8C)5htQ7T35a zSe1nDFtneQTHuHmUFrtAtNmaqs}WIEvA0@VO-pLq;%BHD0CBW`|IbE%0MFNcjM;r@7b7!G8 zHZ*y(4OpYvgFI_+wvlH`Ue5JWrVnmOXNZ1!ieXhJwHEG4;U>30Yy`A7`7Ud}h9Rv|3+zaXOU`BkRT+FT+B zS3xto5wtehM6;>s^CUH-F+C~#Kqm*LpFop<`x#Hr{e<}fH-Ux(wYwPY?0#M0judJ* zAlfS3%}q)2WbxnZKv47 z)iUZ1_=4nU!M<}{8q!E|3abOvTfK*~1HRDXGebmHjap(E8=`!R*AZ6WBA2y~VY_!4 zW>`R*7$%d|#w|g%u}Tl7vjc&tU6nC|f zSwvQ!vVc*_CHvxi<_>fdG9g#Zgdi+WH{fGXC>%Qme*2+I66uTouieG6Rx*G699aP zxE0`}XmPPi!uz2KkL{GE$I^)@>xu*X4tfwQi!^EX=N}<~YO?!OT4YUEdKMF}0^DMS z5Eyv4%n~m@NPEQ7?^}(*`$DUt(TJ*fIyxa7y%D~s@_O7*nowP=!7$FR%LhK)Z_trz zy@rOaoJz9-(N%)Q77LUhdm_h#Jqu?e%(7^t^P$HJ7GY6uC4o zX_?GyXBYQl(F`u~v9=*mes#^=cl;^%alTrx4d5iQH2P$f7fU&5!)-(kn;kJ4%T8`!?~{7lojnv5!F z=9l8Jxbx|kv0d!9FY9kokCq{$+^khdA0uj&S#Czwveyx&gE{UnoE2UhT$h+oya#xHWF}8Qf;$qL*!O zx=%?jo99Mr zGGg`B8nxGWQVDx$JR5y{Y8{uwX122t``6{1{|$Wp)1^#gBKlv$?dBYgVX15xtjf=uDS4Z<($J|Un%2~WZxdK-55KYfs)GUE}Cur%ZB?dlS+xP5_FABw`DOSm3 z4vxGjBj!_c94z6mWJ{VsX2}TY~`(C4c2NLgk;PlJFa@m{h+}OGO8Loj3LK*2t>!+XWFsn}| z#ny2wr0?6^>w}>BM2;mvBhVyl(Cr7Z9FFf-%^lo8_H|q$Y}+sg&9)d?745}BVB|<# zpR9J5MY4rs;zjxB6LdCc4kq;j)77~zSdq+<*D=kRKl|kzK!e zQtwa;FNPM_1cZ-mZDvA#%mXbpLkMj23Ai$QA&Ti7p@dMzMa!qn(yu;yct=h=AziCW zYWx&Z|3H*d4t%6vE87fa6^_*D%pb#tK_0D$K;#X<(BzixveOavs?8>bg5c;^X0>Iv zF=guiG{@q}#hdz7fFR_9dK-2Vj>d8ol4FOy_r&b`w0w%*C}FG37ikr}AH)lYxDXQO zP$9ftKQ9xD+^k*&Jj~LMH~3-=!M?onbaR9*5Q~Am@)ld$k)Or0j5I*`Jvj-aZ16FT ziePSkKqEowE05an?|f@4#t?zo1Tu!k6T1N9i)mcTLh|E>g7kU z>JlX3C8u$k?*k;(Q?3LJ_orrp^M`YHvqIEr=Lo4A)BW%?rP7IcJ#Sx^+Lw`zVx!zUr8kAFz_iRPrr)w zX{(xO5XzfFs2lkVKbanr?-T)YJ~A2_qJ!Q57ZfMbk`oSQMik$XgU9vRe17lAU98z^ zhO`xF%Zhe@hBhM!-ZBYZ0r{~wuTwCDs+?}4nah5W+7k`(Do)ChlR~*9>xWBNL4*B* z82aDsc>Wq4&rw0QS!g=L!{rS$Y>B|lQ_1urQ1g?BUfu$J{Nxc6?WI?EMuSMH$0*M`!sqlqE|LY!I+mtWh%^S#RzcW>0M zDuq@yJyNiG{Z10=%{*K`+;WG5Z`UKb$=X?>W7u%IZ|a;na3Tr4NO@-SFQjuu6!x5zJ8=+wmly_duLP!+HMmGtR zEpOvc@jQZ&`K;-XaPHeeq6?uIBMnoo1(#^|4Re`a6`42B|200NHG{utUD}&bP`taB zOiIhDpD*D1O;AZWvD3Hl&nEwAQ8v0bpl`$H_F+Y-AFU&Fpa;ko{sTq)PMv zcxQr#X^V#W5jACl3SMRuxE~KEP4Ieu(Fo1`q}vEn ze``lI6>>+s^Ex=3yl8Hs)U(0CMs#1kh7JM8znE@~za-iHpXM2zQ#d^8Ib9SdA_w+@ zg5ADp13CzMgJ6Q*?fH7sL21$6wLOON7w!&NU#L~!XkfSkw13-3!i%C3sU($$)35TG z!;MZ2cLs*019#iFlyH#-Wl^qYd6nJ3)EX$k5`)4rE?6Q7kcR5|4bdjhJL=Msg7mOK z$-xd(j#@cv;~3|4Sqk`#GXveCN5^3kkjfmt=Q3-&xLoIz4)hr~*!A&gEE> z5SHe0XyF$yKG`gA$X?$PijN@(2SQQLDv* zouLWSVr%~N$1_FO8l~G>nc+y z12a(CDx-l}(^GX@IKz{ETNaFNbf+18XcZ-8GjEFopZA`Vt>lDe1fA0$yjzSAJ~8B% zBSheGnmpkle%^vl4rpfv2A1TCAdOnaD|Qmk%q8hYimMs!+x!h9IQd2hDG$@@WCZ`7 z&KqOs!hRln{cty4FIvl+Eav2a8$K{d9fonA{y3n?>ul2YUPI+JFhCp%?xrR{yw0I@1z++jXkgOcwy~LyC z7icfW2wF0f0x?ENGK3dP1g(iZ75_uKSwDz=rSj3sX_=mEvq&RmG$|$n_U{jkeM{nq z(X5^#ot(TZpSf%MtLe1r$l>5LMm|7HjPM{a4eNcnTpuB!C1OqQJ9pMkxl(7h2NcQP zk{vL}pMVXFj0;hLLp|sd+}A`L=77Ld(wbbFy_2DFDeoCaRPDf=E;u5KI`1E`VqH)EuqK>2n182C|#!>B< z_$XU)JLvNq*~1o@@@6k9ZXBdsZuBn-UvBh+T%>w|ly{ZtxTCexn~(v0)kMjm&8eZr zAw`g_PVqxzh|MV+W88g7&hJ@ses$}$fToHJDz{xM@x*)H&yJ|298qAF*97mJA#PGS zI}U|XURs!-zV~uC>gC|PY=s6$&W%oA{;a-^J?(=zEKk0<&%>4+t)~r@s|vesE3Lr8r`y{Kji41z*E}60X|QAtS|`vai6eTU+vB} zRnzR}q{hEvF*@q{U#1k*WQ<3Lb%y=Qr_pU<2?RCnNEs`d_qE$X` z#!BvsOGrxEt+QGkirx_B<=0tNL@G;)wXtZvxUezQsD+UUGN>`Jy~GZHFgw`)D{YO>iwIdBepAJf)) z&9;`F4nC*%E$48zjHHJXrTb)vo;v7I`;HYpt#7!R@*V-ufZ>X^y z!j&%1rud@%g0|&fHoqpd$K;b58i1BBmK$w{ldi?(O%e^CN5CJ;Q9)lfP+5{+hibC=+rXkLT{4+#gFA+*toV7TOFP~ zy(~aGe!3`3uUk3#e%CXOWck4sWV?RwEo`+2ZGz!r9=~#1=f1c}NW-`;4a)Yyp?pMw zsVBZYoe-OgfNSh}LLnx57d2)G%PxyN)8djf-DF;Z8#1g88rmGCXsPP2TDwR?-WRJ^ z&#snBgsPrTG1&<6f|#1?$JFHtUW$}}>8pi*hRo&_^=70L<=J@n`K<30cd>WBzD)%?M z2lV6pwf2D0__}*Q$b2JvKx4J1R9*_ko>p_|%S}ay`)D?g7U^Sl9KWlZQL$s?Rq#fo zou1Ixr`$4h!Tk;I7pmnx^=6^uuL@bax8iDy+=gb6FcYW!>hLoL-{>cYUYezEXY%SA z=h12BiMyQ%Rc7sJdDsZabW-H>ysu*k=*5t)`zfTVpKrtl&~7-T0pNH6*K@()VP^;J zf={t-_==LdUD}cnYcxG!N4VJ0qmVhLquN8KKrgq>D0I&9cHgQI1UmIo(iOdej-Wa{ zkzh^7mueIhNwnNNmWUZEh#gcKxAgRc`~8Tfj~KjDFsvtqu8*KMJ-LiS6pr8fxM7AH zoF5cwH?y5QQ6uJW_=}>gUdrv%&LsAt9Mg*ZGzMEcuT&fAH2CF?8iTKWdfb+3r_i}b zDW%o#sdlnB0_o|uV^fVGqj>_~i0RjrE_TEiPF_yYyQycUI#YKF^gx>+CwhN-e$MBj&}T54@~tY6a4k#&v4~l6h$~Z z>vE`g#$3c3+*iaN;XK@3KBiW*d(ZH++!T?MN2~nOts8{=Ih4b%j!{&)*Do>(UKXsB z-GCfXPNH=@L`+`=RWomN`rte{9Y-DS&XwRVnQ$Dg7RMd6$J!hbtOW{)@`rP)6x>aN z8@zKJ)SIRVcJAloDqWtM%1 z{ymCm{bzPza^7s0X7uG5I7q$mByu}d`(X#CfkT4xHOvlDDc*&`uaB0CJQoM2kxXT< zcW}MSdB9KFZr$-ax*B9mV7-L3cQGX%Qlo0y65S*dST9L!>Pv!($Z3DzL7AtlG)cHL26v+rq@E6PP=#VO(!Tv#KuRh7ssq)%l{k*sk zawAgV8-;<9DS_lGI#UzPXeECM(0$|4MHT&B)+M4s);UA(!9>Va+i5(t# zTx?dfk#p+7r)dV8gTo?^$gs7$R`MLlOF!N#LN%iWngF8gRYAnlJ@ z8iCGi9EXNdkO_6>!zZ;*2%0Z9G%Im7+GeHqWp2TurBXlYljR(bcWOy3mfiaYxOfKwvb;tzdB)r2v+aYCJ(Fku_&mV6CJPGX@Wq%OjabNS|-*HY@m3M}1mK7lTQJNJGf)U%QF&QGU6L#vO|zZ;c!%If}wu zA3-!I;C}N~oX})`&}8obo(JM1BgFQ)AZ=qrJLfV$DX};SMp+AGtzQfW9#PK+U)L}1 zFOaGIJ7#J>vV$)oUTgG0N$K?b<;RzyewEi@f9SDqOjk#Lbk51VjpFUj)~lHt&VX(isD%%@Z}?Y<3;M?97- zJf?K>B+90-Y=*ih|8Hm486Zb>q>)zIytBJntuhipGD6UckwA!OBq2Z~fh4kw=$Yx= zp3=748|A?x+9rr12z~3;I8WRd-Y!T zZtu+UZ>H<3di7GhdiBy+iZwJ_9KG`Ao3q6)QvW-RAYd%IihXgTC9Hcxq;gdV=YF2AjkWLUpmoJBk-L4dXkIRd^P$OvNs8$&dD2#@e>knhj48C$j>lQ1p z_hscfS|co>j$AzfYt7{#pDmNsGtwj>QqVXgiK|FNINL0^RsBw}m@%GTi!kExj*%Hh zD?#)Yi}DFrdXJrEccf}u1);00-~@}9_J~4!9sU<7XFja5GM~hAis^%L`1l+y0)(Hm ziloTSvMlbZyhdty9l58H0XJY@_yUx^XMiv{zspFs0*^k<8G|miY9e zjSmjs+dSxw()Dc^;!Nqo%MlJ*f~>h=(!DFgz@uwK4sZ2p4_3H@HHrr2BKlJBaJYdO zT?aZYJDjIl-%yXwHm@u!@QmEy9tyl45benptGfh(!w}WT&dZhKaMk@wF72j5zx{oR17xERTp< znkRo_ks`~gW2?>{i=Ipe6vT*Sw3s-%I0wOB#LCeIp0OCXZn1VL`c@6quq~m-JdU8l z8N3OuTTxuL8M@Z1HgdFK!Q*;`9eE=x(%9wQI+11>m;APLnZ=j~f9a5pnNM!GktVr% z;Yj?jt{aydxkXC$vSTWq5%r4{lY9+pc-@XR)XN91jxJ_!C>;{56~mWBG>LH<(Ri}j z9*_?|Q8&O*h{5NPn|l|};;FyX44z3ntDqiYIU|jNtZL!STKRBElXv2HiNZ&Z#2raQ9?7Y(7-iI2mSJ3Bs1rG5e4jU| zGppNHLk-3Ss)}a{+iF;m*zEIL(UI0Pl4lZ0j-OJBuo`Y&9EWHx zwGhV8W+kgDZ;psfad-&`V;zeR&FQsn zqxu-3%}&)CS}5AUc`=MfoOPl#E@_y^C5VGfMqd-xxoa~K63l9hYKO(l*3clf41=ep zgJ~Tb@Z5SJTnDSk8{%ysN_s#pSY1*ybUrO&n@&fHLCp;`FmPa$eOMPLa$t z8qdiA98Yv*t6SPx66|Dqac4n)Cgs%7nWsx8u|S3XK=owi1RPwNSX}7oSLfS4!Y6!7 ziab>kwPS z6ydvUbXMHx3e_hrsJU3};Z)+Xh;w9G>ZvlDXvbnI)=&;z5!`5wYGMuExX1d%7TBaP zY&3~o!A*h~POUC>>Im4;WY3Pl%K;&-c;z+o(y|*KP8@@b6hwvs-Cg7O)P66T6Rzs5 z<~;{fATFiRy=S?6$LNSRP(>qV8+F!#j+1hrgY^fNdI8PIxA2sohNU%LZ+QT>>X`=_ zA}}#{7&zU$B0zce$AHGAtYZPoirxU)(P1kC7m}(Eva0k1c5t8A%0gfYt8wTg_-diZ z;zJ~fE>1$c#*1C0s292-*c}*4Yqfzw@*rO|y}CoJRnb9gpvF>?cmCH}L~=e}WBJIH z$1!JkQji7WtD4(jZdq$?)$F#m1|g@pwdOVuKK9&dzG}G*`KvYNd6v0uRck$m5~?x( zQ_O^_cZhi=e9VP5Ey`W3=xCQ$jU`4K{iAyY*epf5wZPVBUr2=-%aTq&9F2~>t=DBE z3Dfzn0I^2;lycY9u#FQDMq1LVUORMvL8AzrqF5EHu~x($@mkxj5L{zf zixU7N=2Z_{kunrZZki<)C$1(KbE$~Ylent2X}LMC#sZZ0^w(O1Qh1GpD0jx|GKp&^ z(!&~ap(Mm5grZg2IIm&x7tyHyD3AJb=nz+J;H)XMIueBjcDEKN_3{;YaoQ6nM(7>l z!TUH3sOv`79wG_T1D^OmQl`(j2)uHZto*aT=JuQYO_=8oPITZ>N> zBNpRpl7lsh6(1NxY*eEPr#%jMmw3vlnlpKsMsOLp*uG&#`y!=G7cbP>vIx5tcdX=` za&d_6tu|i{jL7XEtWekKqi9TQ?&TV!s)|uMOvUfUbJqnA?_FD8C_25F!Sxw@PN;*C?5l`y0REDP$K58l6ub3>o6gx} z`^c2D3MoDh;Won+OsumDoHMrt4pq{S3-8*=^zy9UZG|J5n2^LI-m->SVs+YJHDfv? zrjik%1tUiZI9{RPB>bf=IC9%zog|2urrQbY<$t_C#2LiLILeM?7c!h)2c(rGzsW8X zQX#|QPJ%3x+n8*g4RO1`V+cv;wUfn6iW8U&WhOTeIqMBDu^Wb%*nva~E7rlIIkf9Y z7wQx^i&0Y%rItu7yHfT_DLZSdRk>5)8m^iy1DpwLj+QzNt{V|(p;g2dq=D1nVorX# zA<;_VD=k(bICHC8+YGpj;gsQ=Q(|N2&V(zONQG6b;KGOIodug&r)DJM9fPR*Y-6$5oLyFsPNIL1FCvHm&dfhItl|{5kqJ``4hS}K_USUXg2x4KIpx5rX+%0&W zq3sG91@*w24C=ruB&bhdU#8nF^feXkZtxzX>XSvhYFty<0fi3dq5rj3k+%kca)0#XI9EJv$`kLGp0L-kFD8OAyaTL zGfpW2T?|bOP2})k0%kah*$Y}3(~kYHWYeWZ5kX6!ok1=A9)`vvfW2W71Df$Q2ODoN z_j0-^>;qGo-lpkRjwep;zA#HcU=IFLWxF5zN|mACp2~WaG}amIKQ>MD{_qPF8T@Mjd)4UU#9x zA<3MM!FdGykvZEbO{`_Pmq<6GBjL{q1UZBGDELHu$HGG9zDluT6~*y)W9V1~C6%@T zE6L}#xLA&c!4Y7?2C-w{Oa+6p4P{otr>x9mQl^B<0yq#3&(0bcH(qu3DU8f}@|09) zfL{yW|JW+)ikXlQXV%u(O{23a&LdmY@yDqoD{EXz z;C>bEU<4{DO4drDD`=x%qMOSpa5uvyrtqd9O0 zhG5Txa{&(vKhd=Zahm?-#-b{+<`#WrMj03X%{sn6E=g{&%2X0E6$}fU+tAKjGxWNQ1Yo;FL+Xj&H&l zlGhF}7`4PhOSlTEF&D!*jOhqGPuzm+Wh*q%C|6w;K$b;5=q%?8QYx9741sb0vJ8scnk7vEt~5yWOkc5(R;K z4Z&B#x72scA-kjwttaIGT1BZjzth3onIFO2WsfyeP*Fq`P? z%X&#WOW~g^U7}}9c&>#Tnf+;l{W`c+efJwM;LwXRR@UX;;I}NhnO>O8BSN$$*&eE9K%z7hiQLKJzhPH=Cqf9r!=Sm2S zYtTyF40ZS~7$EA6?>~SCSz?0PHV$oG(ZUFg?qjz=tK#*W8MBbJx)nN^-cHe=V%{m| zJiN`osMXtG3WMr0o^H3d!%QX!U%;}*20w;_8AVD9&_Jf7Ti(6! zC|7nT$thDPi^2{eLH`MlGpq##(M<~>IrqUDPAP$avP%~MMs!QBE+u@KUxJdnxf z%y-hvVZS&`u)Oi0<4pTn#C{bKQ5nzsLfOQb%5Y5N6S7W)4fToL;JI9wI_|4aC` z{O`cE_=_XcPr#cp#X#_X!+Y|-1Dgdf`2Hk(s5JB2$kyTTlMCC@r{HFph7GqGtiOWW z)px%&Ygh6qcOY{bnp!Tcr{RZ;Xhv@kuP^G!;_`timS4lYOmC4Pwh-VM*usFeK!DZ1 z5GQb2##`oXD zMD^WIWIb<#S1Hrtk!k;4g4dNytnvVZcF%OpUWT_BGCrq=i>>e{^&LJmlKmfO=49)0 zcrme~dHxqZQ9OPt2As00ggKM1Ks$4H2F0IFW--Ex{oaUAy$Vf@$_JS`3}%J>rPm_H9VT<~U2;C&GZOF(=w_Ky(&EqxEwqg&^batNu~UT$OsjYp z6c|bHn=qXi{jcgvW{ z{srct6!$!Sv%l-%~g@of!q%u4o!$S4lugmpmf9BA1S) zEqeU?1X2v@%BdHm(5Iltps`QE{TuAX$xIIcJq-V1Ffr`C{~cVG+Z}=nq@!O{cmDx7 zhK|o|;B>|0x8?E3XeIAhVVC6p;B1C8D~PTV z*oLNv7cis)z1RUfyvgMBU8P3AU%F*U(7BgH;~j2MCkp3{6BjT#@`1 z3|mn-;7W~7xtTCWD-7Iau(uHDSSHADseu~`(}?`J9Hxb12y5um zILQ%wT1%fM$WM+xsgx_BrAN1^$4E~~oKDzxLhL8fr}vq?iP>4p8HC7iHYU$PPsBmh_?e>)O!Tb9>~>l5VA^@Kok2{foQhaC{E3UD5g<|=;J$sm1+NINT% zKcSe^J{jyp#2zL#Vw^g(E(kjlV37jAVngr(A}vuQzpdz`#r}w}fL#c6rc&DPV$|@s zBkW4pa}^er8;My&x=4}ymLgssj@F9?R<5=l!mKnED&fU2bceUGr~L^B`LczW- z+OvS32KxT&`@)wAyN0niqDw4At&9eC~2be|&m^|aN`sE^}_{Js*3g__9;gFN8aXm&W0#}n)$1%pqF1Wq8* z1DwE=Qr;V|f>^GMGLTcrl1?J{XDn$O+(WL4>trH5q9nltgS3uF^=iL4GzpS)bG}Nn z$0S=YPPZW)5)$q}r+^2fQwaT}O2lt1m2$#x;4DwK!u14thCyuv%H&gGRY9XpCDcrr z3a-QvYGVZh<|U_G(eA2EuEk%$845>3|X`(&P zx=!#iX(t%2#UspOQ3yO(KBqO}Y-P~!?+S$dn6Vuq*wv#sxVAPD@E;89L=PB+j)W`{ z@-s%(mGit<4<({`N^8Fv0|oiu>JUz-Opx)spQTYiaAlhT6#_Icph*C-=y5ZmpAfB# zNR*4@W~3vlFfc%{vssxgv<-M|i<8E=QZ6IhT-_}O3ERmc8^rV^qi1?UM4!U+mi|J@ zLY=TWwOZjv?BOz_%1$SC^^Us@n+(z!L{jg$+i;CRI+IB1U31%SA8@i++JA>@Gbzk3 zm&aLzE!#8=hA9R}$A}@HQ?yRbRog`lrB7MBytgv_$W^WSH=Dx;f6F? literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index 6219b20016..ce031cc916 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quadratic", - "version": "0.5.2", + "version": "0.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quadratic", - "version": "0.5.2", + "version": "0.5.4", "workspaces": [ "quadratic-api", "quadratic-shared", @@ -18,6 +18,7 @@ "quadratic-kernels/python-wasm" ], "dependencies": { + "@ory/kratos-client": "^1.2.1", "tsc": "^2.0.4", "vitest": "^1.5.0", "zod": "^3.23.8" @@ -148,9 +149,166 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/bedrock-sdk": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@anthropic-ai/bedrock-sdk/-/bedrock-sdk-0.11.2.tgz", + "integrity": "sha512-s6YumXjxXAxUW+yS/EUcN1nrKhd4HNZ4bGHxxu2jnU0/y7jYRQ4YHnTtCndDVSgxMq2IKMM0MhLAGKYfWPiing==", + "dependencies": { + "@anthropic-ai/sdk": "^0", + "@aws-crypto/sha256-js": "^4.0.0", + "@aws-sdk/client-bedrock-runtime": "^3.423.0", + "@aws-sdk/credential-providers": "^3.341.0", + "@smithy/eventstream-serde-node": "^2.0.10", + "@smithy/fetch-http-handler": "^2.2.1", + "@smithy/protocol-http": "^3.0.6", + "@smithy/signature-v4": "^3.1.1", + "@smithy/smithy-client": "^2.1.9", + "@smithy/types": "^2.3.4", + "@smithy/util-base64": "^2.0.0" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@aws-crypto/sha256-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-4.0.0.tgz", + "integrity": "sha512-MHGJyjE7TX9aaqXj7zk2ppnFUOhaDs5sP+HtNS0evOxn72c+5njUmyJmpGd7TfyoDznZlHMmdo/xGUdu2NIjNQ==", + "dependencies": { + "@aws-crypto/util": "^4.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@aws-crypto/util": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-4.0.0.tgz", + "integrity": "sha512-2EnmPy2gsFZ6m8bwUQN4jq+IyXV3quHAcwPOS6ZA3k+geujiqI8aRokO2kFJe+idJ/P3v4qWI186rVMo0+zLDQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@smithy/signature-v4": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-3.1.2.tgz", + "integrity": "sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@smithy/signature-v4/node_modules/@smithy/types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz", + "integrity": "sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@smithy/util-middleware": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.8.tgz", + "integrity": "sha512-p7iYAPaQjoeM+AKABpYWeDdtwQNxasr4aXQEA/OmbOaug9V0odRVDy3Wx4ci8soljE/JXQo+abV0qZpW8NX0yA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@smithy/util-middleware/node_modules/@smithy/types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz", + "integrity": "sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@anthropic-ai/bedrock-sdk/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@anthropic-ai/sdk": { - "version": "0.27.3", - "license": "MIT", + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz", + "integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -315,213 +473,230 @@ "version": "1.14.1", "license": "0BSD" }, - "node_modules/@aws-sdk/client-s3": { - "version": "3.514.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha1-browser": "3.0.0", - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.513.0", - "@aws-sdk/core": "3.513.0", - "@aws-sdk/credential-provider-node": "3.514.0", - "@aws-sdk/middleware-bucket-endpoint": "3.511.0", - "@aws-sdk/middleware-expect-continue": "3.511.0", - "@aws-sdk/middleware-flexible-checksums": "3.511.0", - "@aws-sdk/middleware-host-header": "3.511.0", - "@aws-sdk/middleware-location-constraint": "3.511.0", - "@aws-sdk/middleware-logger": "3.511.0", - "@aws-sdk/middleware-recursion-detection": "3.511.0", - "@aws-sdk/middleware-sdk-s3": "3.511.0", - "@aws-sdk/middleware-signing": "3.511.0", - "@aws-sdk/middleware-ssec": "3.511.0", - "@aws-sdk/middleware-user-agent": "3.511.0", - "@aws-sdk/region-config-resolver": "3.511.0", - "@aws-sdk/signature-v4-multi-region": "3.511.0", - "@aws-sdk/types": "3.511.0", - "@aws-sdk/util-endpoints": "3.511.0", - "@aws-sdk/util-user-agent-browser": "3.511.0", - "@aws-sdk/util-user-agent-node": "3.511.0", - "@aws-sdk/xml-builder": "3.496.0", - "@smithy/config-resolver": "^2.1.1", - "@smithy/core": "^1.3.2", - "@smithy/eventstream-serde-browser": "^2.1.1", - "@smithy/eventstream-serde-config-resolver": "^2.1.1", - "@smithy/eventstream-serde-node": "^2.1.1", - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/hash-blob-browser": "^2.1.1", - "@smithy/hash-node": "^2.1.1", - "@smithy/hash-stream-node": "^2.1.1", - "@smithy/invalid-dependency": "^2.1.1", - "@smithy/md5-js": "^2.1.1", - "@smithy/middleware-content-length": "^2.1.1", - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-retry": "^2.1.1", - "@smithy/middleware-serde": "^2.1.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", - "@smithy/util-base64": "^2.1.1", - "@smithy/util-body-length-browser": "^2.1.1", - "@smithy/util-body-length-node": "^2.2.1", - "@smithy/util-defaults-mode-browser": "^2.1.1", - "@smithy/util-defaults-mode-node": "^2.2.0", - "@smithy/util-endpoints": "^1.1.1", - "@smithy/util-retry": "^2.1.1", - "@smithy/util-stream": "^2.1.1", - "@smithy/util-utf8": "^2.1.1", - "@smithy/util-waiter": "^2.1.1", - "fast-xml-parser": "4.2.5", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.583.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sso-oidc": "3.583.0", - "@aws-sdk/client-sts": "3.583.0", - "@aws-sdk/core": "3.582.0", - "@aws-sdk/credential-provider-node": "3.583.0", - "@aws-sdk/middleware-host-header": "3.577.0", - "@aws-sdk/middleware-logger": "3.577.0", - "@aws-sdk/middleware-recursion-detection": "3.577.0", - "@aws-sdk/middleware-user-agent": "3.583.0", - "@aws-sdk/region-config-resolver": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-endpoints": "3.583.0", - "@aws-sdk/util-user-agent-browser": "3.577.0", - "@aws-sdk/util-user-agent-node": "3.577.0", - "@smithy/config-resolver": "^3.0.0", - "@smithy/core": "^2.0.1", - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/hash-node": "^3.0.0", - "@smithy/invalid-dependency": "^3.0.0", - "@smithy/middleware-content-length": "^3.0.0", - "@smithy/middleware-endpoint": "^3.0.0", - "@smithy/middleware-retry": "^3.0.1", - "@smithy/middleware-serde": "^3.0.0", - "@smithy/middleware-stack": "^3.0.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/url-parser": "^3.0.0", + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.682.0.tgz", + "integrity": "sha512-8dPaXEACiwxm47RltmhckwlfwucX9+orKF9UZVPQlvYOo8M7mTxRtTuNq711iwz5dhXI1S3eXR0vQisjT6Ekaw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/client-sts": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/eventstream-serde-browser": "^3.0.10", + "@smithy/eventstream-serde-config-resolver": "^3.0.7", + "@smithy/eventstream-serde-node": "^3.0.9", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.1", - "@smithy/util-defaults-mode-node": "^3.0.1", - "@smithy/util-endpoints": "^2.0.0", - "@smithy/util-middleware": "^3.0.0", - "@smithy/util-retry": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-stream": "^3.1.9", "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sso": { - "version": "3.583.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.582.0", - "@aws-sdk/middleware-host-header": "3.577.0", - "@aws-sdk/middleware-logger": "3.577.0", - "@aws-sdk/middleware-recursion-detection": "3.577.0", - "@aws-sdk/middleware-user-agent": "3.583.0", - "@aws-sdk/region-config-resolver": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-endpoints": "3.583.0", - "@aws-sdk/util-user-agent-browser": "3.577.0", - "@aws-sdk/util-user-agent-node": "3.577.0", - "@smithy/config-resolver": "^3.0.0", - "@smithy/core": "^2.0.1", - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/hash-node": "^3.0.0", - "@smithy/invalid-dependency": "^3.0.0", - "@smithy/middleware-content-length": "^3.0.0", - "@smithy/middleware-endpoint": "^3.0.0", - "@smithy/middleware-retry": "^3.0.1", - "@smithy/middleware-serde": "^3.0.0", - "@smithy/middleware-stack": "^3.0.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/url-parser": "^3.0.0", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.1", - "@smithy/util-defaults-mode-node": "^3.0.1", - "@smithy/util-endpoints": "^2.0.0", - "@smithy/util-middleware": "^3.0.0", - "@smithy/util-retry": "^3.0.0", - "@smithy/util-utf8": "^3.0.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.583.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.583.0", - "@aws-sdk/core": "3.582.0", - "@aws-sdk/credential-provider-node": "3.583.0", - "@aws-sdk/middleware-host-header": "3.577.0", - "@aws-sdk/middleware-logger": "3.577.0", - "@aws-sdk/middleware-recursion-detection": "3.577.0", - "@aws-sdk/middleware-user-agent": "3.583.0", - "@aws-sdk/region-config-resolver": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-endpoints": "3.583.0", - "@aws-sdk/util-user-agent-browser": "3.577.0", - "@aws-sdk/util-user-agent-node": "3.577.0", - "@smithy/config-resolver": "^3.0.0", - "@smithy/core": "^2.0.1", - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/hash-node": "^3.0.0", - "@smithy/invalid-dependency": "^3.0.0", - "@smithy/middleware-content-length": "^3.0.0", - "@smithy/middleware-endpoint": "^3.0.0", - "@smithy/middleware-retry": "^3.0.1", - "@smithy/middleware-serde": "^3.0.0", - "@smithy/middleware-stack": "^3.0.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/url-parser": "^3.0.0", + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/client-sso": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.682.0.tgz", + "integrity": "sha512-PYH9RFUMYLFl66HSBq4tIx6fHViMLkhJHTYJoJONpBs+Td+NwVJ895AdLtDsBIhMS0YseCbPpuyjUCJgsUrwUw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.1", - "@smithy/util-defaults-mode-node": "^3.0.1", - "@smithy/util-endpoints": "^2.0.0", - "@smithy/util-middleware": "^3.0.0", - "@smithy/util-retry": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -529,48 +704,101 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sts": { - "version": "3.583.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sso-oidc": "3.583.0", - "@aws-sdk/core": "3.582.0", - "@aws-sdk/credential-provider-node": "3.583.0", - "@aws-sdk/middleware-host-header": "3.577.0", - "@aws-sdk/middleware-logger": "3.577.0", - "@aws-sdk/middleware-recursion-detection": "3.577.0", - "@aws-sdk/middleware-user-agent": "3.583.0", - "@aws-sdk/region-config-resolver": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-endpoints": "3.583.0", - "@aws-sdk/util-user-agent-browser": "3.577.0", - "@aws-sdk/util-user-agent-node": "3.577.0", - "@smithy/config-resolver": "^3.0.0", - "@smithy/core": "^2.0.1", - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/hash-node": "^3.0.0", - "@smithy/invalid-dependency": "^3.0.0", - "@smithy/middleware-content-length": "^3.0.0", - "@smithy/middleware-endpoint": "^3.0.0", - "@smithy/middleware-retry": "^3.0.1", - "@smithy/middleware-serde": "^3.0.0", - "@smithy/middleware-stack": "^3.0.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/url-parser": "^3.0.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.682.0.tgz", + "integrity": "sha512-ZPZ7Y/r/w3nx/xpPzGSqSQsB090Xk5aZZOH+WBhTDn/pBEuim09BYXCLzvvxb7R7NnuoQdrTJiwimdJAhHl7ZQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.1", - "@smithy/util-defaults-mode-node": "^3.0.1", - "@smithy/util-endpoints": "^2.0.0", - "@smithy/util-middleware": "^3.0.0", - "@smithy/util-retry": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/client-sts": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.682.0.tgz", + "integrity": "sha512-xKuo4HksZ+F8m9DOfx/ZuWNhaPuqZFPwwy0xqcBT6sWH7OAuBjv/fnpOTzyQhpVTWddlf+ECtMAMrxjxuOExGQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -578,267 +806,299 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/core": { - "version": "3.582.0", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^2.0.1", - "@smithy/protocol-http": "^4.0.0", - "@smithy/signature-v4": "^3.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "fast-xml-parser": "4.2.5", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/core": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.679.0.tgz", + "integrity": "sha512-CS6PWGX8l4v/xyvX8RtXnBisdCa5+URzKd0L6GvHChype9qKUVxO/Gg6N/y43Hvg7MNWJt9FBPNWIxUB+byJwg==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-middleware": "^3.0.7", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.679.0.tgz", + "integrity": "sha512-EdlTYbzMm3G7VUNAMxr9S1nC1qUNqhKlAxFU8E7cKsAe8Bp29CD5HAs3POc56AVo9GC4yRIS+/mtlZSmrckzUA==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.582.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/util-stream": "^3.0.1", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.679.0.tgz", + "integrity": "sha512-ZoKLubW5DqqV1/2a3TSn+9sSKg0T8SsYMt1JeirnuLJF0mCoYFUaWMyvxxKuxPoqvUsaycxKru4GkpJ10ltNBw==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-stream": "^3.1.9", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.583.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.577.0", - "@aws-sdk/credential-provider-process": "3.577.0", - "@aws-sdk/credential-provider-sso": "3.583.0", - "@aws-sdk/credential-provider-web-identity": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@smithy/credential-provider-imds": "^3.0.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.682.0.tgz", + "integrity": "sha512-6eqWeHdK6EegAxqDdiCi215nT3QZPwukgWAYuVxNfJ/5m0/P7fAzF+D5kKVgByUvGJEbq/FEL8Fw7OBe64AA+g==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.583.0" + "@aws-sdk/client-sts": "^3.682.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.583.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.682.0.tgz", + "integrity": "sha512-HSmDqZcBVZrTctHCT9m++vdlDfJ1ARI218qmZa+TZzzOFNpKWy6QyHMEra45GB9GnkkMmV6unoDSPMuN0AqcMg==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.577.0", - "@aws-sdk/credential-provider-http": "3.582.0", - "@aws-sdk/credential-provider-ini": "3.583.0", - "@aws-sdk/credential-provider-process": "3.577.0", - "@aws-sdk/credential-provider-sso": "3.583.0", - "@aws-sdk/credential-provider-web-identity": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@smithy/credential-provider-imds": "^3.0.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-ini": "3.682.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.679.0.tgz", + "integrity": "sha512-u/p4TV8kQ0zJWDdZD4+vdQFTMhkDEJFws040Gm113VHa/Xo1SYOjbpvqeuFoz6VmM0bLvoOWjxB9MxnSQbwKpQ==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.583.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.682.0.tgz", + "integrity": "sha512-h7IH1VsWgV6YAJSWWV6y8uaRjGqLY3iBpGZlXuTH/c236NMLaNv+WqCBLeBxkFGUb2WeQ+FUPEJDCD69rgLIkg==", "dependencies": { - "@aws-sdk/client-sso": "3.583.0", - "@aws-sdk/token-providers": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/client-sso": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/token-providers": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.679.0.tgz", + "integrity": "sha512-a74tLccVznXCaBefWPSysUcLXYJiSkeUmQGtalNgJ1vGkE36W5l/8czFiiowdWdKWz7+x6xf0w+Kjkjlj42Ung==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.577.0" + "@aws-sdk/client-sts": "^3.679.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.679.0.tgz", + "integrity": "sha512-y176HuQ8JRY3hGX8rQzHDSbCl9P5Ny9l16z4xmaiLo+Qfte7ee4Yr3yaAKd7GFoJ3/Mhud2XZ37fR015MfYl2w==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-logger": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-logger": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.679.0.tgz", + "integrity": "sha512-0vet8InEj7nvIvGKk+ch7bEF5SyZ7Us9U7YTEgXPrBNStKeRUsgwRm0ijPWWd0a3oz2okaEwXsFl7G/vI0XiEA==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.679.0.tgz", + "integrity": "sha512-sQoAZFsQiW/LL3DfKMYwBoGjYDEnMbA9WslWN8xneCmBAwKo6IcSksvYs23PP8XMIoBGe2I2J9BSr654XWygTQ==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.583.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.682.0.tgz", + "integrity": "sha512-7TyvYR9HdGH1/Nq0eeApUTM4izB6rExiw87khVYuJwZHr6FmvIL1FsOVFro/4WlXa0lg4LiYOm/8H8dHv+fXTg==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-endpoints": "3.583.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.679.0.tgz", + "integrity": "sha512-Ybx54P8Tg6KKq5ck7uwdjiKif7n/8g1x+V0V9uTjBjRWqaIgiqzXwKWoPj6NCNkE7tJNtqI4JrNxp/3S3HvmRw==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/token-providers": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/token-providers": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.679.0.tgz", + "integrity": "sha512-1/+Zso/x2jqgutKixYFQEGli0FELTgah6bm7aB+m2FAWH4Hz7+iMUsazg6nSWm714sG9G3h5u42Dmpvi9X6/hA==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.577.0" + "@aws-sdk/client-sso-oidc": "^3.679.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/types": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/types": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.679.0.tgz", + "integrity": "sha512-NwVq8YvInxQdJ47+zz4fH3BRRLC6lL+WLkvr242PVBbUOLRyK/lkwHlfiKUoeVIMyK5NF+up6TRg71t/8Bny6Q==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-endpoints": { - "version": "3.583.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/util-endpoints": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.679.0.tgz", + "integrity": "sha512-YL6s4Y/1zC45OvddvgE139fjeWSKKPgLlnfrvhVL7alNyY9n7beR4uhoDpNrt5mI6sn9qiBF17790o+xLAXjjg==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/types": "^3.0.0", - "@smithy/util-endpoints": "^2.0.0", + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "@smithy/util-endpoints": "^2.1.3", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.679.0.tgz", + "integrity": "sha512-CusSm2bTBG1kFypcsqU8COhnYc6zltobsqs3nRrvYqYaOqtMnuE46K4XTWpnzKgwDejgZGOE+WYyprtAxrPvmQ==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.577.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.682.0.tgz", + "integrity": "sha512-so5s+j0gPoTS0HM4HPL+G0ajk0T6cQAg8JXzRgvyiQAxqie+zGCZAV3VuVeMNWMVbzsgZl0pYZaatPFTLG/AxA==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/types": "^3.0.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -853,78 +1113,146 @@ } } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/abort-controller": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/abort-controller": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.6.tgz", + "integrity": "sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/config-resolver": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/config-resolver": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.10.tgz", + "integrity": "sha512-Uh0Sz9gdUuz538nvkPiyv1DZRX9+D15EKDtnQP5rYVAzM/dnYk3P8cg73jcxyOitPgT3mE3OVj7ky7sibzHWkw==", "dependencies": { - "@smithy/node-config-provider": "^3.0.0", - "@smithy/types": "^3.0.0", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/types": "^3.6.0", "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/core": { - "version": "2.0.1", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/core": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.1.tgz", + "integrity": "sha512-DujtuDA7BGEKExJ05W5OdxCoyekcKT3Rhg1ZGeiUWaz2BJIWXjZmsG/DIP4W48GHno7AQwRsaCb8NcBgH3QZpg==", "dependencies": { - "@smithy/middleware-endpoint": "^3.0.0", - "@smithy/middleware-retry": "^3.0.1", - "@smithy/middleware-serde": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/util-middleware": "^3.0.0", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-stream": "^3.2.1", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/credential-provider-imds": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/credential-provider-imds": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.5.tgz", + "integrity": "sha512-4FTQGAsuwqTzVMmiRVTn0RR9GrbRfkP0wfu/tXWVHd2LgNpTY0uglQpIScXK4NaEyXbB3JmZt8gfVqO50lP8wg==", "dependencies": { - "@smithy/node-config-provider": "^3.0.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/types": "^3.0.0", - "@smithy/url-parser": "^3.0.0", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.8", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/fetch-http-handler": { - "version": "3.0.1", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/eventstream-codec": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.1.7.tgz", + "integrity": "sha512-kVSXScIiRN7q+s1x7BrQtZ1Aa9hvvP9FeCqCdBxv37GimIHgBCOnZ5Ip80HLt0DhnAKpiobFdGqTFgbaJNrazA==", "dependencies": { - "@smithy/protocol-http": "^4.0.0", - "@smithy/querystring-builder": "^3.0.0", - "@smithy/types": "^3.0.0", + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^3.6.0", + "@smithy/util-hex-encoding": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/eventstream-serde-browser": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.11.tgz", + "integrity": "sha512-Pd1Wnq3CQ/v2SxRifDUihvpXzirJYbbtXfEnnLV/z0OGCTx/btVX74P86IgrZkjOydOASBGXdPpupYQI+iO/6A==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.10", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.8.tgz", + "integrity": "sha512-zkFIG2i1BLbfoGQnf1qEeMqX0h5qAznzaZmMVNnvPZz9J5AWBPkOMckZWPedGUPcVITacwIdQXoPcdIQq5FRcg==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/eventstream-serde-node": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.10.tgz", + "integrity": "sha512-hjpU1tIsJ9qpcoZq9zGHBJPBOeBGYt+n8vfhDwnITPhEre6APrvqq/y3XMDEGUT2cWQ4ramNqBPRbx3qn55rhw==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.10", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/eventstream-serde-universal": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.10.tgz", + "integrity": "sha512-ewG1GHbbqsFZ4asaq40KmxCmXO+AFSM1b+DcO2C03dyJj/ZH71CiTg853FSE/3SHK9q3jiYQIFjlGSwfxQ9kww==", + "dependencies": { + "@smithy/eventstream-codec": "^3.1.7", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz", + "integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", "@smithy/util-base64": "^3.0.0", "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/hash-node": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/hash-node": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.8.tgz", + "integrity": "sha512-tlNQYbfpWXHimHqrvgo14DrMAgUBua/cNoz9fMYcDmYej7MAmUcjav/QKQbFc3NrcPxeJ7QClER4tWZmfwoPng==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -933,17 +1261,19 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/invalid-dependency": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/invalid-dependency": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.8.tgz", + "integrity": "sha512-7Qynk6NWtTQhnGTTZwks++nJhQ1O54Mzi7fz4PqZOiYXb4Z1Flpb2yRvdALoggTS8xjtohWUM+RygOtB30YL3Q==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", "dependencies": { "tslib": "^2.6.2" }, @@ -951,45 +1281,49 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-content-length": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/middleware-content-length": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.10.tgz", + "integrity": "sha512-T4dIdCs1d/+/qMpwhJ1DzOhxCZjZHbHazEPJWdB4GDi2HjIZllVzeBEcdJUN0fomV8DURsgOyrbEUzg3vzTaOg==", "dependencies": { - "@smithy/protocol-http": "^4.0.0", - "@smithy/types": "^3.0.0", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-endpoint": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@smithy/middleware-serde": "^3.0.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", - "@smithy/url-parser": "^3.0.0", - "@smithy/util-middleware": "^3.0.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/middleware-endpoint": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.1.tgz", + "integrity": "sha512-wWO3xYmFm6WRW8VsEJ5oU6h7aosFXfszlz3Dj176pTij6o21oZnzkCLzShfmRaaCHDkBXWBdO0c4sQAvLFP6zA==", + "dependencies": { + "@smithy/core": "^2.5.1", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.9", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "@smithy/util-middleware": "^3.0.8", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-retry": { - "version": "3.0.1", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/middleware-retry": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.25.tgz", + "integrity": "sha512-m1F70cPaMBML4HiTgCw5I+jFNtjgz5z5UdGnUbG37vw6kh4UvizFYjqJGHvicfgKMkDL6mXwyPp5mhZg02g5sg==", "dependencies": { - "@smithy/node-config-provider": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/service-error-classification": "^3.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/util-middleware": "^3.0.0", - "@smithy/util-retry": "^3.0.0", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.5", + "@smithy/service-error-classification": "^3.0.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-retry": "^3.0.8", "tslib": "^2.6.2", "uuid": "^9.0.1" }, @@ -997,82 +1331,89 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-serde": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/middleware-serde": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.8.tgz", + "integrity": "sha512-Xg2jK9Wc/1g/MBMP/EUn2DLspN8LNt+GMe7cgF+Ty3vl+Zvu+VeZU5nmhveU+H8pxyTsjrAkci8NqY6OuvZnjA==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-stack": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/middleware-stack": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.8.tgz", + "integrity": "sha512-d7ZuwvYgp1+3682Nx0MD3D/HtkmZd49N3JUndYWQXfRZrYEnCWYc8BHcNmVsPAp9gKvlurdg/mubE6b/rPS9MA==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/node-config-provider": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/node-config-provider": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.9.tgz", + "integrity": "sha512-qRHoah49QJ71eemjuS/WhUXB+mpNtwHRWQr77J/m40ewBVVwvo52kYAmb7iuaECgGTTcYxHS4Wmewfwy++ueew==", "dependencies": { - "@smithy/property-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", + "@smithy/property-provider": "^3.1.8", + "@smithy/shared-ini-file-loader": "^3.1.9", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/node-http-handler": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/node-http-handler": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.5.tgz", + "integrity": "sha512-PkOwPNeKdvX/jCpn0A8n9/TyoxjGZB8WVoJmm9YzsnAgggTj4CrjpRHlTQw7dlLZ320n1mY1y+nTRUDViKi/3w==", "dependencies": { - "@smithy/abort-controller": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/querystring-builder": "^3.0.0", - "@smithy/types": "^3.0.0", + "@smithy/abort-controller": "^3.1.6", + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/property-provider": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/property-provider": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.8.tgz", + "integrity": "sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/protocol-http": { - "version": "4.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/protocol-http": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.5.tgz", + "integrity": "sha512-hsjtwpIemmCkm3ZV5fd/T0bPIugW1gJXwZ/hpuVubt2hEUApIoUTrf6qIdh9MAWlw0vjMrA1ztJLAwtNaZogvg==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/querystring-builder": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/querystring-builder": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.8.tgz", + "integrity": "sha512-btYxGVqFUARbUrN6VhL9c3dnSviIwBYD9Rz1jHuN1hgh28Fpv2xjU1HeCeDJX68xctz7r4l1PBnFhGg1WBBPuA==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "@smithy/util-uri-escape": "^3.0.0", "tslib": "^2.6.2" }, @@ -1080,46 +1421,51 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/querystring-parser": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/querystring-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.8.tgz", + "integrity": "sha512-BtEk3FG7Ks64GAbt+JnKqwuobJNX8VmFLBsKIwWr1D60T426fGrV2L3YS5siOcUhhp6/Y6yhBw1PSPxA5p7qGg==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/service-error-classification": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/service-error-classification": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.8.tgz", + "integrity": "sha512-uEC/kCCFto83bz5ZzapcrgGqHOh/0r69sZ2ZuHlgoD5kYgXJEThCoTuw/y1Ub3cE7aaKdznb+jD9xRPIfIwD7g==", "dependencies": { - "@smithy/types": "^3.0.0" + "@smithy/types": "^3.6.0" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/shared-ini-file-loader": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.9.tgz", + "integrity": "sha512-/+OsJRNtoRbtsX0UpSgWVxFZLsJHo/4sTr+kBg/J78sr7iC+tHeOvOJrS5hCpVQ6sWBbhWLp1UNiuMyZhE6pmA==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/signature-v4": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/signature-v4": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.1.tgz", + "integrity": "sha512-NsV1jF4EvmO5wqmaSzlnTVetemBS3FZHdyc5CExbDljcyJCEEkJr8ANu2JvtNbVg/9MvKAWV44kTrGS+Pi4INg==", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", - "@smithy/types": "^3.0.0", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", "@smithy/util-hex-encoding": "^3.0.0", - "@smithy/util-middleware": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -1128,24 +1474,27 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/smithy-client": { - "version": "3.0.1", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/smithy-client": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.2.tgz", + "integrity": "sha512-dxw1BDxJiY9/zI3cBqfVrInij6ShjpV4fmGHesGZZUiP9OSE/EVfdwdRz0PgvkEvrZHpsj2htRaHJfftE8giBA==", "dependencies": { - "@smithy/middleware-endpoint": "^3.0.0", - "@smithy/middleware-stack": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/types": "^3.0.0", - "@smithy/util-stream": "^3.0.1", + "@smithy/core": "^2.5.1", + "@smithy/middleware-endpoint": "^3.2.1", + "@smithy/middleware-stack": "^3.0.8", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-stream": "^3.2.1", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/types": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz", + "integrity": "sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==", "dependencies": { "tslib": "^2.6.2" }, @@ -1153,18 +1502,20 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/url-parser": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/url-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.8.tgz", + "integrity": "sha512-4FdOhwpTW7jtSFWm7SpfLGKIBC9ZaTKG5nBF0wK24aoQKQyDIKUw3+KFWCQ9maMzrgTJIuOvOnsV2lLGW5XjTg==", "dependencies": { - "@smithy/querystring-parser": "^3.0.0", - "@smithy/types": "^3.0.0", + "@smithy/querystring-parser": "^3.0.8", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-base64": { + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-base64": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-utf8": "^3.0.0", @@ -1174,16 +1525,18 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-body-length-browser": { + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-body-length-browser": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", "dependencies": { "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-body-length-node": { + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-body-length-node": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", "dependencies": { "tslib": "^2.6.2" }, @@ -1191,9 +1544,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-buffer-from": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" @@ -1202,9 +1556,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-config-provider": { + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-config-provider": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", "dependencies": { "tslib": "^2.6.2" }, @@ -1212,13 +1567,14 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-defaults-mode-browser": { - "version": "3.0.1", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.25.tgz", + "integrity": "sha512-fRw7zymjIDt6XxIsLwfJfYUfbGoO9CmCJk6rjJ/X5cd20+d2Is7xjU5Kt/AiDt6hX8DAf5dztmfP5O82gR9emA==", "dependencies": { - "@smithy/property-provider": "^3.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", + "@smithy/property-provider": "^3.1.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -1226,37 +1582,40 @@ "node": ">= 10.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-defaults-mode-node": { - "version": "3.0.1", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.25.tgz", + "integrity": "sha512-H3BSZdBDiVZGzt8TG51Pd2FvFO0PAx/A0mJ0EH8a13KJ6iUCdYnw/Dk/MdC1kTd0eUuUGisDFaxXVXo4HHFL1g==", "dependencies": { - "@smithy/config-resolver": "^3.0.0", - "@smithy/credential-provider-imds": "^3.0.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", + "@smithy/config-resolver": "^3.0.10", + "@smithy/credential-provider-imds": "^3.2.5", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">= 10.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-endpoints": { - "version": "2.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-endpoints": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.4.tgz", + "integrity": "sha512-kPt8j4emm7rdMWQyL0F89o92q10gvCUa6sBkBtDJ7nV2+P7wpXczzOfoDJ49CKXe5CCqb8dc1W+ZdLlrKzSAnQ==", "dependencies": { - "@smithy/node-config-provider": "^3.0.0", - "@smithy/types": "^3.0.0", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-hex-encoding": { + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-hex-encoding": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", "dependencies": { "tslib": "^2.6.2" }, @@ -1264,36 +1623,39 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-middleware": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-middleware": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.8.tgz", + "integrity": "sha512-p7iYAPaQjoeM+AKABpYWeDdtwQNxasr4aXQEA/OmbOaug9V0odRVDy3Wx4ci8soljE/JXQo+abV0qZpW8NX0yA==", "dependencies": { - "@smithy/types": "^3.0.0", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-retry": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-retry": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.8.tgz", + "integrity": "sha512-TCEhLnY581YJ+g1x0hapPz13JFqzmh/pMWL2KEFASC51qCfw3+Y47MrTmea4bUE5vsdxQ4F6/KFbUeSz22Q1ow==", "dependencies": { - "@smithy/service-error-classification": "^3.0.0", - "@smithy/types": "^3.0.0", + "@smithy/service-error-classification": "^3.0.8", + "@smithy/types": "^3.6.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-stream": { - "version": "3.0.1", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.2.1.tgz", + "integrity": "sha512-R3ufuzJRxSJbE58K9AEnL/uSZyVdHzud9wLS8tIbXclxKzoe09CRohj2xV8wpx5tj7ZbiJaKYcutMm1eYgz/0A==", "dependencies": { - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/types": "^3.0.0", + "@smithy/fetch-http-handler": "^4.0.0", + "@smithy/node-http-handler": "^3.2.5", + "@smithy/types": "^3.6.0", "@smithy/util-base64": "^3.0.0", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-hex-encoding": "^3.0.0", @@ -1304,9 +1666,22 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-uri-escape": { + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-stream/node_modules/@smithy/fetch-http-handler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz", + "integrity": "sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==", + "dependencies": { + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-uri-escape": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", "dependencies": { "tslib": "^2.6.2" }, @@ -1314,9 +1689,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/util-utf8": { "version": "3.0.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" @@ -1325,226 +1701,2498 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.513.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.685.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.685.0.tgz", + "integrity": "sha512-4h+aw0pUEOVP77TpF1ec9AX0mzotsiw2alXfh+P0t+43eg2EjaKRftRpNXyt5XmUPws+wsH10uEL4CzSZo1sxQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/client-sts": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.513.0", - "@aws-sdk/middleware-host-header": "3.511.0", - "@aws-sdk/middleware-logger": "3.511.0", - "@aws-sdk/middleware-recursion-detection": "3.511.0", - "@aws-sdk/middleware-user-agent": "3.511.0", - "@aws-sdk/region-config-resolver": "3.511.0", - "@aws-sdk/types": "3.511.0", - "@aws-sdk/util-endpoints": "3.511.0", - "@aws-sdk/util-user-agent-browser": "3.511.0", - "@aws-sdk/util-user-agent-node": "3.511.0", - "@smithy/config-resolver": "^2.1.1", - "@smithy/core": "^1.3.2", - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/hash-node": "^2.1.1", - "@smithy/invalid-dependency": "^2.1.1", - "@smithy/middleware-content-length": "^2.1.1", - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-retry": "^2.1.1", - "@smithy/middleware-serde": "^2.1.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", - "@smithy/util-base64": "^2.1.1", - "@smithy/util-body-length-browser": "^2.1.1", - "@smithy/util-body-length-node": "^2.2.1", - "@smithy/util-defaults-mode-browser": "^2.1.1", - "@smithy/util-defaults-mode-node": "^2.2.0", - "@smithy/util-endpoints": "^1.1.1", - "@smithy/util-middleware": "^2.1.1", - "@smithy/util-retry": "^2.1.1", - "@smithy/util-utf8": "^2.1.1", - "tslib": "^2.5.0" + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.513.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.513.0", - "@aws-sdk/core": "3.513.0", - "@aws-sdk/middleware-host-header": "3.511.0", - "@aws-sdk/middleware-logger": "3.511.0", - "@aws-sdk/middleware-recursion-detection": "3.511.0", - "@aws-sdk/middleware-user-agent": "3.511.0", - "@aws-sdk/region-config-resolver": "3.511.0", - "@aws-sdk/types": "3.511.0", - "@aws-sdk/util-endpoints": "3.511.0", - "@aws-sdk/util-user-agent-browser": "3.511.0", - "@aws-sdk/util-user-agent-node": "3.511.0", - "@smithy/config-resolver": "^2.1.1", - "@smithy/core": "^1.3.2", - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/hash-node": "^2.1.1", - "@smithy/invalid-dependency": "^2.1.1", - "@smithy/middleware-content-length": "^2.1.1", - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-retry": "^2.1.1", - "@smithy/middleware-serde": "^2.1.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", - "@smithy/util-base64": "^2.1.1", - "@smithy/util-body-length-browser": "^2.1.1", - "@smithy/util-body-length-node": "^2.2.1", - "@smithy/util-defaults-mode-browser": "^2.1.1", - "@smithy/util-defaults-mode-node": "^2.2.0", - "@smithy/util-endpoints": "^1.1.1", - "@smithy/util-middleware": "^2.1.1", - "@smithy/util-retry": "^2.1.1", - "@smithy/util-utf8": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" - }, - "peerDependencies": { - "@aws-sdk/credential-provider-node": "^3.513.0" } }, - "node_modules/@aws-sdk/client-sts": { - "version": "3.513.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.513.0", - "@aws-sdk/middleware-host-header": "3.511.0", - "@aws-sdk/middleware-logger": "3.511.0", - "@aws-sdk/middleware-recursion-detection": "3.511.0", - "@aws-sdk/middleware-user-agent": "3.511.0", - "@aws-sdk/region-config-resolver": "3.511.0", - "@aws-sdk/types": "3.511.0", - "@aws-sdk/util-endpoints": "3.511.0", - "@aws-sdk/util-user-agent-browser": "3.511.0", - "@aws-sdk/util-user-agent-node": "3.511.0", - "@smithy/config-resolver": "^2.1.1", - "@smithy/core": "^1.3.2", - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/hash-node": "^2.1.1", - "@smithy/invalid-dependency": "^2.1.1", - "@smithy/middleware-content-length": "^2.1.1", - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-retry": "^2.1.1", - "@smithy/middleware-serde": "^2.1.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", - "@smithy/util-base64": "^2.1.1", - "@smithy/util-body-length-browser": "^2.1.1", - "@smithy/util-body-length-node": "^2.2.1", - "@smithy/util-defaults-mode-browser": "^2.1.1", - "@smithy/util-defaults-mode-node": "^2.2.0", - "@smithy/util-endpoints": "^1.1.1", - "@smithy/util-middleware": "^2.1.1", - "@smithy/util-retry": "^2.1.1", - "@smithy/util-utf8": "^2.1.1", - "fast-xml-parser": "4.2.5", - "tslib": "^2.5.0" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" - }, - "peerDependencies": { - "@aws-sdk/credential-provider-node": "^3.513.0" } }, - "node_modules/@aws-sdk/core": { - "version": "3.513.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "dependencies": { - "@smithy/core": "^1.3.2", - "@smithy/protocol-http": "^3.1.1", - "@smithy/signature-v4": "^2.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "tslib": "^2.5.0" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.511.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "dependencies": { - "@aws-sdk/types": "3.511.0", - "@smithy/property-provider": "^2.1.1", - "@smithy/types": "^2.9.1", - "tslib": "^2.5.0" + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.511.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dependencies": { - "@aws-sdk/types": "3.511.0", - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/property-provider": "^2.1.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/util-stream": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.513.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dependencies": { - "@aws-sdk/client-sts": "3.513.0", - "@aws-sdk/credential-provider-env": "3.511.0", - "@aws-sdk/credential-provider-process": "3.511.0", - "@aws-sdk/credential-provider-sso": "3.513.0", - "@aws-sdk/credential-provider-web-identity": "3.513.0", - "@aws-sdk/types": "3.511.0", - "@smithy/credential-provider-imds": "^2.2.1", - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", - "tslib": "^2.5.0" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.514.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.511.0", + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/client-sso": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.682.0.tgz", + "integrity": "sha512-PYH9RFUMYLFl66HSBq4tIx6fHViMLkhJHTYJoJONpBs+Td+NwVJ895AdLtDsBIhMS0YseCbPpuyjUCJgsUrwUw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.682.0.tgz", + "integrity": "sha512-ZPZ7Y/r/w3nx/xpPzGSqSQsB090Xk5aZZOH+WBhTDn/pBEuim09BYXCLzvvxb7R7NnuoQdrTJiwimdJAhHl7ZQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/client-sts": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.682.0.tgz", + "integrity": "sha512-xKuo4HksZ+F8m9DOfx/ZuWNhaPuqZFPwwy0xqcBT6sWH7OAuBjv/fnpOTzyQhpVTWddlf+ECtMAMrxjxuOExGQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/core": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.679.0.tgz", + "integrity": "sha512-CS6PWGX8l4v/xyvX8RtXnBisdCa5+URzKd0L6GvHChype9qKUVxO/Gg6N/y43Hvg7MNWJt9FBPNWIxUB+byJwg==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-middleware": "^3.0.7", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.679.0.tgz", + "integrity": "sha512-EdlTYbzMm3G7VUNAMxr9S1nC1qUNqhKlAxFU8E7cKsAe8Bp29CD5HAs3POc56AVo9GC4yRIS+/mtlZSmrckzUA==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.679.0.tgz", + "integrity": "sha512-ZoKLubW5DqqV1/2a3TSn+9sSKg0T8SsYMt1JeirnuLJF0mCoYFUaWMyvxxKuxPoqvUsaycxKru4GkpJ10ltNBw==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-stream": "^3.1.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.682.0.tgz", + "integrity": "sha512-6eqWeHdK6EegAxqDdiCi215nT3QZPwukgWAYuVxNfJ/5m0/P7fAzF+D5kKVgByUvGJEbq/FEL8Fw7OBe64AA+g==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.682.0.tgz", + "integrity": "sha512-HSmDqZcBVZrTctHCT9m++vdlDfJ1ARI218qmZa+TZzzOFNpKWy6QyHMEra45GB9GnkkMmV6unoDSPMuN0AqcMg==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-ini": "3.682.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.679.0.tgz", + "integrity": "sha512-u/p4TV8kQ0zJWDdZD4+vdQFTMhkDEJFws040Gm113VHa/Xo1SYOjbpvqeuFoz6VmM0bLvoOWjxB9MxnSQbwKpQ==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.682.0.tgz", + "integrity": "sha512-h7IH1VsWgV6YAJSWWV6y8uaRjGqLY3iBpGZlXuTH/c236NMLaNv+WqCBLeBxkFGUb2WeQ+FUPEJDCD69rgLIkg==", + "dependencies": { + "@aws-sdk/client-sso": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/token-providers": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.679.0.tgz", + "integrity": "sha512-a74tLccVznXCaBefWPSysUcLXYJiSkeUmQGtalNgJ1vGkE36W5l/8czFiiowdWdKWz7+x6xf0w+Kjkjlj42Ung==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.679.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.679.0.tgz", + "integrity": "sha512-y176HuQ8JRY3hGX8rQzHDSbCl9P5Ny9l16z4xmaiLo+Qfte7ee4Yr3yaAKd7GFoJ3/Mhud2XZ37fR015MfYl2w==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-logger": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.679.0.tgz", + "integrity": "sha512-0vet8InEj7nvIvGKk+ch7bEF5SyZ7Us9U7YTEgXPrBNStKeRUsgwRm0ijPWWd0a3oz2okaEwXsFl7G/vI0XiEA==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.679.0.tgz", + "integrity": "sha512-sQoAZFsQiW/LL3DfKMYwBoGjYDEnMbA9WslWN8xneCmBAwKo6IcSksvYs23PP8XMIoBGe2I2J9BSr654XWygTQ==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.682.0.tgz", + "integrity": "sha512-7TyvYR9HdGH1/Nq0eeApUTM4izB6rExiw87khVYuJwZHr6FmvIL1FsOVFro/4WlXa0lg4LiYOm/8H8dHv+fXTg==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.679.0.tgz", + "integrity": "sha512-Ybx54P8Tg6KKq5ck7uwdjiKif7n/8g1x+V0V9uTjBjRWqaIgiqzXwKWoPj6NCNkE7tJNtqI4JrNxp/3S3HvmRw==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/token-providers": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.679.0.tgz", + "integrity": "sha512-1/+Zso/x2jqgutKixYFQEGli0FELTgah6bm7aB+m2FAWH4Hz7+iMUsazg6nSWm714sG9G3h5u42Dmpvi9X6/hA==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.679.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/types": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.679.0.tgz", + "integrity": "sha512-NwVq8YvInxQdJ47+zz4fH3BRRLC6lL+WLkvr242PVBbUOLRyK/lkwHlfiKUoeVIMyK5NF+up6TRg71t/8Bny6Q==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/util-endpoints": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.679.0.tgz", + "integrity": "sha512-YL6s4Y/1zC45OvddvgE139fjeWSKKPgLlnfrvhVL7alNyY9n7beR4uhoDpNrt5mI6sn9qiBF17790o+xLAXjjg==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "@smithy/util-endpoints": "^2.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.679.0.tgz", + "integrity": "sha512-CusSm2bTBG1kFypcsqU8COhnYc6zltobsqs3nRrvYqYaOqtMnuE46K4XTWpnzKgwDejgZGOE+WYyprtAxrPvmQ==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.682.0.tgz", + "integrity": "sha512-so5s+j0gPoTS0HM4HPL+G0ajk0T6cQAg8JXzRgvyiQAxqie+zGCZAV3VuVeMNWMVbzsgZl0pYZaatPFTLG/AxA==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/abort-controller": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.6.tgz", + "integrity": "sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/config-resolver": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.10.tgz", + "integrity": "sha512-Uh0Sz9gdUuz538nvkPiyv1DZRX9+D15EKDtnQP5rYVAzM/dnYk3P8cg73jcxyOitPgT3mE3OVj7ky7sibzHWkw==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.9", + "@smithy/types": "^3.6.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/core": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.1.tgz", + "integrity": "sha512-DujtuDA7BGEKExJ05W5OdxCoyekcKT3Rhg1ZGeiUWaz2BJIWXjZmsG/DIP4W48GHno7AQwRsaCb8NcBgH3QZpg==", + "dependencies": { + "@smithy/middleware-serde": "^3.0.8", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-stream": "^3.2.1", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/credential-provider-imds": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.5.tgz", + "integrity": "sha512-4FTQGAsuwqTzVMmiRVTn0RR9GrbRfkP0wfu/tXWVHd2LgNpTY0uglQpIScXK4NaEyXbB3JmZt8gfVqO50lP8wg==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.8", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz", + "integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/hash-node": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.8.tgz", + "integrity": "sha512-tlNQYbfpWXHimHqrvgo14DrMAgUBua/cNoz9fMYcDmYej7MAmUcjav/QKQbFc3NrcPxeJ7QClER4tWZmfwoPng==", + "dependencies": { + "@smithy/types": "^3.6.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/invalid-dependency": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.8.tgz", + "integrity": "sha512-7Qynk6NWtTQhnGTTZwks++nJhQ1O54Mzi7fz4PqZOiYXb4Z1Flpb2yRvdALoggTS8xjtohWUM+RygOtB30YL3Q==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-content-length": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.10.tgz", + "integrity": "sha512-T4dIdCs1d/+/qMpwhJ1DzOhxCZjZHbHazEPJWdB4GDi2HjIZllVzeBEcdJUN0fomV8DURsgOyrbEUzg3vzTaOg==", + "dependencies": { + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-endpoint": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.1.tgz", + "integrity": "sha512-wWO3xYmFm6WRW8VsEJ5oU6h7aosFXfszlz3Dj176pTij6o21oZnzkCLzShfmRaaCHDkBXWBdO0c4sQAvLFP6zA==", + "dependencies": { + "@smithy/core": "^2.5.1", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.9", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "@smithy/util-middleware": "^3.0.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-retry": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.25.tgz", + "integrity": "sha512-m1F70cPaMBML4HiTgCw5I+jFNtjgz5z5UdGnUbG37vw6kh4UvizFYjqJGHvicfgKMkDL6mXwyPp5mhZg02g5sg==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.5", + "@smithy/service-error-classification": "^3.0.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-retry": "^3.0.8", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-serde": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.8.tgz", + "integrity": "sha512-Xg2jK9Wc/1g/MBMP/EUn2DLspN8LNt+GMe7cgF+Ty3vl+Zvu+VeZU5nmhveU+H8pxyTsjrAkci8NqY6OuvZnjA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-stack": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.8.tgz", + "integrity": "sha512-d7ZuwvYgp1+3682Nx0MD3D/HtkmZd49N3JUndYWQXfRZrYEnCWYc8BHcNmVsPAp9gKvlurdg/mubE6b/rPS9MA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/node-config-provider": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.9.tgz", + "integrity": "sha512-qRHoah49QJ71eemjuS/WhUXB+mpNtwHRWQr77J/m40ewBVVwvo52kYAmb7iuaECgGTTcYxHS4Wmewfwy++ueew==", + "dependencies": { + "@smithy/property-provider": "^3.1.8", + "@smithy/shared-ini-file-loader": "^3.1.9", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/node-http-handler": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.5.tgz", + "integrity": "sha512-PkOwPNeKdvX/jCpn0A8n9/TyoxjGZB8WVoJmm9YzsnAgggTj4CrjpRHlTQw7dlLZ320n1mY1y+nTRUDViKi/3w==", + "dependencies": { + "@smithy/abort-controller": "^3.1.6", + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/property-provider": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.8.tgz", + "integrity": "sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/protocol-http": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.5.tgz", + "integrity": "sha512-hsjtwpIemmCkm3ZV5fd/T0bPIugW1gJXwZ/hpuVubt2hEUApIoUTrf6qIdh9MAWlw0vjMrA1ztJLAwtNaZogvg==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/querystring-builder": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.8.tgz", + "integrity": "sha512-btYxGVqFUARbUrN6VhL9c3dnSviIwBYD9Rz1jHuN1hgh28Fpv2xjU1HeCeDJX68xctz7r4l1PBnFhGg1WBBPuA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/querystring-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.8.tgz", + "integrity": "sha512-BtEk3FG7Ks64GAbt+JnKqwuobJNX8VmFLBsKIwWr1D60T426fGrV2L3YS5siOcUhhp6/Y6yhBw1PSPxA5p7qGg==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/service-error-classification": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.8.tgz", + "integrity": "sha512-uEC/kCCFto83bz5ZzapcrgGqHOh/0r69sZ2ZuHlgoD5kYgXJEThCoTuw/y1Ub3cE7aaKdznb+jD9xRPIfIwD7g==", + "dependencies": { + "@smithy/types": "^3.6.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.9.tgz", + "integrity": "sha512-/+OsJRNtoRbtsX0UpSgWVxFZLsJHo/4sTr+kBg/J78sr7iC+tHeOvOJrS5hCpVQ6sWBbhWLp1UNiuMyZhE6pmA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/signature-v4": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.1.tgz", + "integrity": "sha512-NsV1jF4EvmO5wqmaSzlnTVetemBS3FZHdyc5CExbDljcyJCEEkJr8ANu2JvtNbVg/9MvKAWV44kTrGS+Pi4INg==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/smithy-client": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.2.tgz", + "integrity": "sha512-dxw1BDxJiY9/zI3cBqfVrInij6ShjpV4fmGHesGZZUiP9OSE/EVfdwdRz0PgvkEvrZHpsj2htRaHJfftE8giBA==", + "dependencies": { + "@smithy/core": "^2.5.1", + "@smithy/middleware-endpoint": "^3.2.1", + "@smithy/middleware-stack": "^3.0.8", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-stream": "^3.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz", + "integrity": "sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/url-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.8.tgz", + "integrity": "sha512-4FdOhwpTW7jtSFWm7SpfLGKIBC9ZaTKG5nBF0wK24aoQKQyDIKUw3+KFWCQ9maMzrgTJIuOvOnsV2lLGW5XjTg==", + "dependencies": { + "@smithy/querystring-parser": "^3.0.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.25.tgz", + "integrity": "sha512-fRw7zymjIDt6XxIsLwfJfYUfbGoO9CmCJk6rjJ/X5cd20+d2Is7xjU5Kt/AiDt6hX8DAf5dztmfP5O82gR9emA==", + "dependencies": { + "@smithy/property-provider": "^3.1.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.25.tgz", + "integrity": "sha512-H3BSZdBDiVZGzt8TG51Pd2FvFO0PAx/A0mJ0EH8a13KJ6iUCdYnw/Dk/MdC1kTd0eUuUGisDFaxXVXo4HHFL1g==", + "dependencies": { + "@smithy/config-resolver": "^3.0.10", + "@smithy/credential-provider-imds": "^3.2.5", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-endpoints": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.4.tgz", + "integrity": "sha512-kPt8j4emm7rdMWQyL0F89o92q10gvCUa6sBkBtDJ7nV2+P7wpXczzOfoDJ49CKXe5CCqb8dc1W+ZdLlrKzSAnQ==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.9", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-middleware": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.8.tgz", + "integrity": "sha512-p7iYAPaQjoeM+AKABpYWeDdtwQNxasr4aXQEA/OmbOaug9V0odRVDy3Wx4ci8soljE/JXQo+abV0qZpW8NX0yA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-retry": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.8.tgz", + "integrity": "sha512-TCEhLnY581YJ+g1x0hapPz13JFqzmh/pMWL2KEFASC51qCfw3+Y47MrTmea4bUE5vsdxQ4F6/KFbUeSz22Q1ow==", + "dependencies": { + "@smithy/service-error-classification": "^3.0.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.2.1.tgz", + "integrity": "sha512-R3ufuzJRxSJbE58K9AEnL/uSZyVdHzud9wLS8tIbXclxKzoe09CRohj2xV8wpx5tj7ZbiJaKYcutMm1eYgz/0A==", + "dependencies": { + "@smithy/fetch-http-handler": "^4.0.0", + "@smithy/node-http-handler": "^3.2.5", + "@smithy/types": "^3.6.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-stream/node_modules/@smithy/fetch-http-handler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz", + "integrity": "sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==", + "dependencies": { + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.514.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "3.0.0", + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.513.0", + "@aws-sdk/core": "3.513.0", + "@aws-sdk/credential-provider-node": "3.514.0", + "@aws-sdk/middleware-bucket-endpoint": "3.511.0", + "@aws-sdk/middleware-expect-continue": "3.511.0", + "@aws-sdk/middleware-flexible-checksums": "3.511.0", + "@aws-sdk/middleware-host-header": "3.511.0", + "@aws-sdk/middleware-location-constraint": "3.511.0", + "@aws-sdk/middleware-logger": "3.511.0", + "@aws-sdk/middleware-recursion-detection": "3.511.0", + "@aws-sdk/middleware-sdk-s3": "3.511.0", + "@aws-sdk/middleware-signing": "3.511.0", + "@aws-sdk/middleware-ssec": "3.511.0", + "@aws-sdk/middleware-user-agent": "3.511.0", + "@aws-sdk/region-config-resolver": "3.511.0", + "@aws-sdk/signature-v4-multi-region": "3.511.0", + "@aws-sdk/types": "3.511.0", + "@aws-sdk/util-endpoints": "3.511.0", + "@aws-sdk/util-user-agent-browser": "3.511.0", + "@aws-sdk/util-user-agent-node": "3.511.0", + "@aws-sdk/xml-builder": "3.496.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.2", + "@smithy/eventstream-serde-browser": "^2.1.1", + "@smithy/eventstream-serde-config-resolver": "^2.1.1", + "@smithy/eventstream-serde-node": "^2.1.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-blob-browser": "^2.1.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/hash-stream-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/md5-js": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.2.0", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-stream": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "@smithy/util-waiter": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/client-secrets-manager": { + "version": "3.583.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sso-oidc": "3.583.0", + "@aws-sdk/client-sts": "3.583.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/credential-provider-node": "3.583.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.583.0", + "@aws-sdk/region-config-resolver": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.583.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sso": { + "version": "3.583.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.583.0", + "@aws-sdk/region-config-resolver": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.583.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.583.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.583.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/credential-provider-node": "3.583.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.583.0", + "@aws-sdk/region-config-resolver": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.583.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sts": { + "version": "3.583.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sso-oidc": "3.583.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/credential-provider-node": "3.583.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.583.0", + "@aws-sdk/region-config-resolver": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.583.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/core": { + "version": "3.582.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.0.1", + "@smithy/protocol-http": "^4.0.0", + "@smithy/signature-v4": "^3.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "fast-xml-parser": "4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.582.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/util-stream": "^3.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.583.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.577.0", + "@aws-sdk/credential-provider-process": "3.577.0", + "@aws-sdk/credential-provider-sso": "3.583.0", + "@aws-sdk/credential-provider-web-identity": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@smithy/credential-provider-imds": "^3.0.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.583.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.583.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.577.0", + "@aws-sdk/credential-provider-http": "3.582.0", + "@aws-sdk/credential-provider-ini": "3.583.0", + "@aws-sdk/credential-provider-process": "3.577.0", + "@aws-sdk/credential-provider-sso": "3.583.0", + "@aws-sdk/credential-provider-web-identity": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@smithy/credential-provider-imds": "^3.0.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.583.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.583.0", + "@aws-sdk/token-providers": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.577.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-logger": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.583.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.583.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/token-providers": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.577.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/types": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-endpoints": { + "version": "3.583.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/types": "^3.0.0", + "@smithy/util-endpoints": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/types": "^3.0.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.577.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/abort-controller": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/config-resolver": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/core": { + "version": "2.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/util-middleware": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/credential-provider-imds": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.0.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/fetch-http-handler": { + "version": "3.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.0.0", + "@smithy/querystring-builder": "^3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/hash-node": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/invalid-dependency": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-content-length": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-endpoint": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-middleware": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-retry": { + "version": "3.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/service-error-classification": "^3.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-serde": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/middleware-stack": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/node-config-provider": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/node-http-handler": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/querystring-builder": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/property-provider": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/protocol-http": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/querystring-builder": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/querystring-parser": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/service-error-classification": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/signature-v4": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/smithy-client": { + "version": "3.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-stream": "^3.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/types": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/url-parser": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-base64": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^3.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^3.0.0", + "@smithy/credential-provider-imds": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-endpoints": { + "version": "2.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-middleware": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-retry": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-stream": { + "version": "3.0.1", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.513.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.513.0", + "@aws-sdk/middleware-host-header": "3.511.0", + "@aws-sdk/middleware-logger": "3.511.0", + "@aws-sdk/middleware-recursion-detection": "3.511.0", + "@aws-sdk/middleware-user-agent": "3.511.0", + "@aws-sdk/region-config-resolver": "3.511.0", + "@aws-sdk/types": "3.511.0", + "@aws-sdk/util-endpoints": "3.511.0", + "@aws-sdk/util-user-agent-browser": "3.511.0", + "@aws-sdk/util-user-agent-node": "3.511.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.2", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.2.0", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.513.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.513.0", + "@aws-sdk/core": "3.513.0", + "@aws-sdk/middleware-host-header": "3.511.0", + "@aws-sdk/middleware-logger": "3.511.0", + "@aws-sdk/middleware-recursion-detection": "3.511.0", + "@aws-sdk/middleware-user-agent": "3.511.0", + "@aws-sdk/region-config-resolver": "3.511.0", + "@aws-sdk/types": "3.511.0", + "@aws-sdk/util-endpoints": "3.511.0", + "@aws-sdk/util-user-agent-browser": "3.511.0", + "@aws-sdk/util-user-agent-node": "3.511.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.2", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.2.0", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.513.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.513.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.513.0", + "@aws-sdk/middleware-host-header": "3.511.0", + "@aws-sdk/middleware-logger": "3.511.0", + "@aws-sdk/middleware-recursion-detection": "3.511.0", + "@aws-sdk/middleware-user-agent": "3.511.0", + "@aws-sdk/region-config-resolver": "3.511.0", + "@aws-sdk/types": "3.511.0", + "@aws-sdk/util-endpoints": "3.511.0", + "@aws-sdk/util-user-agent-browser": "3.511.0", + "@aws-sdk/util-user-agent-node": "3.511.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.2", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.2.0", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.513.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.513.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^1.3.2", + "@smithy/protocol-http": "^3.1.1", + "@smithy/signature-v4": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.685.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.685.0.tgz", + "integrity": "sha512-qw9s09JFhJxEkmbo1gn94pAtyLHSx8YBX2qqrL6v3BdsJbP2fnRJMMssGMGR4jPyhklh5TSgZo0FzflbqJU8sw==", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.685.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@aws-sdk/types": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.679.0.tgz", + "integrity": "sha512-NwVq8YvInxQdJ47+zz4fH3BRRLC6lL+WLkvr242PVBbUOLRyK/lkwHlfiKUoeVIMyK5NF+up6TRg71t/8Bny6Q==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@smithy/property-provider": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.8.tgz", + "integrity": "sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@smithy/types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz", + "integrity": "sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.511.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.511.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.511.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.511.0", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-stream": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.513.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sts": "3.513.0", + "@aws-sdk/credential-provider-env": "3.511.0", + "@aws-sdk/credential-provider-process": "3.511.0", + "@aws-sdk/credential-provider-sso": "3.513.0", + "@aws-sdk/credential-provider-web-identity": "3.513.0", + "@aws-sdk/types": "3.511.0", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.514.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.511.0", "@aws-sdk/credential-provider-http": "3.511.0", "@aws-sdk/credential-provider-ini": "3.513.0", "@aws-sdk/credential-provider-process": "3.511.0", @@ -1558,51 +4206,1190 @@ "tslib": "^2.5.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.511.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.511.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.513.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.513.0", + "@aws-sdk/token-providers": "3.513.0", + "@aws-sdk/types": "3.511.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.513.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sts": "3.513.0", + "@aws-sdk/types": "3.511.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.685.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.685.0.tgz", + "integrity": "sha512-pIXNNwPG2KnzjGYYbADquEkROuKxAJxuWt87TJO7LCFqKwb5l6h0Mc7yc4j13zxOVd/EhXD0VsPeqnobDklUoQ==", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.685.0", + "@aws-sdk/client-sso": "3.682.0", + "@aws-sdk/client-sts": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-cognito-identity": "3.685.0", + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-ini": "3.682.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/client-sso": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.682.0.tgz", + "integrity": "sha512-PYH9RFUMYLFl66HSBq4tIx6fHViMLkhJHTYJoJONpBs+Td+NwVJ895AdLtDsBIhMS0YseCbPpuyjUCJgsUrwUw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.682.0.tgz", + "integrity": "sha512-ZPZ7Y/r/w3nx/xpPzGSqSQsB090Xk5aZZOH+WBhTDn/pBEuim09BYXCLzvvxb7R7NnuoQdrTJiwimdJAhHl7ZQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/client-sts": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.682.0.tgz", + "integrity": "sha512-xKuo4HksZ+F8m9DOfx/ZuWNhaPuqZFPwwy0xqcBT6sWH7OAuBjv/fnpOTzyQhpVTWddlf+ECtMAMrxjxuOExGQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-node": "3.682.0", + "@aws-sdk/middleware-host-header": "3.679.0", + "@aws-sdk/middleware-logger": "3.679.0", + "@aws-sdk/middleware-recursion-detection": "3.679.0", + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/region-config-resolver": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@aws-sdk/util-user-agent-browser": "3.679.0", + "@aws-sdk/util-user-agent-node": "3.682.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/core": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.679.0.tgz", + "integrity": "sha512-CS6PWGX8l4v/xyvX8RtXnBisdCa5+URzKd0L6GvHChype9qKUVxO/Gg6N/y43Hvg7MNWJt9FBPNWIxUB+byJwg==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-middleware": "^3.0.7", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.679.0.tgz", + "integrity": "sha512-EdlTYbzMm3G7VUNAMxr9S1nC1qUNqhKlAxFU8E7cKsAe8Bp29CD5HAs3POc56AVo9GC4yRIS+/mtlZSmrckzUA==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.679.0.tgz", + "integrity": "sha512-ZoKLubW5DqqV1/2a3TSn+9sSKg0T8SsYMt1JeirnuLJF0mCoYFUaWMyvxxKuxPoqvUsaycxKru4GkpJ10ltNBw==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-stream": "^3.1.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.682.0.tgz", + "integrity": "sha512-6eqWeHdK6EegAxqDdiCi215nT3QZPwukgWAYuVxNfJ/5m0/P7fAzF+D5kKVgByUvGJEbq/FEL8Fw7OBe64AA+g==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.682.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.682.0.tgz", + "integrity": "sha512-HSmDqZcBVZrTctHCT9m++vdlDfJ1ARI218qmZa+TZzzOFNpKWy6QyHMEra45GB9GnkkMmV6unoDSPMuN0AqcMg==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.679.0", + "@aws-sdk/credential-provider-http": "3.679.0", + "@aws-sdk/credential-provider-ini": "3.682.0", + "@aws-sdk/credential-provider-process": "3.679.0", + "@aws-sdk/credential-provider-sso": "3.682.0", + "@aws-sdk/credential-provider-web-identity": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.679.0.tgz", + "integrity": "sha512-u/p4TV8kQ0zJWDdZD4+vdQFTMhkDEJFws040Gm113VHa/Xo1SYOjbpvqeuFoz6VmM0bLvoOWjxB9MxnSQbwKpQ==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.682.0.tgz", + "integrity": "sha512-h7IH1VsWgV6YAJSWWV6y8uaRjGqLY3iBpGZlXuTH/c236NMLaNv+WqCBLeBxkFGUb2WeQ+FUPEJDCD69rgLIkg==", + "dependencies": { + "@aws-sdk/client-sso": "3.682.0", + "@aws-sdk/core": "3.679.0", + "@aws-sdk/token-providers": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.679.0.tgz", + "integrity": "sha512-a74tLccVznXCaBefWPSysUcLXYJiSkeUmQGtalNgJ1vGkE36W5l/8czFiiowdWdKWz7+x6xf0w+Kjkjlj42Ung==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.679.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.679.0.tgz", + "integrity": "sha512-y176HuQ8JRY3hGX8rQzHDSbCl9P5Ny9l16z4xmaiLo+Qfte7ee4Yr3yaAKd7GFoJ3/Mhud2XZ37fR015MfYl2w==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/middleware-logger": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.679.0.tgz", + "integrity": "sha512-0vet8InEj7nvIvGKk+ch7bEF5SyZ7Us9U7YTEgXPrBNStKeRUsgwRm0ijPWWd0a3oz2okaEwXsFl7G/vI0XiEA==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.679.0.tgz", + "integrity": "sha512-sQoAZFsQiW/LL3DfKMYwBoGjYDEnMbA9WslWN8xneCmBAwKo6IcSksvYs23PP8XMIoBGe2I2J9BSr654XWygTQ==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.682.0.tgz", + "integrity": "sha512-7TyvYR9HdGH1/Nq0eeApUTM4izB6rExiw87khVYuJwZHr6FmvIL1FsOVFro/4WlXa0lg4LiYOm/8H8dHv+fXTg==", + "dependencies": { + "@aws-sdk/core": "3.679.0", + "@aws-sdk/types": "3.679.0", + "@aws-sdk/util-endpoints": "3.679.0", + "@smithy/core": "^2.4.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.679.0.tgz", + "integrity": "sha512-Ybx54P8Tg6KKq5ck7uwdjiKif7n/8g1x+V0V9uTjBjRWqaIgiqzXwKWoPj6NCNkE7tJNtqI4JrNxp/3S3HvmRw==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/token-providers": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.679.0.tgz", + "integrity": "sha512-1/+Zso/x2jqgutKixYFQEGli0FELTgah6bm7aB+m2FAWH4Hz7+iMUsazg6nSWm714sG9G3h5u42Dmpvi9X6/hA==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.679.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/types": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.679.0.tgz", + "integrity": "sha512-NwVq8YvInxQdJ47+zz4fH3BRRLC6lL+WLkvr242PVBbUOLRyK/lkwHlfiKUoeVIMyK5NF+up6TRg71t/8Bny6Q==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/util-endpoints": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.679.0.tgz", + "integrity": "sha512-YL6s4Y/1zC45OvddvgE139fjeWSKKPgLlnfrvhVL7alNyY9n7beR4uhoDpNrt5mI6sn9qiBF17790o+xLAXjjg==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "@smithy/util-endpoints": "^2.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.679.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.679.0.tgz", + "integrity": "sha512-CusSm2bTBG1kFypcsqU8COhnYc6zltobsqs3nRrvYqYaOqtMnuE46K4XTWpnzKgwDejgZGOE+WYyprtAxrPvmQ==", + "dependencies": { + "@aws-sdk/types": "3.679.0", + "@smithy/types": "^3.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.682.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.682.0.tgz", + "integrity": "sha512-so5s+j0gPoTS0HM4HPL+G0ajk0T6cQAg8JXzRgvyiQAxqie+zGCZAV3VuVeMNWMVbzsgZl0pYZaatPFTLG/AxA==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.682.0", + "@aws-sdk/types": "3.679.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/abort-controller": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.6.tgz", + "integrity": "sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/config-resolver": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.10.tgz", + "integrity": "sha512-Uh0Sz9gdUuz538nvkPiyv1DZRX9+D15EKDtnQP5rYVAzM/dnYk3P8cg73jcxyOitPgT3mE3OVj7ky7sibzHWkw==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.9", + "@smithy/types": "^3.6.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/core": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.1.tgz", + "integrity": "sha512-DujtuDA7BGEKExJ05W5OdxCoyekcKT3Rhg1ZGeiUWaz2BJIWXjZmsG/DIP4W48GHno7AQwRsaCb8NcBgH3QZpg==", + "dependencies": { + "@smithy/middleware-serde": "^3.0.8", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-stream": "^3.2.1", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/core/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/credential-provider-imds": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.5.tgz", + "integrity": "sha512-4FTQGAsuwqTzVMmiRVTn0RR9GrbRfkP0wfu/tXWVHd2LgNpTY0uglQpIScXK4NaEyXbB3JmZt8gfVqO50lP8wg==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.8", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz", + "integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/hash-node": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.8.tgz", + "integrity": "sha512-tlNQYbfpWXHimHqrvgo14DrMAgUBua/cNoz9fMYcDmYej7MAmUcjav/QKQbFc3NrcPxeJ7QClER4tWZmfwoPng==", + "dependencies": { + "@smithy/types": "^3.6.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/hash-node/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/invalid-dependency": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.8.tgz", + "integrity": "sha512-7Qynk6NWtTQhnGTTZwks++nJhQ1O54Mzi7fz4PqZOiYXb4Z1Flpb2yRvdALoggTS8xjtohWUM+RygOtB30YL3Q==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/middleware-content-length": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.10.tgz", + "integrity": "sha512-T4dIdCs1d/+/qMpwhJ1DzOhxCZjZHbHazEPJWdB4GDi2HjIZllVzeBEcdJUN0fomV8DURsgOyrbEUzg3vzTaOg==", + "dependencies": { + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/middleware-endpoint": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.1.tgz", + "integrity": "sha512-wWO3xYmFm6WRW8VsEJ5oU6h7aosFXfszlz3Dj176pTij6o21oZnzkCLzShfmRaaCHDkBXWBdO0c4sQAvLFP6zA==", + "dependencies": { + "@smithy/core": "^2.5.1", + "@smithy/middleware-serde": "^3.0.8", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.9", + "@smithy/types": "^3.6.0", + "@smithy/url-parser": "^3.0.8", + "@smithy/util-middleware": "^3.0.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/middleware-retry": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.25.tgz", + "integrity": "sha512-m1F70cPaMBML4HiTgCw5I+jFNtjgz5z5UdGnUbG37vw6kh4UvizFYjqJGHvicfgKMkDL6mXwyPp5mhZg02g5sg==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.5", + "@smithy/service-error-classification": "^3.0.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-retry": "^3.0.8", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.511.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/middleware-serde": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.8.tgz", + "integrity": "sha512-Xg2jK9Wc/1g/MBMP/EUn2DLspN8LNt+GMe7cgF+Ty3vl+Zvu+VeZU5nmhveU+H8pxyTsjrAkci8NqY6OuvZnjA==", "dependencies": { - "@aws-sdk/types": "3.511.0", - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", - "tslib": "^2.5.0" + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.513.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/middleware-stack": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.8.tgz", + "integrity": "sha512-d7ZuwvYgp1+3682Nx0MD3D/HtkmZd49N3JUndYWQXfRZrYEnCWYc8BHcNmVsPAp9gKvlurdg/mubE6b/rPS9MA==", "dependencies": { - "@aws-sdk/client-sso": "3.513.0", - "@aws-sdk/token-providers": "3.513.0", - "@aws-sdk/types": "3.511.0", - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", - "tslib": "^2.5.0" + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/node-config-provider": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.9.tgz", + "integrity": "sha512-qRHoah49QJ71eemjuS/WhUXB+mpNtwHRWQr77J/m40ewBVVwvo52kYAmb7iuaECgGTTcYxHS4Wmewfwy++ueew==", + "dependencies": { + "@smithy/property-provider": "^3.1.8", + "@smithy/shared-ini-file-loader": "^3.1.9", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/node-http-handler": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.5.tgz", + "integrity": "sha512-PkOwPNeKdvX/jCpn0A8n9/TyoxjGZB8WVoJmm9YzsnAgggTj4CrjpRHlTQw7dlLZ320n1mY1y+nTRUDViKi/3w==", + "dependencies": { + "@smithy/abort-controller": "^3.1.6", + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/property-provider": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.8.tgz", + "integrity": "sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/protocol-http": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.5.tgz", + "integrity": "sha512-hsjtwpIemmCkm3ZV5fd/T0bPIugW1gJXwZ/hpuVubt2hEUApIoUTrf6qIdh9MAWlw0vjMrA1ztJLAwtNaZogvg==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/querystring-builder": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.8.tgz", + "integrity": "sha512-btYxGVqFUARbUrN6VhL9c3dnSviIwBYD9Rz1jHuN1hgh28Fpv2xjU1HeCeDJX68xctz7r4l1PBnFhGg1WBBPuA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/querystring-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.8.tgz", + "integrity": "sha512-BtEk3FG7Ks64GAbt+JnKqwuobJNX8VmFLBsKIwWr1D60T426fGrV2L3YS5siOcUhhp6/Y6yhBw1PSPxA5p7qGg==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/service-error-classification": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.8.tgz", + "integrity": "sha512-uEC/kCCFto83bz5ZzapcrgGqHOh/0r69sZ2ZuHlgoD5kYgXJEThCoTuw/y1Ub3cE7aaKdznb+jD9xRPIfIwD7g==", + "dependencies": { + "@smithy/types": "^3.6.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.9.tgz", + "integrity": "sha512-/+OsJRNtoRbtsX0UpSgWVxFZLsJHo/4sTr+kBg/J78sr7iC+tHeOvOJrS5hCpVQ6sWBbhWLp1UNiuMyZhE6pmA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/signature-v4": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.1.tgz", + "integrity": "sha512-NsV1jF4EvmO5wqmaSzlnTVetemBS3FZHdyc5CExbDljcyJCEEkJr8ANu2JvtNbVg/9MvKAWV44kTrGS+Pi4INg==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.8", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/signature-v4/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/smithy-client": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.2.tgz", + "integrity": "sha512-dxw1BDxJiY9/zI3cBqfVrInij6ShjpV4fmGHesGZZUiP9OSE/EVfdwdRz0PgvkEvrZHpsj2htRaHJfftE8giBA==", + "dependencies": { + "@smithy/core": "^2.5.1", + "@smithy/middleware-endpoint": "^3.2.1", + "@smithy/middleware-stack": "^3.0.8", + "@smithy/protocol-http": "^4.1.5", + "@smithy/types": "^3.6.0", + "@smithy/util-stream": "^3.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz", + "integrity": "sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/url-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.8.tgz", + "integrity": "sha512-4FdOhwpTW7jtSFWm7SpfLGKIBC9ZaTKG5nBF0wK24aoQKQyDIKUw3+KFWCQ9maMzrgTJIuOvOnsV2lLGW5XjTg==", + "dependencies": { + "@smithy/querystring-parser": "^3.0.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-base64/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.25.tgz", + "integrity": "sha512-fRw7zymjIDt6XxIsLwfJfYUfbGoO9CmCJk6rjJ/X5cd20+d2Is7xjU5Kt/AiDt6hX8DAf5dztmfP5O82gR9emA==", + "dependencies": { + "@smithy/property-provider": "^3.1.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.25.tgz", + "integrity": "sha512-H3BSZdBDiVZGzt8TG51Pd2FvFO0PAx/A0mJ0EH8a13KJ6iUCdYnw/Dk/MdC1kTd0eUuUGisDFaxXVXo4HHFL1g==", + "dependencies": { + "@smithy/config-resolver": "^3.0.10", + "@smithy/credential-provider-imds": "^3.2.5", + "@smithy/node-config-provider": "^3.1.9", + "@smithy/property-provider": "^3.1.8", + "@smithy/smithy-client": "^3.4.2", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-endpoints": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.4.tgz", + "integrity": "sha512-kPt8j4emm7rdMWQyL0F89o92q10gvCUa6sBkBtDJ7nV2+P7wpXczzOfoDJ49CKXe5CCqb8dc1W+ZdLlrKzSAnQ==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.9", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-middleware": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.8.tgz", + "integrity": "sha512-p7iYAPaQjoeM+AKABpYWeDdtwQNxasr4aXQEA/OmbOaug9V0odRVDy3Wx4ci8soljE/JXQo+abV0qZpW8NX0yA==", + "dependencies": { + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-retry": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.8.tgz", + "integrity": "sha512-TCEhLnY581YJ+g1x0hapPz13JFqzmh/pMWL2KEFASC51qCfw3+Y47MrTmea4bUE5vsdxQ4F6/KFbUeSz22Q1ow==", + "dependencies": { + "@smithy/service-error-classification": "^3.0.8", + "@smithy/types": "^3.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.2.1.tgz", + "integrity": "sha512-R3ufuzJRxSJbE58K9AEnL/uSZyVdHzud9wLS8tIbXclxKzoe09CRohj2xV8wpx5tj7ZbiJaKYcutMm1eYgz/0A==", + "dependencies": { + "@smithy/fetch-http-handler": "^4.0.0", + "@smithy/node-http-handler": "^3.2.5", + "@smithy/types": "^3.6.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-stream/node_modules/@smithy/fetch-http-handler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz", + "integrity": "sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==", + "dependencies": { + "@smithy/protocol-http": "^4.1.5", + "@smithy/querystring-builder": "^3.0.8", + "@smithy/types": "^3.6.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-stream/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.513.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", "dependencies": { - "@aws-sdk/client-sts": "3.513.0", - "@aws-sdk/types": "3.511.0", - "@smithy/property-provider": "^2.1.1", - "@smithy/types": "^2.9.1", - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, "node_modules/@aws-sdk/lib-storage": { @@ -4284,12 +8071,73 @@ "version": "0.3.1", "license": "MIT" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -4298,6 +8146,276 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "dev": true, @@ -6168,6 +10286,14 @@ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "dev": true }, + "node_modules/@ory/kratos-client": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-1.2.1.tgz", + "integrity": "sha512-HvipnVQotCKjEQC9I9DPjSlfBEww4pjDycMAKdUPj3g/0WkNSq6wbPDyqeclFz99rsOOsFMcpOO8qiCYHSgQeA==", + "dependencies": { + "axios": "^1.6.1" + } + }, "node_modules/@pixi/accessibility": { "version": "6.5.10", "license": "MIT", @@ -7407,8 +11533,7 @@ }, "node_modules/@radix-ui/react-menubar": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.1.tgz", - "integrity": "sha512-V05Hryq/BE2m+rs8d5eLfrS0jmSWSDHEbG7jEyLA5D5J9jTvWj/o3v3xDN9YsOlH6QIkJgiaNDaP+S4T1rdykw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collection": "1.1.0", @@ -7438,13 +11563,11 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + "license": "MIT" }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-arrow": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", - "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.0.0" }, @@ -7465,8 +11588,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-collection": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", - "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.0", @@ -7490,8 +11612,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7504,8 +11625,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-context": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7518,8 +11638,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-direction": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7532,8 +11651,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", - "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", @@ -7558,8 +11676,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-focus-guards": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", - "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7572,8 +11689,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-focus-scope": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", - "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", @@ -7596,8 +11712,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-id": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -7613,8 +11728,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-menu": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.1.tgz", - "integrity": "sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collection": "1.1.0", @@ -7652,8 +11766,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-popper": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", - "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", + "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.0", @@ -7683,8 +11796,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-portal": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", - "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -7706,8 +11818,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-presence": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", - "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -7729,8 +11840,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-primitive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.1.0" }, @@ -7751,8 +11861,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", - "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collection": "1.1.0", @@ -7781,8 +11890,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -7798,8 +11906,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7812,8 +11919,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -7829,8 +11935,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -7846,8 +11951,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7860,8 +11964,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.0" }, @@ -7877,8 +11980,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-size": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -7894,13 +11996,11 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/rect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" + "license": "MIT" }, "node_modules/@radix-ui/react-menubar/node_modules/react-remove-scroll": { "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", - "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.4", "react-style-singleton": "^2.2.1", @@ -8311,8 +12411,7 @@ }, "node_modules/@radix-ui/react-toggle": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", - "integrity": "sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-primitive": "2.0.0", @@ -8335,8 +12434,7 @@ }, "node_modules/@radix-ui/react-toggle-group": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.0.tgz", - "integrity": "sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-context": "1.1.0", @@ -8363,13 +12461,11 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + "license": "MIT" }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-collection": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", - "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.0", @@ -8393,8 +12489,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8407,8 +12502,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-context": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8421,8 +12515,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-direction": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8435,8 +12528,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-id": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -8452,8 +12544,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.1.0" }, @@ -8474,8 +12565,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", - "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collection": "1.1.0", @@ -8504,8 +12594,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -8521,8 +12610,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8535,8 +12623,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -8552,8 +12639,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8566,13 +12652,11 @@ }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + "license": "MIT" }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8585,8 +12669,7 @@ }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.1.0" }, @@ -8607,8 +12690,7 @@ }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -8624,8 +12706,7 @@ }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8638,8 +12719,7 @@ }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -8838,30 +12918,15 @@ "node": ">=14.0.0" } }, - "node_modules/@rollup/plugin-virtual": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, "node_modules/@rollup/pluginutils": { - "version": "5.1.0", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" + "picomatch": "^4.0.2" }, "engines": { "node": ">=14.0.0" @@ -8877,8 +12942,21 @@ }, "node_modules/@rollup/pluginutils/node_modules/estree-walker": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.22.4", @@ -8906,11 +12984,10 @@ }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", - "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -9013,9 +13090,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", - "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -10597,26 +14674,6 @@ "@svgr/core": "*" } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.4.1", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@swc/helpers": { "version": "0.4.36", "dev": true, @@ -10626,11 +14683,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@swc/types": { - "version": "0.1.5", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@szhsin/react-menu": { "version": "4.1.0", "license": "MIT", @@ -11134,6 +15186,7 @@ }, "node_modules/@types/react-avatar-editor": { "version": "13.0.2", + "dev": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -11156,6 +15209,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-linkify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/react-linkify/-/react-linkify-1.0.4.tgz", + "integrity": "sha512-NOMw4X3kjvjY0lT5kXQdxZCXpPNi2hOuuqG+Kz+5EOQpi9rDUJJDitdE1j2JRNmrTnNIjrLnYG0HKyuOWN/uKA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "license": "MIT", @@ -11260,6 +15322,7 @@ }, "node_modules/@types/ua-parser-js": { "version": "0.7.39", + "dev": true, "license": "MIT" }, "node_modules/@types/unist": { @@ -14758,9 +18821,10 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.20.2", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -14768,29 +18832,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/esbuild-wasm": { @@ -15991,6 +20055,8 @@ }, "node_modules/fast-xml-parser": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", "funding": [ { "type": "github", @@ -16001,7 +20067,6 @@ "url": "https://paypal.me/naturalintelligence" } ], - "license": "MIT", "dependencies": { "strnum": "^1.0.5" }, @@ -16750,16 +20815,17 @@ } }, "node_modules/happy-dom": { - "version": "13.3.8", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.10.2.tgz", + "integrity": "sha512-NbA5XrSovenJIIcfixCREX3ZnV7yHP4phhbfuxxf4CPn+LZpz/jIM9EqJ2DrPwgVDSMoAKH3pZwQvkbsSiCrUw==", "devOptional": true, - "license": "MIT", "dependencies": { "entities": "^4.5.0", "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, "node_modules/has-bigints": { @@ -19469,6 +23535,14 @@ "version": "1.2.4", "license": "MIT" }, + "node_modules/linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/load-bmfont": { "version": "1.4.1", "dev": true, @@ -21704,7 +25778,7 @@ } }, "node_modules/openai": { - "version": "4.61.0", + "version": "4.60.0", "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", @@ -22138,6 +26212,11 @@ "node": ">= 0.8" } }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==" + }, "node_modules/pascal-case": { "version": "1.1.2", "license": "MIT", @@ -22367,8 +26446,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.0", - "license": "ISC" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -22405,6 +26485,16 @@ "pixelmatch": "bin/pixelmatch" } }, + "node_modules/pixi-viewport": { + "version": "4.38.0", + "license": "MIT", + "peerDependencies": { + "@pixi/display": "^6.5.8", + "@pixi/interaction": "^6.5.8", + "@pixi/math": "^6.5.8", + "@pixi/ticker": "^6.5.8" + } + }, "node_modules/pixi.js": { "version": "6.5.10", "license": "MIT", @@ -22568,7 +26658,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -22583,11 +26675,10 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -23236,6 +27327,15 @@ "version": "18.2.0", "license": "MIT" }, + "node_modules/react-linkify": { + "version": "1.0.0-alpha", + "resolved": "https://registry.npmjs.org/react-linkify/-/react-linkify-1.0.0-alpha.tgz", + "integrity": "sha512-7gcIUvJkAXXttt1fmBK9cwn+1jTa4hbKLGCZ9J1U6EOkyb2/+LKL1Z28d9rtDLMnpvImlNlLPdTPooorl5cpmg==", + "dependencies": { + "linkify-it": "^2.0.3", + "tlds": "^1.199.0" + } + }, "node_modules/react-markdown": { "version": "9.0.1", "license": "MIT", @@ -23805,8 +27905,7 @@ }, "node_modules/rollup": { "version": "4.22.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", - "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -23837,6 +27936,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/run-async": { "version": "3.0.0", "dev": true, @@ -24385,8 +28496,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "license": "BSD-3-Clause", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -24780,7 +28892,8 @@ }, "node_modules/strnum": { "version": "1.0.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" }, "node_modules/style-to-object": { "version": "1.0.6", @@ -25367,6 +29480,14 @@ "upper-case": "^1.0.3" } }, + "node_modules/tlds": { + "version": "1.255.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz", + "integrity": "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==", + "bin": { + "tlds": "bin.js" + } + }, "node_modules/tmpl": { "version": "1.0.5", "dev": true, @@ -25926,6 +30047,11 @@ "node": "*" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "node_modules/ufo": { "version": "1.4.0", "license": "MIT" @@ -26580,13 +30706,13 @@ } }, "node_modules/vite": { - "version": "5.2.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.14.tgz", - "integrity": "sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==", + "version": "5.4.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", + "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -26605,6 +30731,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -26622,6 +30749,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -26654,9 +30784,10 @@ } }, "node_modules/vite-plugin-checker": { - "version": "0.6.4", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.8.0.tgz", + "integrity": "sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", "ansi-escapes": "^4.3.0", @@ -26666,7 +30797,6 @@ "fast-glob": "^3.2.7", "fs-extra": "^11.1.0", "npm-run-path": "^4.0.1", - "semver": "^7.5.0", "strip-ansi": "^6.0.0", "tiny-invariant": "^1.1.0", "vscode-languageclient": "^7.0.0", @@ -26678,6 +30808,7 @@ "node": ">=14.16" }, "peerDependencies": { + "@biomejs/biome": ">=1.7", "eslint": ">=7", "meow": "^9.0.0", "optionator": "^0.9.1", @@ -26686,9 +30817,12 @@ "vite": ">=2.0.0", "vls": "*", "vti": "*", - "vue-tsc": ">=1.3.9" + "vue-tsc": "~2.1.6" }, "peerDependenciesMeta": { + "@biomejs/biome": { + "optional": true + }, "eslint": { "optional": true }, @@ -26752,31 +30886,6 @@ "node": ">= 12" } }, - "node_modules/vite-plugin-checker/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/vite-plugin-checker/node_modules/semver": { - "version": "7.6.0", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/vite-plugin-checker/node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -26821,90 +30930,18 @@ "dev": true, "license": "MIT" }, - "node_modules/vite-plugin-checker/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, "node_modules/vite-plugin-svgr": { - "version": "4.2.0", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", + "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==", "dev": true, - "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.0.5", + "@rollup/pluginutils": "^5.1.3", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, "peerDependencies": { - "vite": "^2.6.0 || 3 || 4 || 5" - } - }, - "node_modules/vite-plugin-top-level-await": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/plugin-virtual": "^3.0.2", - "@swc/core": "^1.3.100", - "uuid": "^9.0.1" - }, - "peerDependencies": { - "vite": ">=2.8" - } - }, - "node_modules/vite-plugin-top-level-await/node_modules/@swc/core": { - "version": "1.4.1", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.2", - "@swc/types": "^0.1.5" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.4.1", - "@swc/core-darwin-x64": "1.4.1", - "@swc/core-linux-arm-gnueabihf": "1.4.1", - "@swc/core-linux-arm64-gnu": "1.4.1", - "@swc/core-linux-arm64-musl": "1.4.1", - "@swc/core-linux-x64-gnu": "1.4.1", - "@swc/core-linux-x64-musl": "1.4.1", - "@swc/core-win32-arm64-msvc": "1.4.1", - "@swc/core-win32-ia32-msvc": "1.4.1", - "@swc/core-win32-x64-msvc": "1.4.1" - }, - "peerDependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/vite-plugin-top-level-await/node_modules/@swc/helpers": { - "version": "0.5.6", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/vite-plugin-wasm": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "peerDependencies": { - "vite": "^2 || ^3 || ^4 || ^5" + "vite": ">=2.6.0" } }, "node_modules/vite-tsconfig-paths": { @@ -27798,13 +31835,16 @@ } }, "quadratic-api": { - "version": "0.5.2", + "version": "0.5.4", "hasInstallScript": true, "dependencies": { - "@anthropic-ai/sdk": "^0.27.2", + "@anthropic-ai/bedrock-sdk": "^0.11.2", + "@anthropic-ai/sdk": "^0.32.1", + "@aws-sdk/client-bedrock-runtime": "^3.682.0", "@aws-sdk/client-s3": "^3.427.0", "@aws-sdk/client-secrets-manager": "^3.441.0", "@aws-sdk/s3-request-presigner": "^3.427.0", + "@ory/kratos-client": "^1.2.1", "@prisma/client": "^4.12.0", "@sendgrid/mail": "^8.1.0", "@sentry/node": "^7.50.0", @@ -27875,7 +31915,7 @@ } }, "quadratic-client": { - "version": "0.5.2", + "version": "0.5.4", "dependencies": { "@amplitude/analytics-browser": "^1.9.4", "@auth0/auth0-spa-js": "^2.1.0", @@ -27885,6 +31925,7 @@ "@monaco-editor/react": "^4.3.1", "@mui/icons-material": "^5.2.0", "@mui/material": "^5.2.2", + "@ory/kratos-client": "^1.2.1", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", @@ -27910,8 +31951,6 @@ "@sentry/vite-plugin": "^2.22.6", "@szhsin/react-menu": "^4.0.2", "@tailwindcss/container-queries": "^0.1.1", - "@types/react-avatar-editor": "^13.0.2", - "@types/ua-parser-js": "^0.7.39", "bignumber.js": "^9.1.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -27925,6 +31964,7 @@ "mixpanel-browser": "^2.46.0", "monaco-editor": "^0.40.0", "next-themes": "^0.3.0", + "partial-json": "^0.1.7", "pixi-viewport": "^4.37.0", "pixi.js": "^6.5.10", "react": "^18.2.0", @@ -27934,6 +31974,7 @@ "react-device-detect": "^2.2.2", "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", + "react-linkify": "^1.0.0-alpha", "react-markdown": "^9.0.1", "react-router-dom": "^6.26.1", "recoil": "^0.7.7", @@ -27955,8 +31996,11 @@ "@types/fontfaceobserver": "^2.1.0", "@types/mixpanel-browser": "^2.38.1", "@types/react": "^18.2.16", + "@types/react-avatar-editor": "^13.0.2", "@types/react-color": "^3.0.6", "@types/react-dom": "^18.2.7", + "@types/react-linkify": "^1.0.4", + "@types/ua-parser-js": "^0.7.39", "@types/uuid": "^9.0.1", "@vitejs/plugin-react": "^4.2.0", "@vitest/web-worker": "^1.3.1", @@ -27967,7 +32011,7 @@ "fake-indexeddb": "^5.0.2", "fontkit": "^2.0.2", "fs-extra": "^11.1.0", - "happy-dom": "^13.0.0", + "happy-dom": "^15.10.2", "jest-environment-jsdom": "^29.7.0", "msdf-bmfont-xml": "^2.7.0", "msw": "^2.4.11", @@ -27979,11 +32023,9 @@ "serve": "^14.2.0", "tailwindcss": "^3.3.5", "typescript": "^5.4.5", - "vite": "^5.2.14", - "vite-plugin-checker": "^0.6.2", - "vite-plugin-svgr": "^4.2.0", - "vite-plugin-top-level-await": "^1.4.1", - "vite-plugin-wasm": "^3.3.0", + "vite": "^5.4.10", + "vite-plugin-checker": "^0.8.0", + "vite-plugin-svgr": "^4.3.0", "vite-tsconfig-paths": "^4.2.1", "vitest": "^1.3.1", "vscode-languageserver-types": "^3.17.5" @@ -28026,17 +32068,6 @@ "node": ">=10" } }, - "quadratic-client/node_modules/pixi-viewport": { - "version": "4.38.0", - "resolved": "https://registry.npmjs.org/pixi-viewport/-/pixi-viewport-4.38.0.tgz", - "integrity": "sha512-TGj6Ymk/BU0wZcW4c1eP4e96aETJmB7jhjBflMjQU06/ZHPy7qHw8JyDqZ+C84SEg0ewCHjDNZ2vgR3Kjk74BQ==", - "peerDependencies": { - "@pixi/display": "^6.5.8", - "@pixi/interaction": "^6.5.8", - "@pixi/math": "^6.5.8", - "@pixi/ticker": "^6.5.8" - } - }, "quadratic-client/node_modules/prettier-plugin-tailwindcss": { "version": "0.4.1", "dev": true, @@ -28163,7 +32194,7 @@ }, "quadratic-rust-client": {}, "quadratic-shared": { - "version": "0.5.2", + "version": "0.5.4", "license": "ISC", "dependencies": { "typescript": "^5.6.2", diff --git a/package.json b/package.json index 643c913f3a..d685418d0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "quadratic", - "version": "0.5.2", + "version": "0.5.4", "author": { "name": "David Kircos", "email": "david@quadratichq.com", @@ -62,6 +62,7 @@ "gen:pyright:worker": "npm run gen:pyright:worker --workspace=quadratic-kernels/python-wasm" }, "dependencies": { + "@ory/kratos-client": "^1.2.1", "tsc": "^2.0.4", "vitest": "^1.5.0", "zod": "^3.23.8" diff --git a/quadratic-api/.env.docker b/quadratic-api/.env.docker index 96c46112c0..f4f8144b51 100644 --- a/quadratic-api/.env.docker +++ b/quadratic-api/.env.docker @@ -1,6 +1,20 @@ +CORS="*" +DATABASE_URL="postgresql://postgres:postgres@host.docker.internal:5432/postgres" ENVIRONMENT=docker -DATABASE_URL='postgresql://postgres:postgres@postgres:5432/postgres' -AWS_S3_ENDPOINT=http://localstack:4566 +STRIPE_SECRET_KEY=STRIPE_SECRET_KEY +STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET +OPENAI_API_KEY= +M2M_AUTH_TOKEN=M2M_AUTH_TOKEN # Hex string to be used as the key for enctyption, use npm run key:generate ENCRYPTION_KEY=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc + +# Auth +AUTH_TYPE=auth0 +AWS_S3_ENDPOINT=http://localstack:4566 + +# Storage +STORAGE_TYPE=s3 + +# Admin +LICENSE_KEY=LICENSE_KEY diff --git a/quadratic-api/.env.example b/quadratic-api/.env.example index fd1ce14945..466ea24bf8 100644 --- a/quadratic-api/.env.example +++ b/quadratic-api/.env.example @@ -4,27 +4,37 @@ DATABASE_URL=postgresql://postgres:postgres@0.0.0.0:5432/postgres CORS='*' +OPENAI_API_KEY= +ANTHROPIC_API_KEY= + +SENTRY_DSN= + +M2M_AUTH_TOKEN=M2M_AUTH_TOKEN + +STRIPE_SECRET_KEY=STRIPE_SECRET_KEY +STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET + +ENCRYPTION_KEY=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc + +# Auth +AUTH_TYPE=auth0 # auth0 or ory AUTH0_JWKS_URI=https://quadratic-community.us.auth0.com/.well-known/jwks.json AUTH0_ISSUER=https://quadratic-community.us.auth0.com/ AUTH0_DOMAIN=quadratic-community.us.auth0.com AUTH0_CLIENT_ID=DCPCvqyU5Q0bJD8Q3QmJEoV48x1zLH7W AUTH0_CLIENT_SECRET=94dp3PDcxlI9ZDqBSvkdjQHWgGdx0ZSeyTr5-Rn3Kcts-ZyTdj1FLlJjCyqrTXEG AUTH0_AUDIENCE=community-quadratic +ORY_JWKS_URI='http://host.docker.internal:3000/.well-known/jwks.json' +ORY_ADMIN_HOST=http://0.0.0.0:4434 +# Storage +STORAGE_TYPE=s3 # s3 or file-system +QUADRATIC_FILE_URI=http://localhost:3002 +QUADRATIC_FILE_URI_PUBLIC=http://localhost:3002 AWS_S3_REGION=us-east-2 AWS_S3_ACCESS_KEY_ID=test AWS_S3_SECRET_ACCESS_KEY=test AWS_S3_BUCKET_NAME=quadratic-api-docker -AWS_S3_ENDPOINT=http://0.0.0.0:4566 -ANTHROPIC_API_KEY= -OPENAI_API_KEY= - -SENTRY_DSN= - -M2M_AUTH_TOKEN=M2M_AUTH_TOKEN - -STRIPE_SECRET_KEY=STRIPE_SECRET_KEY -STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET - -ENCRYPTION_KEY=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc +# Admin +LICENSE_KEY=LICENSE_KEY diff --git a/quadratic-api/.env.test b/quadratic-api/.env.test index 0aa0efdebc..27d0e2cfca 100644 --- a/quadratic-api/.env.test +++ b/quadratic-api/.env.test @@ -1,16 +1,32 @@ +# DEBUG=express:* + DATABASE_URL='postgresql://prisma:prisma@localhost:5433/quadratic-api’' +M2M_AUTH_TOKEN=M2M_AUTH_TOKEN +STRIPE_SECRET_KEY=STRIPE_SECRET_KEY +STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET + +# Hex string to be used as the key for enctyption, use npm run key:generate +ENCRYPTION_KEY=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc + +# Auth +AUTH_TYPE=auth0 # auth0 or ory AUTH0_JWKS_URI='https://dev.us.auth0.com/.well-known/jwks.json' AUTH0_ISSUER='https://auth-dev.quadratic.to/' AUTH0_CLIENT_ID="AUTH0_CLIENT_ID" AUTH0_CLIENT_SECRET="AUTH0_CLIENT_SECRET" AUTH0_DOMAIN="AUTH0_DOMAIN" +ORY_JWKS_URI='http://host.docker.internal:3000/.well-known/jwks.json' +ORY_ADMIN_HOST=http://0.0.0.0:4434 + +# Storage +STORAGE_TYPE=s3 # s3 or file-system +QUADRATIC_FILE_URI=http://localhost:3002 +QUADRATIC_FILE_URI_PUBLIC=http://localhost:3002 AWS_S3_REGION=us-west-2 AWS_S3_BUCKET_NAME=AWS_S3_BUCKET_NAME AWS_S3_ACCESS_KEY_ID=AWS_S3_ACCESS_KEY_ID AWS_S3_SECRET_ACCESS_KEY=AWS_S3_SECRET_ACCESS_KEY -M2M_AUTH_TOKEN=M2M_AUTH_TOKEN -STRIPE_SECRET_KEY=STRIPE_SECRET_KEY -STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET +AWS_S3_ENDPOINT=http://0.0.0.0:4566 -# Hex string to be used as the key for enctyption, use npm run key:generate -ENCRYPTION_KEY=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc +# Admin +LICENSE_KEY=LICENSE_KEY diff --git a/quadratic-api/Dockerfile b/quadratic-api/Dockerfile index 7ac0726780..77a8ba4e16 100644 --- a/quadratic-api/Dockerfile +++ b/quadratic-api/Dockerfile @@ -10,5 +10,6 @@ FROM node:18-slim AS runtime WORKDIR /app COPY --from=builder /app . RUN apt-get update && apt install -y openssl +RUN npm run postinstall --workspace=quadratic-api RUN npm run build:prod --workspace=quadratic-api CMD ["npm", "start:prod"] diff --git a/quadratic-api/jest.setup.js b/quadratic-api/jest.setup.js index 6f9e6c3d93..ce11ad241b 100644 --- a/quadratic-api/jest.setup.js +++ b/quadratic-api/jest.setup.js @@ -1,3 +1,5 @@ +const { multerS3Storage } = require('./src/storage/s3'); + // For auth we expect the following Authorization header format: // Bearer ValidToken {user.sub} jest.mock('./src/middleware/validateAccessToken', () => { @@ -16,13 +18,32 @@ jest.mock('./src/middleware/validateAccessToken', () => { }; }); -jest.mock('./src/aws/s3', () => { +const licenseClientResponse = { + limits: { + seats: 10, + }, + status: 'active', +}; + +jest.mock('./src/licenseClient', () => { + return { + licenseClient: { + post: async () => licenseClientResponse, + checkFromServer: async () => licenseClientResponse, + check: async () => licenseClientResponse, + }, + }; +}); + +jest.mock('./src/storage/storage', () => { return { s3Client: {}, - generatePresignedUrl: jest.fn().mockImplementation(async (str) => str), - uploadStringAsFileS3: jest.fn().mockImplementation(async () => { + getFileUrl: jest.fn().mockImplementation(async (str) => str), + getPresignedFileUrl: jest.fn().mockImplementation(async (str) => str), + uploadFile: jest.fn().mockImplementation(async () => { return { bucket: 'test-bucket', key: 'test-key' }; }), + uploadMiddleware: multerS3Storage, }; }); diff --git a/quadratic-api/package.json b/quadratic-api/package.json index 2252f1bd22..1a2983c463 100644 --- a/quadratic-api/package.json +++ b/quadratic-api/package.json @@ -1,6 +1,6 @@ { "name": "quadratic-api", - "version": "0.5.2", + "version": "0.5.4", "description": "", "main": "index.js", "scripts": { @@ -31,10 +31,13 @@ }, "author": "David Kircos", "dependencies": { - "@anthropic-ai/sdk": "^0.27.2", + "@anthropic-ai/bedrock-sdk": "^0.11.2", + "@anthropic-ai/sdk": "^0.32.1", + "@aws-sdk/client-bedrock-runtime": "^3.682.0", "@aws-sdk/client-s3": "^3.427.0", "@aws-sdk/client-secrets-manager": "^3.441.0", "@aws-sdk/s3-request-presigner": "^3.427.0", + "@ory/kratos-client": "^1.2.1", "@prisma/client": "^4.12.0", "@sendgrid/mail": "^8.1.0", "@sentry/node": "^7.50.0", diff --git a/quadratic-api/prisma/schema.prisma b/quadratic-api/prisma/schema.prisma index 6c09546b85..a81721cb91 100644 --- a/quadratic-api/prisma/schema.prisma +++ b/quadratic-api/prisma/schema.prisma @@ -1,6 +1,6 @@ generator client { provider = "prisma-client-js" - binaryTargets = ["native", "linux-arm64-openssl-3.0.x"] + binaryTargets = ["native", "linux-arm64-openssl-3.0.x", "debian-openssl-3.0.x"] } datasource db { diff --git a/quadratic-api/src/app.ts b/quadratic-api/src/app.ts index 6dab3eecb4..9a0960dca0 100644 --- a/quadratic-api/src/app.ts +++ b/quadratic-api/src/app.ts @@ -8,6 +8,7 @@ import helmet from 'helmet'; import path from 'path'; import { CORS, NODE_ENV, SENTRY_DSN } from './env-vars'; import anthropic_router from './routes/ai/anthropic'; +import bedrock_router from './routes/ai/bedrock'; import openai_router from './routes/ai/openai'; import internal_router from './routes/internal'; import { ApiError } from './utils/ApiError'; @@ -69,6 +70,7 @@ app.get('/', (req, res) => { // App routes // TODO: eventually move all of these into the `v0` directory and register them dynamically +app.use('/ai', bedrock_router); app.use('/ai', anthropic_router); app.use('/ai', openai_router); // Internal routes diff --git a/quadratic-api/src/auth/auth.ts b/quadratic-api/src/auth/auth.ts new file mode 100644 index 0000000000..74df06ee67 --- /dev/null +++ b/quadratic-api/src/auth/auth.ts @@ -0,0 +1,53 @@ +import { AUTH_TYPE } from '../env-vars'; +import { getUsersFromAuth0, jwtConfigAuth0, lookupUsersFromAuth0ByEmail } from './auth0'; +import { getUsersFromOry, getUsersFromOryByEmail, jwtConfigOry } from './ory'; + +export type UsersRequest = { + id: number; + auth0Id: string; +}; + +export type User = { + id: number; + auth0Id: string; + email: string; + name?: string | undefined; + picture?: string | undefined; +}; + +export type ByEmailUser = { + user_id?: string; +}; + +export const getUsers = async (users: UsersRequest[]): Promise> => { + switch (AUTH_TYPE) { + case 'auth0': + return await getUsersFromAuth0(users); + case 'ory': + return await getUsersFromOry(users); + default: + throw new Error(`Unsupported auth type in getUsers(): ${AUTH_TYPE}`); + } +}; + +export const getUsersByEmail = async (email: string): Promise => { + switch (AUTH_TYPE) { + case 'auth0': + return await lookupUsersFromAuth0ByEmail(email); + case 'ory': + return await getUsersFromOryByEmail(email); + default: + throw new Error(`Unsupported auth type in getUsersByEmail(): ${AUTH_TYPE}`); + } +}; + +export const jwtConfig = () => { + switch (AUTH_TYPE) { + case 'auth0': + return jwtConfigAuth0; + case 'ory': + return jwtConfigOry; + default: + throw new Error(`Unsupported auth type in jwtConfig(): ${AUTH_TYPE}`); + } +}; diff --git a/quadratic-api/src/auth0/profile.ts b/quadratic-api/src/auth/auth0.ts similarity index 70% rename from quadratic-api/src/auth0/profile.ts rename to quadratic-api/src/auth/auth0.ts index 932b74e82d..8fbbe46378 100644 --- a/quadratic-api/src/auth0/profile.ts +++ b/quadratic-api/src/auth/auth0.ts @@ -1,6 +1,16 @@ import * as Sentry from '@sentry/node'; import { ManagementClient } from 'auth0'; -import { AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_DOMAIN } from '../env-vars'; +import { Algorithm } from 'jsonwebtoken'; +import jwksRsa, { GetVerificationKey } from 'jwks-rsa'; +import { + AUTH0_AUDIENCE, + AUTH0_CLIENT_ID, + AUTH0_CLIENT_SECRET, + AUTH0_DOMAIN, + AUTH0_ISSUER, + AUTH0_JWKS_URI, +} from '../env-vars'; +import { ByEmailUser } from './auth'; // Guide to Setting up on Auth0 // 1. Create an Auth0 Machine to Machine Application @@ -11,12 +21,20 @@ import { AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_DOMAIN } from '../env-vars' // We need to use account linking to ensure only one account per user // https://auth0.com/docs/customize/extensions/account-link-extension -const auth0 = new ManagementClient({ - domain: AUTH0_DOMAIN, - clientId: AUTH0_CLIENT_ID, - clientSecret: AUTH0_CLIENT_SECRET, - scope: 'read:users', -}); +let auth0: ManagementClient | undefined; + +const getAuth0 = () => { + if (!auth0) { + auth0 = auth0 = new ManagementClient({ + domain: AUTH0_DOMAIN, + clientId: AUTH0_CLIENT_ID, + clientSecret: AUTH0_CLIENT_SECRET, + scope: 'read:users', + }); + } + + return auth0; +}; /** * Given a list of users from our system, we lookup their info in Auth0. @@ -45,7 +63,7 @@ export const getUsersFromAuth0 = async (users: { id: number; auth0Id: string }[] // Search for users on Auth0 const auth0Ids = users.map(({ auth0Id }) => auth0Id); - const auth0Users = await auth0.getUsers({ + const auth0Users = await getAuth0().getUsers({ q: `user_id:(${auth0Ids.join(' OR ')})`, }); @@ -92,7 +110,19 @@ export const getUsersFromAuth0 = async (users: { id: number; auth0Id: string }[] return usersById; }; -export const lookupUsersFromAuth0ByEmail = async (email: string) => { - const auth0Users = await auth0.getUsersByEmail(email); +export const lookupUsersFromAuth0ByEmail = async (email: string): Promise => { + const auth0Users = await getAuth0().getUsersByEmail(email); return auth0Users; }; + +export const jwtConfigAuth0 = { + secret: jwksRsa.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: AUTH0_JWKS_URI, + }) as GetVerificationKey, + audience: AUTH0_AUDIENCE, + issuer: AUTH0_ISSUER, + algorithms: ['RS256'] as Algorithm[], +}; diff --git a/quadratic-api/src/auth/ory.ts b/quadratic-api/src/auth/ory.ts new file mode 100644 index 0000000000..e818e76c97 --- /dev/null +++ b/quadratic-api/src/auth/ory.ts @@ -0,0 +1,85 @@ +import { Configuration, IdentityApi } from '@ory/kratos-client'; +import * as Sentry from '@sentry/node'; +import { Algorithm } from 'jsonwebtoken'; +import jwksRsa, { GetVerificationKey } from 'jwks-rsa'; +import { ORY_ADMIN_HOST, ORY_JWKS_URI } from '../env-vars'; +import { ByEmailUser, User } from './auth'; + +const config = new Configuration({ + basePath: ORY_ADMIN_HOST, + baseOptions: { + withCredentials: true, + }, +}); +const sdk = new IdentityApi(config); + +export const jwtConfigOry = { + secret: jwksRsa.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: ORY_JWKS_URI, + }) as GetVerificationKey, + algorithms: ['RS256'] as Algorithm[], +}; + +export const getUsersFromOry = async (users: { id: number; auth0Id: string }[]): Promise> => { + // If we got nothing, we return an empty object + if (users.length === 0) return {}; + + const ids = users.map(({ auth0Id }) => auth0Id); + let identities; + + try { + identities = (await sdk.listIdentities({ ids })).data; + } catch (e) { + console.error(e); + return {}; + } + + // Map users by their Quadratic ID. If we didn't find a user, throw. + const usersById: Record = users.reduce((acc: Record, { id, auth0Id }) => { + const oryUser = identities.find(({ id }) => id === auth0Id); + + // If we're missing data we expect, log it to Sentry and skip this user + if (!oryUser || oryUser.traits.email === undefined) { + Sentry.captureException({ + message: 'Ory user returned without `email`', + level: 'error', + extra: { + auth0IdInOurDb: auth0Id, + oryUserResult: oryUser, + }, + }); + throw new Error('Failed to retrieve all user info from Ory'); + } + + const { email, name } = oryUser.traits; + + return { + ...acc, + [id]: { + id, + auth0Id, + email, + name: `${name.first} ${name.last}`, + picture: undefined, + }, + }; + }, {}); + + return usersById; +}; + +export const getUsersFromOryByEmail = async (email: string): Promise => { + let identities; + + try { + identities = (await sdk.listIdentities({ credentialsIdentifier: email })).data; + } catch (e) { + console.error(e); + return []; + } + + return identities.map(({ id }) => ({ user_id: id })); +}; diff --git a/quadratic-api/src/aws/s3.ts b/quadratic-api/src/aws/s3.ts deleted file mode 100644 index a42ef03cb7..0000000000 --- a/quadratic-api/src/aws/s3.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { - AWS_S3_ACCESS_KEY_ID, - AWS_S3_BUCKET_NAME, - AWS_S3_ENDPOINT, - AWS_S3_REGION, - AWS_S3_SECRET_ACCESS_KEY, -} from '../env-vars'; -const endpoint = AWS_S3_ENDPOINT; - -// Initialize S3 client -export const s3Client = new S3Client({ - region: AWS_S3_REGION, - credentials: { - accessKeyId: AWS_S3_ACCESS_KEY_ID, - secretAccessKey: AWS_S3_SECRET_ACCESS_KEY, - }, - endpoint, - forcePathStyle: true, -}); - -export const uploadStringAsFileS3 = async (fileKey: string, contents: string) => { - const command = new PutObjectCommand({ - Bucket: AWS_S3_BUCKET_NAME, - Key: fileKey, - Body: new Uint8Array(Buffer.from(contents, 'base64')), - // Optionally, you can add other configuration like ContentType - // ContentType: 'text/plain' - }); - const response = await s3Client.send(command); - - // Check if the upload was successful - if (response && response.$metadata.httpStatusCode === 200) { - return { - bucket: AWS_S3_BUCKET_NAME, - key: fileKey, - }; - } else { - throw new Error('Failed to upload file to S3'); - } -}; - -// Get file URL from S3 -export const generatePresignedUrl = async (key: string) => { - const command = new GetObjectCommand({ - Bucket: AWS_S3_BUCKET_NAME, - Key: key, - }); - - return await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 * 24 * 7 }); // 7 days -}; diff --git a/quadratic-api/src/env-vars.ts b/quadratic-api/src/env-vars.ts index fc94885e7c..fc5c6224d4 100644 --- a/quadratic-api/src/env-vars.ts +++ b/quadratic-api/src/env-vars.ts @@ -10,34 +10,28 @@ export const NODE_ENV = process.env.NODE_ENV || 'development'; export const PORT = process.env.PORT || 8000; export const AWS_S3_ENDPOINT = process.env.AWS_S3_ENDPOINT || undefined; export const ENVIRONMENT = process.env.ENVIRONMENT; - -// Required export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN as string; export const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID as string; export const AUTH0_CLIENT_SECRET = process.env.AUTH0_CLIENT_SECRET as string; export const AUTH0_JWKS_URI = process.env.AUTH0_JWKS_URI as string; export const AUTH0_ISSUER = process.env.AUTH0_ISSUER as string; export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE as string; +export const ORY_JWKS_URI = process.env.ORY_JWKS_URI as string; +export const ORY_ADMIN_HOST = process.env.ORY_ADMIN_HOST as string; +export const QUADRATIC_FILE_URI = process.env.QUADRATIC_FILE_URI as string; +export const QUADRATIC_FILE_URI_PUBLIC = process.env.QUADRATIC_FILE_URI_PUBLIC as string; export const AWS_S3_REGION = process.env.AWS_S3_REGION as string; export const AWS_S3_ACCESS_KEY_ID = process.env.AWS_S3_ACCESS_KEY_ID as string; export const AWS_S3_SECRET_ACCESS_KEY = process.env.AWS_S3_SECRET_ACCESS_KEY as string; export const AWS_S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME as string; + +// Required export const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY as string; export const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY as string; -[ - 'AUTH0_DOMAIN', - 'AUTH0_CLIENT_ID', - 'AUTH0_CLIENT_SECRET', - 'AUTH0_JWKS_URI', - 'AUTH0_ISSUER', - 'AUTH0_AUDIENCE', - 'AWS_S3_REGION', - 'AWS_S3_ACCESS_KEY_ID', - 'AWS_S3_SECRET_ACCESS_KEY', - 'AWS_S3_BUCKET_NAME', - 'STRIPE_SECRET_KEY', - 'ENCRYPTION_KEY', -].forEach(ensureEnvVarExists); +export const STORAGE_TYPE = process.env.STORAGE_TYPE as string; +export const AUTH_TYPE = process.env.AUTH_TYPE as string; +export const LICENSE_KEY = process.env.LICENSE_KEY as string; +['STRIPE_SECRET_KEY', 'ENCRYPTION_KEY', 'STORAGE_TYPE', 'AUTH_TYPE', 'LICENSE_KEY'].forEach(ensureEnvVarExists); // Required in prod, optional locally export const M2M_AUTH_TOKEN = process.env.M2M_AUTH_TOKEN; @@ -50,6 +44,10 @@ if (NODE_ENV === 'production') { ['M2M_AUTH_TOKEN', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'SLACK_FEEDBACK_URL'].forEach(ensureEnvVarExists); } +// Intentionally hard-coded to avoid this being environment-configurable +// NOTE: Modifying this license check is violating the Quadratic Terms and Conditions and is stealing software, and we will come after you. +export const LICENSE_API_URI = 'https://selfhost.quadratichq.com'; + ensureSampleTokenNotUsedInProduction(); function ensureEnvVarExists(key: string) { diff --git a/quadratic-api/src/internal/addUserToTeam.ts b/quadratic-api/src/internal/addUserToTeam.ts index 2c8fadf2cd..0bce08cf28 100644 --- a/quadratic-api/src/internal/addUserToTeam.ts +++ b/quadratic-api/src/internal/addUserToTeam.ts @@ -1,5 +1,6 @@ import { TeamRole } from '@prisma/client'; import dbClient from '../dbClient'; +import { licenseClient } from '../licenseClient'; export const addUserToTeam = async (args: { userId: number; teamId: number; role: TeamRole }) => { const { userId, teamId, role } = args; @@ -13,6 +14,9 @@ export const addUserToTeam = async (args: { userId: number; teamId: number; role }, }); + // update user count in the license server + await licenseClient.check(true); + // Update the seat quantity on the team's stripe subscription // await updateSeatQuantity(teamId); diff --git a/quadratic-api/src/internal/removeUserFromTeam.ts b/quadratic-api/src/internal/removeUserFromTeam.ts index 1e5f2ac94d..63fb5e14cf 100644 --- a/quadratic-api/src/internal/removeUserFromTeam.ts +++ b/quadratic-api/src/internal/removeUserFromTeam.ts @@ -1,4 +1,5 @@ import dbClient from '../dbClient'; +import { licenseClient } from '../licenseClient'; export const removeUserFromTeam = async (userId: number, teamId: number) => { await dbClient.$transaction(async (prisma) => { @@ -23,6 +24,9 @@ export const removeUserFromTeam = async (userId: number, teamId: number) => { }); }); + // update user count in the license server + await licenseClient.check(true); + // Update the seat quantity on the team's stripe subscription // await updateSeatQuantity(teamId); }; diff --git a/quadratic-api/src/licenseClient.ts b/quadratic-api/src/licenseClient.ts new file mode 100644 index 0000000000..2211975110 --- /dev/null +++ b/quadratic-api/src/licenseClient.ts @@ -0,0 +1,80 @@ +//! License Client +//! +//! Modifying this license check is violating the Quadratic Terms and Conditions and is stealing software, and we will come after you. + +import axios from 'axios'; +import { LicenseSchema } from 'quadratic-shared/typesAndSchemas'; +import z from 'zod'; +import dbClient from './dbClient'; +import { LICENSE_API_URI, LICENSE_KEY } from './env-vars'; +import { hash } from './utils/crypto'; +import { ApiError } from './utils/ApiError'; + +type LicenseResponse = z.infer; + +let cachedResult: LicenseResponse | null = null; +let lastCheckedTime: number | null = null; +const cacheDuration = 15 * 60 * 1000; // 15 minutes in milliseconds +// const cacheDuration = 0; // disable the cache for testing + +export const licenseClient = { + post: async (seats: number): Promise => { + try { + const body = { stats: { seats } }; + const response = await axios.post(`${LICENSE_API_URI}/api/license/${LICENSE_KEY}`, body); + + return LicenseSchema.parse(response.data) as LicenseResponse; + } catch (err) { + if (err instanceof Error) { + console.error('Failed to get the license info from the license service:', err.message); + throw new ApiError(402, 'Failed to get the license info from the license service'); + } + + return null; + } + }, + checkFromServer: async (): Promise => { + // NOTE: Modifying this license check is violating the Quadratic Terms and Conditions and is stealing software, and we will come after you. + if (hash(LICENSE_KEY) === '2ef876ddfe6cc783b83ac63cbef0ae84e6807c69fa72066801f130706e2a935a') { + return licenseClient.adminLicenseResponse(); + } + + const userCount = await dbClient.user.count(); + + return licenseClient.post(userCount); + }, + adminLicenseResponse: async (): Promise => { + return { + limits: { + seats: 100000000000, + }, + status: 'active', + }; + }, + /** + * + * @param force boolean to force a license check (ignoring the cache) + * @returns + */ + check: async (force: boolean): Promise => { + const currentTime = Date.now(); + + if (!force && cachedResult && lastCheckedTime && currentTime - lastCheckedTime < cacheDuration) { + // Use cached result if within the cache duration + return cachedResult; + } + // Otherwise, perform the check + const result = await licenseClient.checkFromServer(); + + // don't cache errors or non-active licenses + if (!result || result.status === 'revoked') { + return null; + } + + // Cache the result and update the last checked time + cachedResult = result; + lastCheckedTime = currentTime; + + return result; + }, +}; diff --git a/quadratic-api/src/middleware/user.ts b/quadratic-api/src/middleware/user.ts index 4bf5dfe2cd..e2fe6f2fb9 100644 --- a/quadratic-api/src/middleware/user.ts +++ b/quadratic-api/src/middleware/user.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from 'express'; -import { getUsersFromAuth0 } from '../auth0/profile'; +import { getUsers } from '../auth/auth'; import dbClient from '../dbClient'; import { addUserToTeam } from '../internal/addUserToTeam'; import { RequestWithAuth, RequestWithOptionalAuth, RequestWithUser } from '../types/Request'; @@ -8,7 +8,7 @@ const runFirstTimeUserLogic = async (user: Awaited { auth0Id, }, }); + if (user) { return user; } diff --git a/quadratic-api/src/middleware/validateAccessToken.ts b/quadratic-api/src/middleware/validateAccessToken.ts index b256bff7d9..e73dab1ecb 100644 --- a/quadratic-api/src/middleware/validateAccessToken.ts +++ b/quadratic-api/src/middleware/validateAccessToken.ts @@ -1,16 +1,5 @@ -import { GetVerificationKey, expressjwt } from 'express-jwt'; -import jwksRsa from 'jwks-rsa'; -import { AUTH0_AUDIENCE, AUTH0_ISSUER, AUTH0_JWKS_URI } from '../env-vars'; +import { Params, expressjwt } from 'express-jwt'; +import { jwtConfig } from '../auth/auth'; // based on implementation from https://github.com/auth0-developer-hub/api_express_typescript_hello-world/blob/main/src/middleware/auth0.middleware.ts -export const validateAccessToken = expressjwt({ - secret: jwksRsa.expressJwtSecret({ - cache: true, - rateLimit: true, - jwksRequestsPerMinute: 5, - jwksUri: AUTH0_JWKS_URI, - }) as GetVerificationKey, - audience: AUTH0_AUDIENCE, - issuer: AUTH0_ISSUER, - algorithms: ['RS256'], -}); +export const validateAccessToken = expressjwt(jwtConfig() as Params); diff --git a/quadratic-api/src/routes/ai/aiRateLimiter.ts b/quadratic-api/src/routes/ai/aiRateLimiter.ts new file mode 100644 index 0000000000..12a74da7fb --- /dev/null +++ b/quadratic-api/src/routes/ai/aiRateLimiter.ts @@ -0,0 +1,13 @@ +import rateLimit from 'express-rate-limit'; +import { RATE_LIMIT_AI_REQUESTS_MAX, RATE_LIMIT_AI_WINDOW_MS } from '../../env-vars'; +import { Request } from '../../types/Request'; + +export const ai_rate_limiter = rateLimit({ + windowMs: Number(RATE_LIMIT_AI_WINDOW_MS) || 3 * 60 * 60 * 1000, // 3 hours + max: Number(RATE_LIMIT_AI_REQUESTS_MAX) || 5000, // Limit number of requests per windowMs + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + keyGenerator: (request: Request) => { + return request.auth?.sub || 'anonymous'; + }, +}); diff --git a/quadratic-api/src/routes/ai/anthropic.ts b/quadratic-api/src/routes/ai/anthropic.ts index 0f47e24d8b..bc4f91f5b0 100644 --- a/quadratic-api/src/routes/ai/anthropic.ts +++ b/quadratic-api/src/routes/ai/anthropic.ts @@ -1,10 +1,11 @@ import Anthropic from '@anthropic-ai/sdk'; import express from 'express'; -import rateLimit from 'express-rate-limit'; +import { MODEL_OPTIONS } from 'quadratic-shared/AI_MODELS'; import { AnthropicAutoCompleteRequestBodySchema } from 'quadratic-shared/typesAndSchemasAI'; -import { ANTHROPIC_API_KEY, RATE_LIMIT_AI_REQUESTS_MAX, RATE_LIMIT_AI_WINDOW_MS } from '../../env-vars'; +import { ANTHROPIC_API_KEY } from '../../env-vars'; import { validateAccessToken } from '../../middleware/validateAccessToken'; import { Request } from '../../types/Request'; +import { ai_rate_limiter } from './aiRateLimiter'; const anthropic_router = express.Router(); @@ -12,33 +13,27 @@ const anthropic = new Anthropic({ apiKey: ANTHROPIC_API_KEY, }); -const ai_rate_limiter = rateLimit({ - windowMs: Number(RATE_LIMIT_AI_WINDOW_MS) || 3 * 60 * 60 * 1000, // 3 hours - max: Number(RATE_LIMIT_AI_REQUESTS_MAX) || 25, // Limit number of requests per windowMs - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers - keyGenerator: (request: Request) => { - return request.auth?.sub || 'anonymous'; - }, -}); - anthropic_router.post('/anthropic/chat', validateAccessToken, ai_rate_limiter, async (request, response) => { try { - const { model, messages, temperature } = AnthropicAutoCompleteRequestBodySchema.parse(request.body); + const { model, system, messages, tools, tool_choice } = AnthropicAutoCompleteRequestBodySchema.parse(request.body); + const { temperature, max_tokens } = MODEL_OPTIONS[model]; const result = await anthropic.messages.create({ model, + system, messages, temperature, - max_tokens: 8192, + max_tokens, + tools, + tool_choice, }); - response.json(result.content[0]); + response.json(result.content); } catch (error: any) { - if (error.response) { - response.status(error.response.status).json(error.response.data); - console.log(error.response.status, error.response.data); + if (error instanceof Anthropic.APIError) { + response.status(error.status ?? 400).json(error.message); + console.log(error.status, error.message); } else { - response.status(400).json(error.message); - console.log(error.message); + response.status(400).json(error); + console.log(error); } } }); @@ -49,30 +44,47 @@ anthropic_router.post( ai_rate_limiter, async (request: Request, response) => { try { - const { model, messages, temperature } = AnthropicAutoCompleteRequestBodySchema.parse(request.body); + const { model, system, messages, tools, tool_choice } = AnthropicAutoCompleteRequestBodySchema.parse( + request.body + ); + const { temperature, max_tokens } = MODEL_OPTIONS[model]; const chunks = await anthropic.messages.create({ model, + system, messages, temperature, - max_tokens: 8192, + max_tokens, stream: true, + tools, + tool_choice, }); response.setHeader('Content-Type', 'text/event-stream'); response.setHeader('Cache-Control', 'no-cache'); response.setHeader('Connection', 'keep-alive'); for await (const chunk of chunks) { - response.write(`data: ${JSON.stringify(chunk)}\n\n`); + if (!response.writableEnded) { + response.write(`data: ${JSON.stringify(chunk)}\n\n`); + } else { + break; + } } - response.end(); + if (!response.writableEnded) { + response.end(); + } } catch (error: any) { - if (error.response) { - response.status(error.response.status).json(error.response.data); - console.log(error.response.status, error.response.data); + if (!response.headersSent) { + if (error instanceof Anthropic.APIError) { + response.status(error.status ?? 400).json(error.message); + console.log(error.status, error.message); + } else { + response.status(400).json(error); + console.log(error); + } } else { - response.status(400).json(error.message); - console.log(error.message); + response.status(500).json('Error occurred after headers were sent'); + console.error('Error occurred after headers were sent:', error); } } } diff --git a/quadratic-api/src/routes/ai/bedrock.ts b/quadratic-api/src/routes/ai/bedrock.ts new file mode 100644 index 0000000000..9b1de6d667 --- /dev/null +++ b/quadratic-api/src/routes/ai/bedrock.ts @@ -0,0 +1,191 @@ +import { AnthropicBedrock } from '@anthropic-ai/bedrock-sdk'; +import Anthropic from '@anthropic-ai/sdk'; +import { BedrockRuntimeClient, ConverseCommand, ConverseStreamCommand } from '@aws-sdk/client-bedrock-runtime'; +import express from 'express'; +import { MODEL_OPTIONS } from 'quadratic-shared/AI_MODELS'; +import { + BedrockAnthropicAutoCompleteRequestBodySchema, + BedrockAutoCompleteRequestBodySchema, +} from 'quadratic-shared/typesAndSchemasAI'; +import { AWS_S3_ACCESS_KEY_ID, AWS_S3_REGION, AWS_S3_SECRET_ACCESS_KEY } from '../../env-vars'; +import { validateAccessToken } from '../../middleware/validateAccessToken'; +import { Request } from '../../types/Request'; +import { ai_rate_limiter } from './aiRateLimiter'; + +const bedrock_router = express.Router(); + +// aws-sdk for bedrock, generic for all models +const bedrock = new BedrockRuntimeClient({ + region: AWS_S3_REGION, + credentials: { accessKeyId: AWS_S3_ACCESS_KEY_ID, secretAccessKey: AWS_S3_SECRET_ACCESS_KEY }, +}); + +// anthropic-sdk for bedrock +const bedrock_anthropic = new AnthropicBedrock({ + awsSecretKey: AWS_S3_SECRET_ACCESS_KEY, + awsAccessKey: AWS_S3_ACCESS_KEY_ID, + awsRegion: AWS_S3_REGION, +}); + +bedrock_router.post('/bedrock/chat', validateAccessToken, ai_rate_limiter, async (request, response) => { + try { + const { model, system, messages, tools, tool_choice } = BedrockAutoCompleteRequestBodySchema.parse(request.body); + const { temperature, max_tokens } = MODEL_OPTIONS[model]; + const command = new ConverseCommand({ + modelId: model, + system, + messages, + inferenceConfig: { maxTokens: max_tokens, temperature }, + toolConfig: tools && + tool_choice && { + tools, + toolChoice: tool_choice, + }, + }); + + const result = await bedrock.send(command); + response.json(result.output); + } catch (error: any) { + if (error.response) { + response.status(error.response.status).json(error.response.data); + console.log(error.response.status, error.response.data); + } else { + response.status(400).json(error.message); + console.log(error.message); + } + } +}); + +bedrock_router.post( + '/bedrock/chat/stream', + validateAccessToken, + ai_rate_limiter, + async (request: Request, response) => { + try { + const { model, system, messages, tools, tool_choice } = BedrockAutoCompleteRequestBodySchema.parse(request.body); + const { temperature, max_tokens } = MODEL_OPTIONS[model]; + const command = new ConverseStreamCommand({ + modelId: model, + system, + messages, + inferenceConfig: { maxTokens: max_tokens, temperature }, + toolConfig: tools && + tool_choice && { + tools, + toolChoice: tool_choice, + }, + }); + + const chunks = (await bedrock.send(command)).stream ?? []; + + response.setHeader('Content-Type', 'text/event-stream'); + response.setHeader('Cache-Control', 'no-cache'); + response.setHeader('Connection', 'keep-alive'); + for await (const chunk of chunks) { + if (!response.writableEnded) { + response.write(`data: ${JSON.stringify(chunk)}\n\n`); + } else { + break; + } + } + + if (!response.writableEnded) { + response.end(); + } + } catch (error: any) { + if (!response.headersSent) { + if (error.response) { + response.status(error.response.status).json(error.response.data); + console.log(error.response.status, error.response.data); + } else { + response.status(400).json(error.message); + console.log(error.message); + } + } else { + console.error('Error occurred after headers were sent:', error); + } + } + } +); + +// anthropic-sdk for bedrock +bedrock_router.post('/bedrock/anthropic/chat', validateAccessToken, ai_rate_limiter, async (request, response) => { + try { + const { model, system, messages, tools, tool_choice } = BedrockAnthropicAutoCompleteRequestBodySchema.parse( + request.body + ); + const { temperature, max_tokens } = MODEL_OPTIONS[model]; + const result = await bedrock_anthropic.messages.create({ + model, + system, + messages, + temperature, + max_tokens, + tools, + tool_choice, + }); + response.json(result.content); + } catch (error: any) { + if (error instanceof Anthropic.APIError) { + response.status(error.status ?? 400).json(error.message); + console.log(error.status, error.message); + } else { + response.status(400).json(error); + console.log(error); + } + } +}); + +// anthropic-sdk for bedrock +bedrock_router.post( + '/bedrock/anthropic/chat/stream', + validateAccessToken, + ai_rate_limiter, + async (request: Request, response) => { + try { + const { model, system, messages, tools, tool_choice } = BedrockAnthropicAutoCompleteRequestBodySchema.parse( + request.body + ); + const { temperature, max_tokens } = MODEL_OPTIONS[model]; + const chunks = await bedrock_anthropic.messages.create({ + model, + system, + messages, + temperature, + max_tokens, + stream: true, + tools, + tool_choice, + }); + + response.setHeader('Content-Type', 'text/event-stream'); + response.setHeader('Cache-Control', 'no-cache'); + response.setHeader('Connection', 'keep-alive'); + for await (const chunk of chunks) { + if (!response.writableEnded) { + response.write(`data: ${JSON.stringify(chunk)}\n\n`); + } else { + break; + } + } + + if (!response.writableEnded) { + response.end(); + } + } catch (error: any) { + if (!response.headersSent) { + if (error instanceof Anthropic.APIError) { + response.status(error.status ?? 400).json(error.message); + console.log(error.status, error.message); + } else { + response.status(400).json(error); + console.log(error); + } + } else { + console.error('Error occurred after headers were sent:', error); + } + } + } +); + +export default bedrock_router; diff --git a/quadratic-api/src/routes/ai/openai.ts b/quadratic-api/src/routes/ai/openai.ts index f01406f819..e2c6227e73 100644 --- a/quadratic-api/src/routes/ai/openai.ts +++ b/quadratic-api/src/routes/ai/openai.ts @@ -1,10 +1,11 @@ import express from 'express'; -import rateLimit from 'express-rate-limit'; import OpenAI from 'openai'; +import { MODEL_OPTIONS } from 'quadratic-shared/AI_MODELS'; import { OpenAIAutoCompleteRequestBodySchema } from 'quadratic-shared/typesAndSchemasAI'; -import { OPENAI_API_KEY, RATE_LIMIT_AI_REQUESTS_MAX, RATE_LIMIT_AI_WINDOW_MS } from '../../env-vars'; +import { OPENAI_API_KEY } from '../../env-vars'; import { validateAccessToken } from '../../middleware/validateAccessToken'; import { Request } from '../../types/Request'; +import { ai_rate_limiter } from './aiRateLimiter'; const openai_router = express.Router(); @@ -12,61 +13,67 @@ const openai = new OpenAI({ apiKey: OPENAI_API_KEY || '', }); -const ai_rate_limiter = rateLimit({ - windowMs: Number(RATE_LIMIT_AI_WINDOW_MS) || 3 * 60 * 60 * 1000, // 3 hours - max: Number(RATE_LIMIT_AI_REQUESTS_MAX) || 25, // Limit number of requests per windowMs - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers - keyGenerator: (request: Request) => { - return request.auth?.sub || 'anonymous'; - }, -}); - openai_router.post('/openai/chat', validateAccessToken, ai_rate_limiter, async (request, response) => { try { - const { model, messages, temperature } = OpenAIAutoCompleteRequestBodySchema.parse(request.body); + const { model, messages, tools, tool_choice } = OpenAIAutoCompleteRequestBodySchema.parse(request.body); + const { temperature } = MODEL_OPTIONS[model]; const result = await openai.chat.completions.create({ model, messages, temperature, + tools, + tool_choice, }); response.json(result.choices[0].message); } catch (error: any) { - if (error.response) { - response.status(error.response.status).json(error.response.data); - console.log(error.response.status, error.response.data); + if (error instanceof OpenAI.APIError) { + response.status(error.status ?? 400).json(error.message); + console.log(error.status, error.message); } else { - response.status(400).json(error.message); - console.log(error.message); + response.status(400).json(error); + console.log(error); } } }); openai_router.post('/openai/chat/stream', validateAccessToken, ai_rate_limiter, async (request: Request, response) => { try { - const { model, messages, temperature } = OpenAIAutoCompleteRequestBodySchema.parse(request.body); + const { model, messages, tools, tool_choice } = OpenAIAutoCompleteRequestBodySchema.parse(request.body); + const { temperature } = MODEL_OPTIONS[model]; const completion = await openai.chat.completions.create({ model, messages, temperature, stream: true, + tools, + tool_choice, }); response.setHeader('Content-Type', 'text/event-stream'); response.setHeader('Cache-Control', 'no-cache'); response.setHeader('Connection', 'keep-alive'); for await (const chunk of completion) { - response.write(`data: ${JSON.stringify(chunk)}\n\n`); + if (!response.writableEnded) { + response.write(`data: ${JSON.stringify(chunk)}\n\n`); + } else { + break; + } } - response.end(); + if (!response.writableEnded) { + response.end(); + } } catch (error: any) { - if (error.response) { - response.status(error.response.status).json(error.response.data); - console.log(error.response.status, error.response.data); + if (!response.headersSent) { + if (error instanceof OpenAI.APIError) { + response.status(error.status ?? 400).json(error.message); + console.log(error.status, error.message); + } else { + response.status(400).json(error); + console.log(error); + } } else { - response.status(400).json(error.message); - console.log(error.message); + console.error('Error occurred after headers were sent:', error); } } }); diff --git a/quadratic-api/src/routes/v0/education.POST.ts b/quadratic-api/src/routes/v0/education.POST.ts index c3f2002c89..97c35274eb 100644 --- a/quadratic-api/src/routes/v0/education.POST.ts +++ b/quadratic-api/src/routes/v0/education.POST.ts @@ -1,7 +1,7 @@ import { Response } from 'express'; import { sanityClient } from 'quadratic-shared/sanityClient'; import { ApiTypes } from 'quadratic-shared/typesAndSchemas'; -import { getUsersFromAuth0 } from '../../auth0/profile'; +import { getUsers } from '../../auth/auth'; import universityDomains from '../../data/universityDomains'; import dbClient from '../../dbClient'; import { userMiddleware } from '../../middleware/user'; @@ -19,7 +19,7 @@ async function handler(req: RequestWithUser, res: Response { diff --git a/quadratic-api/src/routes/v0/examples.POST.ts b/quadratic-api/src/routes/v0/examples.POST.ts index 663f1c2561..dc40cdc36c 100644 --- a/quadratic-api/src/routes/v0/examples.POST.ts +++ b/quadratic-api/src/routes/v0/examples.POST.ts @@ -1,3 +1,4 @@ +import axios from 'axios'; import { Response } from 'express'; import { ApiSchemas, ApiTypes } from 'quadratic-shared/typesAndSchemas'; import z from 'zod'; @@ -27,6 +28,12 @@ async function handler(req: RequestWithUser, res: Response res.json())) as ApiTypes['/v0/files/:uuid.GET.response']; + } = (await axios.get(apiUrl).then((res) => res.data)) as ApiTypes['/v0/files/:uuid.GET.response']; // Fetch the contents of the file - const fileContents = await fetch(lastCheckpointDataUrl).then((res) => res.arrayBuffer()); + const fileContents = await axios + .get(lastCheckpointDataUrl, { responseType: 'arraybuffer' }) + .then((res) => res.data); const buffer = new Uint8Array(fileContents); // Create a private file for the user in the requested team @@ -56,6 +65,7 @@ async function handler(req: RequestWithUser, res: Response void) => { - cb(null, { fieldName: file.fieldname }); - }, - key: (req: Request, file: Express.Multer.File, cb: (error: Error | null, key: string) => void) => { - const fileUuid = req.params.uuid; - cb(null, `${fileUuid}-${file.originalname}`); - }, - }) as StorageEngine, -}); - async function handler(req: RequestWithUser & RequestWithFile, res: Response) { const { params: { uuid }, @@ -65,6 +48,6 @@ export default [ ), validateAccessToken, userMiddleware, - uploadThumbnailToS3.single('thumbnail'), + uploadMiddleware().single('thumbnail'), handler, ]; diff --git a/quadratic-api/src/routes/v0/files.GET.ts b/quadratic-api/src/routes/v0/files.GET.ts index 1f51c9119c..71be69ef34 100644 --- a/quadratic-api/src/routes/v0/files.GET.ts +++ b/quadratic-api/src/routes/v0/files.GET.ts @@ -1,9 +1,9 @@ import { Response } from 'express'; import { ApiTypes } from 'quadratic-shared/typesAndSchemas'; -import { generatePresignedUrl } from '../../aws/s3'; import dbClient from '../../dbClient'; import { userMiddleware } from '../../middleware/user'; import { validateAccessToken } from '../../middleware/validateAccessToken'; +import { getFileUrl } from '../../storage/storage'; import { RequestWithUser } from '../../types/Request'; import { ResponseError } from '../../types/Response'; @@ -44,7 +44,7 @@ async function handler(req: RequestWithUser, res: Response { if (file.thumbnail) { - file.thumbnail = await generatePresignedUrl(file.thumbnail); + file.thumbnail = await getFileUrl(file.thumbnail); } }) ); diff --git a/quadratic-api/src/routes/v0/files.POST.ts b/quadratic-api/src/routes/v0/files.POST.ts index b10b45c31c..12b5e63d1b 100644 --- a/quadratic-api/src/routes/v0/files.POST.ts +++ b/quadratic-api/src/routes/v0/files.POST.ts @@ -23,6 +23,12 @@ async function handler(req: RequestWithUser, res: Response) { +async function handler(req: Request, res: Response) { const { params: { uuid }, } = parseRequest(req, schema); @@ -88,14 +91,35 @@ async function handler(req: Request, res: Response user)); + // Get user info from auth + const authUsersById = await getUsers(dbUsers.map(({ user }) => user)); + + // IDEA: (enhancement) we could put this in /sharing and just return the userCount + // then require the data for the team share modal to be a seaparte network request + const users = dbUsers + .filter(({ userId: id }) => authUsersById[id]) + .map(({ userId: id, role }) => { + const { email, name, picture } = authUsersById[id]; + return { + id, + email, + role, + name, + picture, + }; + }); + + const license = await licenseClient.check(false); + + if (!license) { + throw new ApiError(500, 'Unable to retrieve license'); + } // Get signed thumbnail URLs await Promise.all( dbFiles.map(async (file) => { if (file.thumbnail) { - file.thumbnail = await generatePresignedUrl(file.thumbnail); + file.thumbnail = await getPresignedFileUrl(file.thumbnail); } }) ); @@ -115,18 +139,7 @@ async function handler(req: Request, res: Response { - const { email, name, picture } = auth0UsersById[id]; - return { - id, - email, - role, - name, - picture, - }; - }), + users, invites: dbInvites.map(({ email, role, id }) => ({ email, role, id })), files: dbFiles .filter((file) => !file.ownerUserId) @@ -172,6 +185,7 @@ async function handler(req: Request, res: Response ({ uuid: connection.uuid, name: connection.name, diff --git a/quadratic-api/src/routes/v0/teams.$uuid.invites.POST.ts b/quadratic-api/src/routes/v0/teams.$uuid.invites.POST.ts index 0af3fe7461..2e3866304f 100644 --- a/quadratic-api/src/routes/v0/teams.$uuid.invites.POST.ts +++ b/quadratic-api/src/routes/v0/teams.$uuid.invites.POST.ts @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node'; import { Response } from 'express'; import { ApiSchemas, ApiTypes } from 'quadratic-shared/typesAndSchemas'; import { z } from 'zod'; -import { getUsersFromAuth0, lookupUsersFromAuth0ByEmail } from '../../auth0/profile'; +import { getUsers, getUsersByEmail } from '../../auth/auth'; import dbClient from '../../dbClient'; import { sendEmail } from '../../email/sendEmail'; import { templates } from '../../email/templates'; @@ -68,7 +68,7 @@ async function handler(req: RequestWithUser, res: Response { diff --git a/quadratic-api/src/storage/fileSystem.ts b/quadratic-api/src/storage/fileSystem.ts new file mode 100644 index 0000000000..d43f56eb4a --- /dev/null +++ b/quadratic-api/src/storage/fileSystem.ts @@ -0,0 +1,114 @@ +import axios from 'axios'; +import { Request } from 'express'; +import multer from 'multer'; +import stream, { Readable } from 'node:stream'; +import { QUADRATIC_FILE_URI, QUADRATIC_FILE_URI_PUBLIC } from '../env-vars'; +import { UploadFile } from '../types/Request'; +import { encryptFromEnv } from '../utils/crypto'; +import { UploadFileResponse } from './storage'; + +const generateUrl = (key: string, isPublic: boolean): string => { + const baseUrl = isPublic ? QUADRATIC_FILE_URI_PUBLIC : QUADRATIC_FILE_URI; + return `${baseUrl}/storage/${key}`; +}; + +const generatePresignedUrl = (key: string): string => generateUrl(`presigned/${key}`, true); + +// Get the URL for a given file (key) for the file service. +export const getStorageUrl = (key: string): string => { + return generateUrl(key, true); +}; + +// Get a presigned URL for a given file (key) for the file service. +export const getPresignedStorageUrl = (key: string): string => { + const encrypted = encryptFromEnv(key); + return generatePresignedUrl(encrypted); +}; + +// Upload a file to the file service. +export const upload = async (key: string, contents: string | Uint8Array, jwt: string): Promise => { + const url = generateUrl(key, false); + + if (typeof contents === 'string') { + contents = new Uint8Array(Buffer.from(contents, 'base64')); + } + + try { + const response = await axios + .post(url, contents, { + headers: { + 'Content-Type': 'text/plain', + Authorization: `${jwt}`, + }, + }) + .then((res) => res.data); + + return response; + } catch (e) { + console.error(e); + throw new Error(`Failed to upload file to ${url}`); + } +}; + +// Collect a full stream and place in a byte array. +function streamToByteArray(stream: Readable): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + + stream.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + + stream.on('end', () => { + const buffer = Buffer.concat(chunks); + resolve(new Uint8Array(buffer)); + }); + + stream.on('error', (err: Error) => { + reject(err); + }); + }); +} + +// Multer storage engine for file-system storage. +// +// This middleware is used to handled client upload files and send them to +// the file service. +export const multerFileSystemStorage: multer.Multer = multer({ + storage: { + _handleFile( + req: Request, + file: Express.Multer.File & UploadFile, + cb: (error?: any, info?: Partial) => void + ): void { + const fileUuid = req.params.uuid; + const key = `${fileUuid}-${file.originalname}`; + const jwt = req.header('Authorization'); + + file.key = key; + + if (!jwt) { + cb('No authorization header'); + return; + } + + // Create a pass-through stream to pipe the file stream to + const passThrough = new stream.PassThrough(); + file.stream.pipe(passThrough); + + // Collect the stream and upload to the file service + streamToByteArray(passThrough) + .then((data) => { + upload(key, data, jwt) + .then((_response) => cb(null, file)) + .catch((error) => cb(error)); + }) + .catch((error) => cb(error)); + }, + + // only implement if needed (not currently used) + _removeFile(_req: Request, _file: Express.Multer.File, cb: (error: Error | null) => void): void { + cb(null); + }, + }, +}); diff --git a/quadratic-api/src/storage/s3.ts b/quadratic-api/src/storage/s3.ts new file mode 100644 index 0000000000..11d8e9e9cc --- /dev/null +++ b/quadratic-api/src/storage/s3.ts @@ -0,0 +1,89 @@ +import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { Request } from 'express'; +import multer, { StorageEngine } from 'multer'; +import multerS3 from 'multer-s3'; +import { + AWS_S3_ACCESS_KEY_ID, + AWS_S3_BUCKET_NAME, + AWS_S3_ENDPOINT, + AWS_S3_REGION, + AWS_S3_SECRET_ACCESS_KEY, +} from '../env-vars'; +import { UploadFileResponse } from './storage'; + +const endpoint = AWS_S3_ENDPOINT; +let s3Client: S3Client; + +// Get S3 client slngleton +const getS3Client = () => { + if (!s3Client) { + s3Client = new S3Client({ + region: AWS_S3_REGION, + credentials: { + accessKeyId: AWS_S3_ACCESS_KEY_ID, + secretAccessKey: AWS_S3_SECRET_ACCESS_KEY, + }, + ...(endpoint === undefined + ? // for aws, using transfer acceleration + { + useAccelerateEndpoint: true, + } + : // for localstack, using path style + { + endpoint, + forcePathStyle: true, + }), + }); + } + + return s3Client; +}; + +// Upload a string as a file to S3 +export const uploadStringAsFileS3 = async (fileKey: string, contents: string): Promise => { + const command = new PutObjectCommand({ + Bucket: AWS_S3_BUCKET_NAME, + Key: fileKey, + Body: new Uint8Array(Buffer.from(contents, 'base64')), + // Optionally, you can add other configuration like ContentType + // ContentType: 'text/plain' + }); + const response = await getS3Client().send(command); + + // Check if the upload was successful + if (response && response.$metadata.httpStatusCode === 200) { + return { + bucket: AWS_S3_BUCKET_NAME, + key: fileKey, + }; + } else { + throw new Error('Failed to upload file to S3'); + } +}; + +// Multer storage engine for S3 +export const multerS3Storage = (): multer.Multer => + multer({ + storage: multerS3({ + s3: getS3Client(), + bucket: AWS_S3_BUCKET_NAME, + metadata: (req: Request, file: Express.Multer.File, cb: (error: Error | null, metadata: any) => void) => { + cb(null, { fieldName: file.fieldname }); + }, + key: (req: Request, file: Express.Multer.File, cb: (error: Error | null, key: string) => void) => { + const fileUuid = req.params.uuid; + cb(null, `${fileUuid}-${file.originalname}`); + }, + }) as StorageEngine, + }); + +// Get the presigned file URL from S3 +export const generatePresignedUrl = async (key: string): Promise => { + const command = new GetObjectCommand({ + Bucket: AWS_S3_BUCKET_NAME, + Key: key, + }); + + return await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 * 24 * 7 }); // 7 days +}; diff --git a/quadratic-api/src/storage/storage.ts b/quadratic-api/src/storage/storage.ts new file mode 100644 index 0000000000..0960194079 --- /dev/null +++ b/quadratic-api/src/storage/storage.ts @@ -0,0 +1,57 @@ +import multer from 'multer'; +import { STORAGE_TYPE } from '../env-vars'; +import { getPresignedStorageUrl, getStorageUrl, multerFileSystemStorage, upload } from './fileSystem'; +import { generatePresignedUrl, multerS3Storage, uploadStringAsFileS3 } from './s3'; + +export type UploadFileResponse = { + bucket: string; + key: string; +}; + +// Get the URL for a given file (key). +export const getFileUrl = async (key: string) => { + switch (STORAGE_TYPE) { + case 's3': + return await generatePresignedUrl(key); + case 'file-system': + return getStorageUrl(key); + default: + throw new Error(`Unsupported storage type in getFileUrl(): ${STORAGE_TYPE}`); + } +}; + +// Get a presigned URL for a given file (key). +export const getPresignedFileUrl = async (key: string) => { + switch (STORAGE_TYPE) { + case 's3': + return await generatePresignedUrl(key); + case 'file-system': + return getPresignedStorageUrl(key); + default: + throw new Error(`Unsupported storage type in getPresignedFileUrl(): ${STORAGE_TYPE}`); + } +}; + +// Upload a file (key). +export const uploadFile = async (key: string, contents: string, jwt: string): Promise => { + switch (STORAGE_TYPE) { + case 's3': + return await uploadStringAsFileS3(key, contents); + case 'file-system': + return await upload(key, contents, jwt); + default: + throw new Error(`Unsupported storage type in uploadFile(): ${STORAGE_TYPE}`); + } +}; + +// Multer middleware for file uploads. +export const uploadMiddleware = (): multer.Multer => { + switch (STORAGE_TYPE) { + case 's3': + return multerS3Storage(); + case 'file-system': + return multerFileSystemStorage as unknown as multer.Multer; + default: + throw new Error(`Unsupported storage type in uploadMiddleware(): ${STORAGE_TYPE}`); + } +}; diff --git a/quadratic-api/src/utils/createFile.ts b/quadratic-api/src/utils/createFile.ts index 50a91cd446..9a4f5a533a 100644 --- a/quadratic-api/src/utils/createFile.ts +++ b/quadratic-api/src/utils/createFile.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; -import { uploadStringAsFileS3 } from '../aws/s3'; import dbClient from '../dbClient'; +import { uploadFile } from '../storage/storage'; export async function createFile({ contents, @@ -10,6 +10,7 @@ export async function createFile({ version, teamId, isPrivate, + jwt, }: { contents?: string; name: string; @@ -17,6 +18,7 @@ export async function createFile({ version: string; teamId: number; isPrivate?: boolean; + jwt: string; }) { return await dbClient.$transaction(async (transaction) => { // Create file in db @@ -44,7 +46,7 @@ export async function createFile({ // Upload file contents to S3 and create a checkpoint const { uuid, id: fileId } = dbFile; - const response = await uploadStringAsFileS3(`${uuid}-0.grid`, contents); + const response = await uploadFile(`${uuid}-0.grid`, contents, jwt); await transaction.fileCheckpoint.create({ data: { diff --git a/quadratic-api/src/utils/crypto.test.ts b/quadratic-api/src/utils/crypto.test.ts index 43e70b79d6..a228b42fdf 100644 --- a/quadratic-api/src/utils/crypto.test.ts +++ b/quadratic-api/src/utils/crypto.test.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import { decrypt, encrypt } from './crypto'; +import { decrypt, encrypt, hash } from './crypto'; // Convert a hex string to a buffer. // @@ -13,6 +13,13 @@ describe('Encryption and Decryption', () => { const key = hexStringToBuffer(keyBytes.toString('hex')); const text = 'Hello, world!'; + it('should hash a value', () => { + const hashed = hash(text); + const expected = '315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3'; + + expect(hashed).toEqual(expected); + }); + it('should convert a hex to a buffer', () => { const hex = keyBytes.toString('hex'); const buffer = hexStringToBuffer(hex); diff --git a/quadratic-api/src/utils/crypto.ts b/quadratic-api/src/utils/crypto.ts index 4f7e5a0360..98a290c859 100644 --- a/quadratic-api/src/utils/crypto.ts +++ b/quadratic-api/src/utils/crypto.ts @@ -7,6 +7,13 @@ const algorithm = 'aes-256-cbc'; // Get the encryption key from the env and convert it to a buffer. const encryption_key = Buffer.from(ENCRYPTION_KEY, 'hex'); +export const hash = (text: string): string => { + const hash = crypto.createHash('sha256'); + hash.update(text); + + return hash.digest('hex'); +}; + // Encrypts the given text using the given key. // Store the IV with the encrypted text (prepended). export const encrypt = (key: Buffer, text: string): string => { diff --git a/quadratic-client/.env.docker b/quadratic-client/.env.docker index 18549755a0..f12821b3b1 100644 --- a/quadratic-client/.env.docker +++ b/quadratic-client/.env.docker @@ -2,3 +2,8 @@ VITE_DEBUG=1 VITE_QUADRATIC_API_URL=http://localhost:8000 VITE_QUADRATIC_MULTIPLAYER_URL=ws://localhost:3001/ws VITE_QUADRATIC_CONNECTION_URL=http://localhost:3003 + +# Auth +VITE_AUTH_TYPE=ory +VITE_STORAGE_TYPE=file-system +VITE_ORY_HOST=http://localhost:4433 diff --git a/quadratic-client/.env.example b/quadratic-client/.env.example index 8f9ca0adfd..59862ad5c7 100644 --- a/quadratic-client/.env.example +++ b/quadratic-client/.env.example @@ -3,10 +3,15 @@ VITE_AMPLITUDE_ANALYTICS_API_KEY= VITE_MIXPANEL_ANALYTICS_KEY= VITE_SENTRY_DSN=https://xxxxxxxxxxxxxxxxxx@xxxxxxxxxxxx.ingest.sentry.io/xxxxxxxxxxxx VITE_DEBUG=0 // use =1 to enable debug flags +VITE_QUADRATIC_API_URL=http://localhost:8000 +VITE_QUADRATIC_MULTIPLAYER_URL=ws://localhost:3001/ws +VITE_QUADRATIC_CONNECTION_URL=http://localhost:3003 + +# Auth +VITE_AUTH_TYPE=auth0 +VITE_STORAGE_TYPE=s3 VITE_AUTH0_ISSUER=https://quadratic-community.us.auth0.com/ VITE_AUTH0_DOMAIN=quadratic-community.us.auth0.com VITE_AUTH0_CLIENT_ID=DCPCvqyU5Q0bJD8Q3QmJEoV48x1zLH7W VITE_AUTH0_AUDIENCE=community-quadratic -VITE_QUADRATIC_API_URL=http://localhost:8000 -VITE_QUADRATIC_MULTIPLAYER_URL=ws://localhost:3001/ws -VITE_QUADRATIC_CONNECTION_URL=http://localhost:3003 +VITE_ORY_HOST=http://localhost:4433 diff --git a/quadratic-client/.env.test b/quadratic-client/.env.test new file mode 100644 index 0000000000..2d1cdff22f --- /dev/null +++ b/quadratic-client/.env.test @@ -0,0 +1,9 @@ +VITE_DEBUG=1 // use =1 to enable debug flags +VITE_QUADRATIC_API_URL=http://localhost:8000 +VITE_QUADRATIC_MULTIPLAYER_URL=ws://localhost:3001/ws +VITE_QUADRATIC_CONNECTION_URL=http://localhost:3003 + +# Auth +VITE_AUTH_TYPE=auth0 +VITE_STORAGE_TYPE=s3 +VITE_ORY_HOST=http://localhost:4433 diff --git a/quadratic-client/Dockerfile b/quadratic-client/Dockerfile index e14d164e17..e814a28455 100644 --- a/quadratic-client/Dockerfile +++ b/quadratic-client/Dockerfile @@ -1,13 +1,82 @@ -FROM node:18-alpine AS builder +# Use an official node image as a parent image +FROM node:18 AS build + +# Install rustup +RUN echo 'Installing rustup...' && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Install wasm-pack +RUN echo 'Installing wasm-pack...' && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh +RUN echo 'wasm-pack version:' && wasm-pack --version + +# Install wasm32-unknown-unknown target +# RUN rustup target add wasm32-unknown-unknown + +# Install python & clean up +RUN apt-get update && apt-get install -y python-is-python3 python3-pip && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install npm dependencies WORKDIR /app COPY package.json . COPY package-lock.json . -COPY updateAlertVersion.json . -COPY quadratic-client ./quadratic-client -COPY quadratic-shared ./quadratic-shared +COPY ./quadratic-kernels/python-wasm/package*.json ./quadratic-kernels/python-wasm/ +COPY ./quadratic-core/package*.json ./quadratic-core/ +COPY ./quadratic-rust-client/package*.json ./quadratic-rust-client/ +COPY ./quadratic-shared/package*.json ./quadratic-shared/ +COPY ./quadratic-client/package*.json ./quadratic-client/ RUN npm install -FROM node:18-slim AS runtime +# Install typescript +RUN npm install -D typescript + +# Copy the rest of the application +WORKDIR /app +COPY updateAlertVersion.json . +COPY ./quadratic-kernels/python-wasm/. ./quadratic-kernels/python-wasm/ +COPY ./quadratic-core/. ./quadratic-core/ +COPY ./quadratic-rust-client/. ./quadratic-rust-client/ +COPY ./quadratic-shared/. ./quadratic-shared/ +COPY ./quadratic-client/. ./quadratic-client/ + +# Run the packaging script for quadratic_py +WORKDIR /app +RUN ./quadratic-kernels/python-wasm/package.sh --no-poetry + +# Build wasm +WORKDIR /app/quadratic-core +RUN echo 'Building wasm...' && wasm-pack build --target web --out-dir ../quadratic-client/src/app/quadratic-core --weak-refs + +# Export TS/Rust types +WORKDIR /app/quadratic-core +RUN echo 'Exporting TS/Rust types...' && cargo run --bin export_types + +# Build the quadratic-rust-client WORKDIR /app -COPY --from=builder /app . -CMD ["npm", "start"] +ARG GIT_COMMIT +ENV GIT_COMMIT=$GIT_COMMIT +RUN echo 'Building quadratic-rust-client...' && npm run build --workspace=quadratic-rust-client + +# Build the quadratic-shared +WORKDIR /app +RUN echo 'Building quadratic-shared...' && npm run compile --workspace=quadratic-shared + +# Build the front-end +WORKDIR /app +RUN echo 'Building front-end...' +ENV VITE_DEBUG=VITE_DEBUG_VAL +ENV VITE_QUADRATIC_API_URL=VITE_QUADRATIC_API_URL_VAL +ENV VITE_QUADRATIC_MULTIPLAYER_URL=VITE_QUADRATIC_MULTIPLAYER_URL_VAL +ENV VITE_QUADRATIC_CONNECTION_URL=VITE_QUADRATIC_CONNECTION_URL_VAL +ENV VITE_AUTH_TYPE=VITE_AUTH_TYPE_VAL +ENV VITE_ORY_HOST=VITE_ORY_HOST_VAL +RUN npm run build --workspace=quadratic-client + +# The default command to run the application +# CMD ["npm", "run", "start:production"] + +FROM nginx:stable-alpine +COPY --from=build /app/build /usr/share/nginx/html + +EXPOSE 80 443 3000 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/quadratic-client/index.html b/quadratic-client/index.html index 95b8d24d0b..cc62cf6683 100644 --- a/quadratic-client/index.html +++ b/quadratic-client/index.html @@ -44,14 +44,14 @@ j.async = true; j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f); - })(window, document, 'script', 'dataLayer', 'GTM-WVC2XPB3'); + })(window, document, 'script', 'dataLayer', 'GTM-MDFG6DX4'); - diff --git a/quadratic-client/package.json b/quadratic-client/package.json index 965d193447..2c84d9ce8b 100644 --- a/quadratic-client/package.json +++ b/quadratic-client/package.json @@ -1,6 +1,6 @@ { "name": "quadratic-client", - "version": "0.5.2", + "version": "0.5.4", "author": { "name": "David Kircos", "email": "david@quadratichq.com", @@ -21,6 +21,7 @@ "@monaco-editor/react": "^4.3.1", "@mui/icons-material": "^5.2.0", "@mui/material": "^5.2.2", + "@ory/kratos-client": "^1.2.1", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", @@ -46,8 +47,6 @@ "@sentry/vite-plugin": "^2.22.6", "@szhsin/react-menu": "^4.0.2", "@tailwindcss/container-queries": "^0.1.1", - "@types/react-avatar-editor": "^13.0.2", - "@types/ua-parser-js": "^0.7.39", "bignumber.js": "^9.1.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -61,6 +60,7 @@ "mixpanel-browser": "^2.46.0", "monaco-editor": "^0.40.0", "next-themes": "^0.3.0", + "partial-json": "^0.1.7", "pixi-viewport": "^4.37.0", "pixi.js": "^6.5.10", "react": "^18.2.0", @@ -70,6 +70,7 @@ "react-device-detect": "^2.2.2", "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", + "react-linkify": "^1.0.0-alpha", "react-markdown": "^9.0.1", "react-router-dom": "^6.26.1", "recoil": "^0.7.7", @@ -114,7 +115,8 @@ "lint:prettier:write": "prettier --write src", "coverage:wasm:gen": "cd .. && cd quadratic-core && CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='coverage/cargo-test-%p-%m.profraw' cargo test", "coverage:wasm:html": "cd .. && cd quadratic-core && grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore 'src/wasm_bindings/*' --ignore 'src/bin/*' --ignore '../*' --ignore '/*' -o coverage/html", - "coverage:wasm:view": "open quadratic-core/coverage/html/index.html" + "coverage:wasm:view": "open quadratic-core/coverage/html/index.html", + "build:javascript:worker": "cd ./src/app/web-workers/javascriptWebWorker/worker/javascript/runner/ && node compileJavascriptRunner.mjs" }, "browserslist": { "production": [ @@ -133,8 +135,11 @@ "@types/fontfaceobserver": "^2.1.0", "@types/mixpanel-browser": "^2.38.1", "@types/react": "^18.2.16", + "@types/react-avatar-editor": "^13.0.2", "@types/react-color": "^3.0.6", "@types/react-dom": "^18.2.7", + "@types/react-linkify": "^1.0.4", + "@types/ua-parser-js": "^0.7.39", "@types/uuid": "^9.0.1", "@vitejs/plugin-react": "^4.2.0", "@vitest/web-worker": "^1.3.1", @@ -145,7 +150,7 @@ "fake-indexeddb": "^5.0.2", "fontkit": "^2.0.2", "fs-extra": "^11.1.0", - "happy-dom": "^13.0.0", + "happy-dom": "^15.10.2", "jest-environment-jsdom": "^29.7.0", "msdf-bmfont-xml": "^2.7.0", "msw": "^2.4.11", @@ -157,11 +162,9 @@ "serve": "^14.2.0", "tailwindcss": "^3.3.5", "typescript": "^5.4.5", - "vite": "^5.2.14", - "vite-plugin-checker": "^0.6.2", - "vite-plugin-svgr": "^4.2.0", - "vite-plugin-top-level-await": "^1.4.1", - "vite-plugin-wasm": "^3.3.0", + "vite": "^5.4.10", + "vite-plugin-checker": "^0.8.0", + "vite-plugin-svgr": "^4.3.0", "vite-tsconfig-paths": "^4.2.1", "vitest": "^1.3.1", "vscode-languageserver-types": "^3.17.5" diff --git a/quadratic-client/public/.well-known/jwks.json b/quadratic-client/public/.well-known/jwks.json new file mode 100644 index 0000000000..fe8f34c5f7 --- /dev/null +++ b/quadratic-client/public/.well-known/jwks.json @@ -0,0 +1,26 @@ +{ + "keys": [ + { + "p": "8cs6LVWfWM3_TOQZdNWG09sqq8qGbuSejp3rcvDedVh_NAO9D5byE7cpdM2_4_enh1wXoUzzpL0MSHFLAAErJywKLUgyGmjdmJdA7IFuOV4lPNydcSuyyHm4pXVSc_ZtB0MfVPdAh1TO5zyjkk5IbIC8IYOICI1dxu8namdP5MM", + "kty": "RSA", + "q": "uK9v3Hp3X_FESMl3Tbv1ZF-7-oAwdpSq_hMnzb0CCVJ1nVK7cs4RtYhZoVLDlPg98oe35HGjemdrk_WVduUH3H2wbbK0bE9v_yG-WPor4GPhxmmw8e7KV0qkOK3y2x8gC0P2IlY7PpuxfOIHl-z9PFaddfXxQaNOym_naiK1jnc", + "d": "cGVslzyvoWR487B2gXnrg3MPFFFpyD4a7epTKFa7baGd_5oBxHDgZrZcYW6wrlHNuN_ZDXucNneZeg7m5ZLUG6Uz9cYh7aBmOXiAU3Ag7ImFEVMSIKUHSGq83eKsLS8hiowEx9LeinGHr8gEHYJ9JqYV8yZOuc0_V3MQuZnCi6Xg_WmYRN8eMBV8jPKIILQX10ifrgkVSF3xXi11jN1fUiC17xCRyUArWM7c22CfONhxIXp4inGzJjoNMU7BhLJnpdgBpm9RCmNESRP2U0Yhd659upFv9NFFWmSILTwFkdYW5puaVfkHBIZV7_g5OJV7DsE2Cti5jv3SLk4CyCj68Q", + "e": "AQAB", + "use": "sig", + "kid": "ory-example", + "qi": "4Ji4_LTZURiRJBp72ULUbEIukrXwigrGKqIMKA7M2fYB6PlZ5RjxNsdGrTttaMzKyHdDPQWY01fBNzWvaZCNndUu-PsjDj2tO0a-EfRys4onIeV0srSfk7QXlH-u-gCqYulEvMDXSDrzjW8HBq4n3Z94GeZxa5kE0XD13qf89NU", + "dp": "ofZKivFuonKiD2Q_NQaOoLyPEbHAaOmU190qSLzVlm7oDfRvINEwaEppZ4cmgVJzknT6kx5TmcbUQnY5EdC2ki-qxXg1r4EM5lhysbllFuJcOS9h-tuVjzoRmCtFRs4LbDDm_Of9_mitizEQNEFhu-RjoGNVrLzc0xOBKIH5fzc", + "alg": "RS256", + "dq": "H71OzSZi46M0KAovrbVSu_hT9v4W1hpAtL-YBJyp_-4i9nGkc1uE4ZzYQohVwoFTLB409VauULf7XgdDs5Yy3qrfKksfBMo2JjOnYeVEqyCfSZkaZsmyDoRuaqtCZHQZ7rW0VDxbnCvnud2ijnKVJsx_7SjiWHR3cwT-UVg7uYs", + "n": "rm_FZLcTUKdiCnv5zc5284DBQ2RO0f-VLpD4CcJ6Y3Po0zYoMiniOCdmTn1I5klau6BfVQWpDfdqV-G-HhRhLpdDy30Zs-t1veN-YxXgBOnF6neqww5tivwtJ--SS5S2m4UyiNxqlWy4-1FttpCKwu-Dm8d2Q7ppUal6wQojGOnCje8P499a0x9JjMZbh0DcUke2mn_ScmVTV8IEC7caMyo3D_HVdaMuNDN2N2O-7fRUJTVn8pgsjUfw1xP8tB-8-k6rK07X9yi_-oUyXqaqj8IhCPNMOc1UaQbrY3vvdMarQQrykkyXDPp6IL4vA3dw8q46BJvfLRsOCa1g-uaApQ" + }, + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "ory-example", + "alg": "RS256", + "n": "rm_FZLcTUKdiCnv5zc5284DBQ2RO0f-VLpD4CcJ6Y3Po0zYoMiniOCdmTn1I5klau6BfVQWpDfdqV-G-HhRhLpdDy30Zs-t1veN-YxXgBOnF6neqww5tivwtJ--SS5S2m4UyiNxqlWy4-1FttpCKwu-Dm8d2Q7ppUal6wQojGOnCje8P499a0x9JjMZbh0DcUke2mn_ScmVTV8IEC7caMyo3D_HVdaMuNDN2N2O-7fRUJTVn8pgsjUfw1xP8tB-8-k6rK07X9yi_-oUyXqaqj8IhCPNMOc1UaQbrY3vvdMarQQrykkyXDPp6IL4vA3dw8q46BJvfLRsOCa1g-uaApQ" + } + ] +} diff --git a/quadratic-client/src/app/actions.ts b/quadratic-client/src/app/actions.ts index 90cd486f7e..ca0232a6de 100644 --- a/quadratic-client/src/app/actions.ts +++ b/quadratic-client/src/app/actions.ts @@ -1,6 +1,5 @@ import { EditorInteractionState } from '@/app/atoms/editorInteractionStateAtom'; -import { getActionFileDuplicate } from '@/routes/api.files.$uuid'; -import { apiClient } from '@/shared/api/apiClient'; +import { getActionFileDelete, getActionFileDuplicate } from '@/routes/api.files.$uuid'; import { GlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; import { ROUTES } from '@/shared/constants/routes'; import { ApiTypes, FilePermission, FilePermissionSchema, TeamPermission } from 'quadratic-shared/typesAndSchemas'; @@ -67,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); }, }; @@ -85,11 +84,23 @@ export const deleteFile = { label: 'Delete', isAvailable: ({ filePermissions }: IsAvailableArgs) => filePermissions.includes(FILE_DELETE), // TODO: (enhancement) handle this async operation in the UI similar to /files/create - async run({ uuid, addGlobalSnackbar }: { uuid: string; addGlobalSnackbar: GlobalSnackbar['addGlobalSnackbar'] }) { + async run({ + uuid, + userEmail, + redirect, + submit, + addGlobalSnackbar, + }: { + uuid: string; + userEmail: string; + redirect: boolean; + submit: SubmitFunction; + addGlobalSnackbar: GlobalSnackbar['addGlobalSnackbar']; + }) { if (window.confirm('Please confirm you want to delete this file.')) { try { - await apiClient.files.delete(uuid); - window.location.href = '/'; + const data = getActionFileDelete({ userEmail, redirect }); + submit(data, { method: 'POST', action: ROUTES.API.FILE(uuid), encType: 'application/json' }); } catch (e) { addGlobalSnackbar('Failed to delete file. Try again.', { severity: 'error' }); } diff --git a/quadratic-client/src/app/actions/actions.ts b/quadratic-client/src/app/actions/actions.ts index 5425f87480..cc5b30c9d7 100644 --- a/quadratic-client/src/app/actions/actions.ts +++ b/quadratic-client/src/app/actions/actions.ts @@ -75,6 +75,7 @@ export enum Action { ZoomTo50 = 'zoom_to_50', ZoomTo100 = 'zoom_to_100', ZoomTo200 = 'zoom_to_200', + ZoomReset = 'zoom_reset', Save = 'save', SwitchSheetNext = 'switch_sheet_next', SwitchSheetPrevious = 'switch_sheet_previous', @@ -113,7 +114,7 @@ export enum Action { JumpCursorContentRight = 'jump_cursor_content_right', ExpandSelectionRight = 'expand_selection_right', ExpandSelectionContentRight = 'expand_selection_content_right', - GotoA0 = 'goto_A0', + GotoA1 = 'goto_A1', GotoBottomRight = 'goto_bottom_right', GotoRowStart = 'goto_row_start', GotoRowEnd = 'goto_row_end', @@ -122,6 +123,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', @@ -139,6 +141,7 @@ export enum Action { InsertRowBelow = 'insert_row_below', DeleteRow = 'delete_row', DeleteColumn = 'delete_column', +<<<<<<< HEAD FlattenTable = 'flatten_table', GridToDataTable = 'grid_to_data_table', @@ -159,4 +162,7 @@ export enum Action { HideTableColumn = 'hide_table_column', ShowAllColumns = 'show_all_columns', EditTableCode = 'edit_table_code', +======= + ToggleAIAnalyst = 'toggle_ai_analyst', +>>>>>>> origin/qa } diff --git a/quadratic-client/src/app/actions/columnRowSpec.ts b/quadratic-client/src/app/actions/columnRowSpec.ts index 3ff2428a03..f13d0830e7 100644 --- a/quadratic-client/src/app/actions/columnRowSpec.ts +++ b/quadratic-client/src/app/actions/columnRowSpec.ts @@ -10,64 +10,59 @@ const isColumnRowAvailable = ({ isAuthenticated }: ActionAvailabilityArgs) => { return !isEmbed && isAuthenticated; }; +const isColumnFinite = () => sheets.sheet.cursor.isSelectedColumnsFinite(); +const isColumnRowAvailableAndColumnFinite = (args: ActionAvailabilityArgs) => + isColumnRowAvailable(args) && isColumnFinite(); +const isRowFinite = () => sheets.sheet.cursor.isSelectedRowsFinite(); +const isColumnRowAvailableAndRowFinite = (args: ActionAvailabilityArgs) => isColumnRowAvailable(args) && isRowFinite(); + const insertColumnLeft: ActionSpec = { label: 'Insert column to the left', - isAvailable: isColumnRowAvailable, + isAvailable: isColumnRowAvailableAndColumnFinite, Icon: AddIcon, run: () => - quadraticCore.insertColumn(sheets.sheet.id, sheets.sheet.cursor.cursorPosition.x, true, sheets.getCursorPosition()), + quadraticCore.insertColumn(sheets.sheet.id, sheets.sheet.cursor.position.x, true, sheets.getCursorPosition()), }; const insertColumnRight: ActionSpec = { label: 'Insert column to the right', - isAvailable: isColumnRowAvailable, + isAvailable: isColumnRowAvailableAndColumnFinite, Icon: AddIcon, run: () => - quadraticCore.insertColumn( - sheets.sheet.id, - sheets.sheet.cursor.cursorPosition.x + 1, - false, - sheets.getCursorPosition() - ), + quadraticCore.insertColumn(sheets.sheet.id, sheets.sheet.cursor.position.x + 1, false, sheets.getCursorPosition()), }; const deleteColumns: ActionSpec = { label: 'Delete columns', - isAvailable: ({ isAuthenticated }: ActionAvailabilityArgs) => !isEmbed && isAuthenticated, + isAvailable: ({ isAuthenticated }: ActionAvailabilityArgs) => !isEmbed && isAuthenticated && isColumnFinite(), Icon: DeleteIcon, run: () => { - const columns = sheets.sheet.cursor.getColumnsSelection(); + const columns = sheets.sheet.cursor.getSelectedColumns(); quadraticCore.deleteColumns(sheets.sheet.id, columns, sheets.getCursorPosition()); }, }; const insertRowAbove: ActionSpec = { label: 'Insert row above', - isAvailable: isColumnRowAvailable, + isAvailable: isColumnRowAvailableAndRowFinite, Icon: AddIcon, - run: () => - quadraticCore.insertRow(sheets.sheet.id, sheets.sheet.cursor.cursorPosition.y, true, sheets.getCursorPosition()), + run: () => quadraticCore.insertRow(sheets.sheet.id, sheets.sheet.cursor.position.y, true, sheets.getCursorPosition()), }; const insertRowBelow: ActionSpec = { label: 'Insert row below', - isAvailable: isColumnRowAvailable, + isAvailable: isColumnRowAvailableAndRowFinite, Icon: AddIcon, run: () => - quadraticCore.insertRow( - sheets.sheet.id, - sheets.sheet.cursor.cursorPosition.y + 1, - false, - sheets.getCursorPosition() - ), + quadraticCore.insertRow(sheets.sheet.id, sheets.sheet.cursor.position.y + 1, false, sheets.getCursorPosition()), }; const deleteRows: ActionSpec = { label: 'Delete rows', - isAvailable: ({ isAuthenticated }: ActionAvailabilityArgs) => !isEmbed && isAuthenticated, + isAvailable: ({ isAuthenticated }: ActionAvailabilityArgs) => !isEmbed && isAuthenticated && isRowFinite(), Icon: DeleteIcon, run: () => { - const rows = sheets.sheet.cursor.getRowsSelection(); + const rows = sheets.sheet.cursor.getSelectedRows(); quadraticCore.deleteRows(sheets.sheet.id, rows, sheets.getCursorPosition()); }, }; diff --git a/quadratic-client/src/app/actions/editActionsSpec.ts b/quadratic-client/src/app/actions/editActionsSpec.ts index 8fc33cb604..b263f09878 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 { 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'; @@ -42,6 +43,7 @@ type EditActionSpec = Pick< | Action.FillRight | Action.FillDown | Action.EditCell + | Action.ToggleArrowMode | Action.DeleteCell | Action.CloseInlineEditor | Action.SaveInlineEditor @@ -162,19 +164,51 @@ 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.position; + 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, + cursorMode: cell ? CursorMode.Edit : CursorMode.Enter, + }); + }); + } + }); + return true; + } + }, + }, + [Action.ToggleArrowMode]: { + label: 'Toggle arrow mode', + run: () => { + if (!inlineEditorHandler.isEditingFormula()) { + const { x, y } = sheets.sheet.cursor.position; + quadraticCore.getCodeCell(sheets.sheet.id, x, y).then((code) => { + if (code) { + doubleClickCell({ + column: Number(code.x), + row: Number(code.y), + language: code.language, + cell: '', + cursorMode: CursorMode.Edit, + }); } 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, cursorMode: CursorMode.Edit }); }); } }); + return true; } }, }, @@ -205,7 +239,7 @@ export const editActionsSpec: EditActionSpec = { [Action.TriggerCell]: { label: 'Trigger cell', run: () => { - const p = sheets.sheet.cursor.cursorPosition; + const p = sheets.sheet.cursor.position; events.emit('triggerCell', p.x, p.y, true); }, }, diff --git a/quadratic-client/src/app/actions/formatActionsSpec.ts b/quadratic-client/src/app/actions/formatActionsSpec.ts index 5991d60e2d..97cbd9b835 100644 --- a/quadratic-client/src/app/actions/formatActionsSpec.ts +++ b/quadratic-client/src/app/actions/formatActionsSpec.ts @@ -4,7 +4,7 @@ import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { convertReactColorToString } from '@/app/helpers/convertColor'; import { clearFormattingAndBorders, - removeCellNumericFormat, + removeNumericFormat, setAlign, setBold, setCellCommas, @@ -212,7 +212,7 @@ export const formatActionsSpec: FormatActionSpec = { label: 'Automatic', Icon: FormatNumberAutomaticIcon, run: () => { - removeCellNumericFormat(); + removeNumericFormat(); }, }, [Action.FormatNumberCurrency]: { diff --git a/quadratic-client/src/app/actions/insertActionsSpec.ts b/quadratic-client/src/app/actions/insertActionsSpec.ts index 979b79be4b..f90b1fd74d 100644 --- a/quadratic-client/src/app/actions/insertActionsSpec.ts +++ b/quadratic-client/src/app/actions/insertActionsSpec.ts @@ -39,9 +39,10 @@ export const insertActionsSpec: InsertActionSpec = { labelVerbose: 'Insert Python code', run: () => { if (!pixiAppSettings.setCodeEditorState) return; - const cursor = sheets.sheet.cursor.getCursor(); + const cursor = sheets.sheet.cursor.position; pixiAppSettings.setCodeEditorState((prev) => ({ ...prev, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -60,9 +61,10 @@ export const insertActionsSpec: InsertActionSpec = { labelVerbose: 'Insert JavaScript code', run: () => { if (!pixiAppSettings.setCodeEditorState) return; - const cursor = sheets.sheet.cursor.getCursor(); + const cursor = sheets.sheet.cursor.position; pixiAppSettings.setCodeEditorState((prev) => ({ ...prev, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -81,9 +83,10 @@ export const insertActionsSpec: InsertActionSpec = { labelVerbose: 'Insert Formula', run: () => { if (!pixiAppSettings.setCodeEditorState) return; - const cursor = sheets.sheet.cursor.getCursor(); + const cursor = sheets.sheet.cursor.position; pixiAppSettings.setCodeEditorState((prev) => ({ ...prev, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -102,9 +105,10 @@ export const insertActionsSpec: InsertActionSpec = { labelVerbose: 'Insert Python chart (Plotly)', run: () => { if (!pixiAppSettings.setCodeEditorState) return; - const cursor = sheets.sheet.cursor.getCursor(); + const cursor = sheets.sheet.cursor.position; pixiAppSettings.setCodeEditorState((prev) => ({ ...prev, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -123,9 +127,10 @@ export const insertActionsSpec: InsertActionSpec = { labelVerbose: 'Insert JavaScript chart (Chart.js)', run: () => { if (!pixiAppSettings.setCodeEditorState) return; - const cursor = sheets.sheet.cursor.getCursor(); + const cursor = sheets.sheet.cursor.position; pixiAppSettings.setCodeEditorState((prev) => ({ ...prev, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -144,9 +149,10 @@ export const insertActionsSpec: InsertActionSpec = { labelVerbose: 'Insert JavaScript API request', run: () => { if (!pixiAppSettings.setCodeEditorState) return; - const cursor = sheets.sheet.cursor.getCursor(); + const cursor = sheets.sheet.cursor.position; pixiAppSettings.setCodeEditorState((prev) => ({ ...prev, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -165,9 +171,10 @@ export const insertActionsSpec: InsertActionSpec = { labelVerbose: 'Insert Python API request', run: () => { if (!pixiAppSettings.setCodeEditorState) return; - const cursor = sheets.sheet.cursor.getCursor(); + const cursor = sheets.sheet.cursor.position; pixiAppSettings.setCodeEditorState((prev) => ({ ...prev, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -230,16 +237,14 @@ export const insertActionsSpec: InsertActionSpec = { labelVerbose: 'Insert cell reference', run: () => { if (pixiAppSettings.codeEditorState.showCodeEditor) { - const { sheetId, pos, language } = pixiAppSettings.codeEditorState.codeCell; - insertCellRef(pos, sheetId, language); + const { sheetId, language } = pixiAppSettings.codeEditorState.codeCell; + insertCellRef(sheetId, language); } }, }, [Action.RemoveInsertedCells]: { label: 'Remove inserted cells', - run: () => { - // TODO(ayush): add this when refactoring shortcuts to use action specs - }, + run: () => {}, // TODO(ayush): add this when refactoring shortcuts to use action specs }, [Action.InsertToday]: { label: "Insert today's date", @@ -249,7 +254,7 @@ export const insertActionsSpec: InsertActionSpec = { const cursor = sheet.cursor; const today = new Date(); const formattedDate = `${today.getFullYear()}/${today.getMonth() + 1}/${today.getDate()}`; - quadraticCore.setCellValue(sheet.id, cursor.cursorPosition.x, cursor.cursorPosition.y, formattedDate); + quadraticCore.setCellValue(sheet.id, cursor.position.x, cursor.position.y, formattedDate); }, }, [Action.InsertTodayTime]: { @@ -260,7 +265,7 @@ export const insertActionsSpec: InsertActionSpec = { const cursor = sheet.cursor; const today = new Date(); const formattedTime = `${today.getHours()}:${today.getMinutes()}:${today.getSeconds()}`; - quadraticCore.setCellValue(sheet.id, cursor.cursorPosition.x, cursor.cursorPosition.y, formattedTime); + quadraticCore.setCellValue(sheet.id, cursor.position.x, cursor.position.y, formattedTime); }, }, }; diff --git a/quadratic-client/src/app/actions/selectionActionsSpec.ts b/quadratic-client/src/app/actions/selectionActionsSpec.ts index b7f060b9be..815b18b99c 100644 --- a/quadratic-client/src/app/actions/selectionActionsSpec.ts +++ b/quadratic-client/src/app/actions/selectionActionsSpec.ts @@ -1,7 +1,6 @@ import { Action } from '@/app/actions/actions'; import { ActionSpecRecord } from '@/app/actions/actionsSpec'; import { sheets } from '@/app/grid/controller/Sheets'; -import { selectAllCells, selectColumns, selectRows } from '@/app/gridGL/helpers/selectCells'; type SelectionActionSpec = Pick< ActionSpecRecord, @@ -26,7 +25,7 @@ type SelectionActionSpec = Pick< | Action.ExpandSelectionRight | Action.ExpandSelectionContentRight | Action.MoveCursorRightWithSelection - | Action.GotoA0 + | Action.GotoA1 | Action.GotoBottomRight | Action.GotoRowStart | Action.GotoRowEnd @@ -36,43 +35,19 @@ export const selectionActionsSpec: SelectionActionSpec = { [Action.SelectAll]: { label: 'Select all', run: () => { - selectAllCells(); + sheets.sheet.cursor.selectAll(); }, }, [Action.SelectColumn]: { label: 'Select column', run: () => { - const cursor = sheets.sheet.cursor; - if (cursor.columnRow?.all || cursor.columnRow?.rows?.length) { - selectAllCells(); - } else { - let columns = new Set(cursor.columnRow?.columns); - columns.add(cursor.cursorPosition.x); - cursor.multiCursor?.forEach((rect) => { - for (let x = rect.x; x < rect.x + rect.width; x++) { - columns.add(x); - } - }); - selectColumns(Array.from(columns), cursor.cursorPosition.x); - } + sheets.sheet.cursor.setColumnsSelected(); }, }, [Action.SelectRow]: { label: 'Select row', run: () => { - const cursor = sheets.sheet.cursor; - if (cursor.columnRow?.all || cursor.columnRow?.columns?.length) { - selectAllCells(); - } else { - let row = new Set(cursor.columnRow?.rows); - row.add(cursor.cursorPosition.y); - cursor.multiCursor?.forEach((rect) => { - for (let y = rect.y; y < rect.y + rect.height; y++) { - row.add(y); - } - }); - selectRows(Array.from(row), cursor.cursorPosition.y); - } + sheets.sheet.cursor.setRowsSelected(); }, }, [Action.MoveCursorUp]: { @@ -127,17 +102,8 @@ export const selectionActionsSpec: SelectionActionSpec = { label: 'Move cursor left with selection', run: () => { const cursor = sheets.sheet.cursor; - const cursorPosition = cursor.cursorPosition; - cursor.changePosition({ - keyboardMovePosition: { - x: cursorPosition.x - 1, - y: cursorPosition.y, - }, - cursorPosition: { - x: cursorPosition.x - 1, - y: cursorPosition.y, - }, - }); + const selectionEnd = cursor.selectionEnd; + cursor.selectTo(selectionEnd.x - 1, selectionEnd.y, false); }, }, [Action.MoveCursorRight]: { @@ -160,21 +126,12 @@ export const selectionActionsSpec: SelectionActionSpec = { label: 'Move cursor right with selection', run: () => { const cursor = sheets.sheet.cursor; - const cursorPosition = cursor.cursorPosition; - cursor.changePosition({ - keyboardMovePosition: { - x: cursorPosition.x + 1, - y: cursorPosition.y, - }, - cursorPosition: { - x: cursorPosition.x + 1, - y: cursorPosition.y, - }, - }); + const selectionEnd = cursor.selectionEnd; + cursor.selectTo(selectionEnd.x + 1, selectionEnd.y, false); }, }, - [Action.GotoA0]: { - label: 'Goto A0', + [Action.GotoA1]: { + label: 'Goto A1', run: () => {}, // TODO(ayush): add this when refactoring shortcuts to use action specs }, [Action.GotoBottomRight]: { @@ -183,7 +140,7 @@ export const selectionActionsSpec: SelectionActionSpec = { }, [Action.GotoRowStart]: { label: 'Goto row start', - run: () => {}, // TODO(ayush): add this when refactoring shortcuts to use action specs + run: () => sheets.sheet.cursor.moveTo(1, sheets.sheet.cursor.position.y), }, [Action.GotoRowEnd]: { label: 'Goto row end', diff --git a/quadratic-client/src/app/actions/viewActionsSpec.ts b/quadratic-client/src/app/actions/viewActionsSpec.ts index 5bb703ef53..7d501f65c1 100644 --- a/quadratic-client/src/app/actions/viewActionsSpec.ts +++ b/quadratic-client/src/app/actions/viewActionsSpec.ts @@ -2,8 +2,8 @@ import { Action } from '@/app/actions/actions'; import { ActionSpecRecord } from '@/app/actions/actionsSpec'; import { openCodeEditor } from '@/app/grid/actions/openCodeEditor'; import { sheets } from '@/app/grid/controller/Sheets'; -import { zoomIn, zoomInOut, zoomOut, zoomToFit, zoomToSelection } from '@/app/gridGL/helpers/zoom'; -import { moveViewport } from '@/app/gridGL/interaction/viewportHelper'; +import { zoomIn, zoomInOut, zoomOut, zoomReset, zoomToFit, zoomToSelection } from '@/app/gridGL/helpers/zoom'; +import { pageUpDown } from '@/app/gridGL/interaction/viewportHelper'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { KeyboardSymbols } from '@/app/helpers/keyboardSymbols'; import { CodeIcon, GoToIcon } from '@/shared/components/Icons'; @@ -18,6 +18,7 @@ type ViewActionSpec = Pick< | Action.ZoomTo50 | Action.ZoomTo100 | Action.ZoomTo200 + | Action.ZoomReset | Action.GridPanMode | Action.ShowCommandPalette | Action.TogglePresentationMode @@ -28,6 +29,7 @@ type ViewActionSpec = Pick< | Action.PageDown | Action.ShowGoToMenu | Action.ShowCellTypeMenu + | Action.ToggleAIAnalyst >; export const viewActionsSpec: ViewActionSpec = { @@ -77,6 +79,10 @@ export const viewActionsSpec: ViewActionSpec = { zoomInOut(2); }, }, + [Action.ZoomReset]: { + label: 'Reset location', + run: () => zoomReset(), + }, [Action.GridPanMode]: { label: 'Grid pan mode', run: () => {}, // TODO(ayush): add this when refactoring shortcuts to use action specs @@ -122,7 +128,6 @@ export const viewActionsSpec: ViewActionSpec = { showConnectionsMenu: false, showGoToMenu: false, showFeedbackMenu: false, - showNewFileMenu: false, showRenameFileMenu: false, showShareFileMenu: false, showSearch: false, @@ -151,15 +156,11 @@ export const viewActionsSpec: ViewActionSpec = { }, [Action.PageUp]: { label: 'Page up', - run: () => { - moveViewport({ pageUp: true }); - }, + run: () => pageUpDown(true), }, [Action.PageDown]: { label: 'Page down', - run: () => { - moveViewport({ pageDown: true }); - }, + run: () => pageUpDown(false), }, [Action.ShowGoToMenu]: { label: 'Go to', @@ -176,4 +177,11 @@ export const viewActionsSpec: ViewActionSpec = { openCodeEditor(); }, }, + [Action.ToggleAIAnalyst]: { + label: 'Chat', + run: () => { + if (!pixiAppSettings.setAIAnalystState) return; + pixiAppSettings.setAIAnalystState((prev) => ({ ...prev, showAIAnalyst: !prev.showAIAnalyst })); + }, + }, }; diff --git a/quadratic-client/src/app/ai/components/SelectAIModelMenu.tsx b/quadratic-client/src/app/ai/components/SelectAIModelMenu.tsx new file mode 100644 index 0000000000..13f271301f --- /dev/null +++ b/quadratic-client/src/app/ai/components/SelectAIModelMenu.tsx @@ -0,0 +1,67 @@ +import { useAIModel } from '@/app/ai/hooks/useAIModel'; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@/shared/shadcn/ui/dropdown-menu'; +import { cn } from '@/shared/shadcn/utils'; +import { CaretDownIcon } from '@radix-ui/react-icons'; +import mixpanel from 'mixpanel-browser'; +import { MODEL_OPTIONS } from 'quadratic-shared/AI_MODELS'; +import { useMemo } from 'react'; + +interface SelectAIModelMenuProps { + loading: boolean; + textAreaRef: React.RefObject; +} + +export function SelectAIModelMenu({ loading, textAreaRef }: SelectAIModelMenuProps) { + const [selectedMode, setSelectedModel] = useAIModel(); + const { displayName: selectedModelDisplayName } = useMemo(() => MODEL_OPTIONS[selectedMode], [selectedMode]); + + const enabledModels = useMemo(() => { + const models = Object.keys(MODEL_OPTIONS) as (keyof typeof MODEL_OPTIONS)[]; + return models.filter((model) => MODEL_OPTIONS[model].enabled); + }, []); + + return ( + + + {selectedMode && <>{selectedModelDisplayName}} + + + + { + e.preventDefault(); + textAreaRef.current?.focus(); + }} + > + {enabledModels.map((enabledModel) => { + const displayName = MODEL_OPTIONS[enabledModel].displayName; + + return ( + { + mixpanel.track('[AI].model.change', { model: enabledModel }); + setSelectedModel(enabledModel); + }} + > +
+ {displayName} +
+
+ ); + })} +
+
+ ); +} diff --git a/quadratic-client/src/app/ai/docs/ConnectionDocs.ts b/quadratic-client/src/app/ai/docs/ConnectionDocs.ts new file mode 100644 index 0000000000..454118e905 --- /dev/null +++ b/quadratic-client/src/app/ai/docs/ConnectionDocs.ts @@ -0,0 +1,79 @@ +export const ConnectionDocs = `# Connections Docs + +Use SQL to create live connections from your spreadsheets to your databases. + +Once established, you have a live connection that can be rerun, refreshed, read from, and written to your SQL database. + +You can both read and write to your databases from Quadratic. + +Once your connection has been made you can use your connection directly in the sheet. Open the code cell selection menu with \`/\` and select your database from the list - in this example it's named **Quadratic Postgres**. + +You can now query your database from your newly opened SQL code editor. You can view the schema or open the AI assistant in the bottom. + +The results of your SQL queries are returned to the sheet, with column 0, row 0 anchored to the cell location. + +You can read the data returned from queries in Python, Formulas, Javascript, etc. + +Read and manipulate your data returned from SQL to summarize results, create charts, or anything else you might want to use your data for! + +Helpful queries + +If you need help generating queries, we recommend first trying the AI assistant in your Quadratic code editor - its outputs are very helpful with writing everything from the simplest to most complex SQL queries. + +Read data into the spreadsheet + +Query all data from single table into the spreadsheet + +\`\`\`sql +SELECT * FROM table_name +\`\`\` + +Query a limited selection (100 rows) from single table into spreadsheet + +\`\`\`sql +SELECT * FROM table_name +LIMIT 100 +\`\`\` + +Query specific columns from single table into the spreadsheet + +\`\`\`sql +SELECT column_name1, column_name2 +FROM table_name +LIMIT 100 +\`\`\` + +Query all unique values in a column + +\`\`\`sql +SELECT DISTINCT column_name1 +FROM table_name +LIMIT 100 +\`\`\` + +Query data conditionally + +\`\`\`sql +-- selects 3 specific columns from a table where column1 equals some value +SELECT column1, column2, column3 +FROM table_name +WHERE column1 = 'some_value'; +\`\`\` + +\`\`\`sql +-- selects 3 specific columns from a table where column1 equals some value and column2 equals some value +SELECT column1, column2, column3 +FROM table_name +WHERE column1 = 'some_value' AND column2 = 5; +\`\`\` + +Extra considerations + +You cannot do two queries at once in SQL in Quadratic. For example, you can not create a table and then query that table in the same SQL query. + +There are some slight differences between SQL syntax across databases. + +* In Postgres it is best practice use quotes around table names and column names. +* In MySQL it is best practice to use backticks around table names and column names. +* In MS SQL Server it is best practice to use double quotes around table names and column names. +`; diff --git a/quadratic-client/src/app/ai/docs/FormulaDocs.ts b/quadratic-client/src/app/ai/docs/FormulaDocs.ts new file mode 100644 index 0000000000..076afb1b78 --- /dev/null +++ b/quadratic-client/src/app/ai/docs/FormulaDocs.ts @@ -0,0 +1,1093 @@ +export const FormulaDocs = `# Formula Docs + +Formulas in Quadratic are similar to how you'd expect in any spreadsheet. Formulas are relatively referenced as seen below. + +Work with classic spreadsheet logic - math, references, and point and click manipulation for quick data analysis. + +Get started with Formulas the same way as any other spreadsheet - click \`=\` on a cell and get started right away. Formulas are in-line by default. + +You can also optionally use multi-line Formulas for those Formulas that need to be expanded to become readable. + +To open the multi-line editor either use / and select it in the cell type selection menu or use the multi-line editor button from the in-line editor as showed below. + +The multi-line editor becomes useful when Formulas become more difficult to read than the space afforded by the in-line editor. Example: + +\`\`\`formula +IF( Z0 > 10, + IF( Z1 > 10, + IF (Z2 > 10, + AVERAGE(Z0:Z2), + "Invalid Data", + ), + "Invalid Data", + ), + "Invalid Data", +) +\`\`\` + +Cells are by default referenced relatively in Quadratic. Use $ notation to do absolute references, similar to what you'd be familiar with in traditional spreadsheets. Learn more on the Reference cells page. + +# Reference cells + +Reference data in other cells from your formula + +1. Reference an individual cell + +To reference an individual cell, use standard spreadsheet notation. The only difference is that Quadratic allows negative axes; for negative notation, append \`n\` to the cell reference. +Cells are, by default, relatively referenced. Use \`$\` notation to use absolute references. + +2. Relative cell reference + +Individual cells and ranges are, by default, referenced relatively. E.g. copy-pasting \`A1\` to the following two rows will produce \`A2\`, and \`A3\` respectively. + +To reference a range of cells relatively, use the traditional spreadsheet notation that separates two distinct cells using a semicolon as a delimiter, e.g. \`A1:D3\` + +Cells in this notation are referenced relatively, so you can drag out a cell to replicate that formula relatively across your selection. + +3. Absolute cell references + +To perform absolute cell references, use standard spreadsheet notation with \`$\`, for example \`$A$1:D3\` - \`A1\` will be copied absolutely and \`D3\` will be copied relatively if you drag to replicate. + +4. Reference across sheets + +To reference the value from another sheet, use the sheet name in quotations with an \`!\`. + +Single cell + +To reference cell F12 in a sheet named "Sheet 1" from a sheet named "Sheet 2" use: + +\`\`\`formula +"Sheet 1"!F12 +\`\`\` + +Range of cells + +To reference cells F12 to F14 in Sheet 1 from Sheet 2, use: + +\`\`\`formula +"Sheet 1"!F12:F14 +\`\`\` + +PreviousGetting startedNextFormulas cheat sheet + +Last updated 5 months ago + +On this page + +Was this helpful? + +| Formula Notation | (x, y) coordinate plane equivalent | +| ---------------- | ---------------------------------- | +| \`A0\` | (0,0) | +| \`A1\` | (0,1) | +| \`B1\` | (1,1) | +| \`An1\` | (0,-1) | +| \`nA1\` | (-1,1) | +| \`nAn1\` | (-1,-1) | + +* 1. Reference an individual cell +* 2. Relative cell reference +* 3. Absolute cell references +* 4. Reference across sheets +* Single cell +* Range of cells + +# Formulas cheat sheet + +Using formulas in the spreadsheet. + +## + +Navigation + +Operators + +Math Functions + +Trig Functions + +Stats Functions + +Logic Functions + +String Functions + +Lookup Functions + +Arrays + +Criteria + +Wildcards + +## + +Operators + +| Precedence | Symbol | Description | +| ------------ | ----------------------------------- | ------------------------ | +| 1 | x% | Percent (divides by 100) | +| 2 | +x | positive | +| -x | negative | | +| 3 | a:b | cell range | +| 4 | a..b | numeric range | +| 5 | a^b or a**b | Exponentiation | +| 6 | a*b | Multiplication | +| a/b | Division | | +| 7 | a+b | Addition | +| a-b | Subtraction | | +| 8 | a&b | String concatenation | +| 9 | a=b or a==b | Equal comparison | +| a<>b or a!=b | Not equal comparison | | +| ab | Greater than comparison | | +| a<=b | Less than or equal to comparison | | +| a>=b | Greater than or equal to comparison | | + +## Mathematics functions + +| **Function** | **Description** | +| ------------ | --------------- | +| \`SUM([numbers...])\` | Adds all values. Returns \`0\` if given no values. | +| \`SUMIF(eval_range, criteria, [sum_range])\` | Evaluates each value based on some criteria, and then adds the ones that meet those criteria. If \`sum_range\` is given, then values in \`sum_range\` are added instead wherever the corresponding value in \`eval_range\` meets the criteria. See [the documentation](https://docs.quadratichq.com/formulas) for more details about how criteria work in formulas. | +| \`SUMIFS(sum_range, eval_range1, criteria1, [more_eval_ranges_and_criteria...])\` | Adds values from \`numbers_range\` wherever the criteria are met at the corresponding value in each \`eval_range\`. See [the documentation](https://docs.quadratichq.com/formulas) for more details about how criteria work in formulas. | +| \`PRODUCT([numbers...])\` | Multiplies all values. Returns \`1\` if given no values. | +| \`ABS(number)\` | Returns the absolute value of a number. | +| \`SQRT(number)\` | Returns the square root of a number. | +| \`CEILING(number, increment)\` | Rounds a number up to the next multiple of \`increment\`. If \`number\` and \`increment\` are both negative, rounds the number down away from zero. Returns an error if \`number\` is positive but \`significance\` is negative. Returns \`0\` if \`increment\` is \`0\`. | +| \`FLOOR(number, increment)\` | Rounds a number down to the next multiple of \`increment\`. If \`number\` and \`increment\` are both negative, rounds the number up toward zero. Returns an error if \`number\` is positive but \`significance\` is negative, or if \`increment\` is \`0\` but \`number\` is nonzero. Returns \`0\` if \`increment\` is \`0\` _and_ \`number\` is \`0\`. | +| \`INT(number)\` | Rounds a number down to the next integer. Always rounds toward negative infinity. | +| \`POWER(base, exponent)\` | Returns the result of raising \`base\` to the power of \`exponent\`. | +| \`PI()\` | Returns π, the circle constant. | +| \`TAU()\` | Returns τ, the circle constant equal to 2π. | + +### CEILING.MATH + +\`\`\`formula +CEILING.MATH(number, [increment], [negative_mode]) +\`\`\` + +Examples: + +\`\`\`formula +CEILING.MATH(6.5) +\`\`\` + +\`\`\`formula +CEILING.MATH(6.5, 2) +\`\`\` + +\`\`\`formula +CEILING.MATH(-12, 5) +\`\`\` + +\`\`\`formula +CEILING.MATH(-12, 5, -1) +\`\`\` + +Rounds a number up or away from zero to the next multiple of \`increment\`. If \`increment\` is omitted, it is assumed to be \`1\`. The sign of \`increment\` is ignored. + +If \`negative_mode\` is positive or zero, then \`number\` is rounded up, toward positive infinity. If \`negative_mode\` is negative, then \`number\` is rounded away from zero. +These are equivalent when \`number\` is positive, so in this case \`negative_mode\` has no effect. + +If \`increment\` is zero, returns zero. + +### FLOOR.MATH + +\`\`\`formula +FLOOR.MATH(number, [increment], [negative_mode]) +\`\`\` + +Examples: + +\`\`\`formula +FLOOR.MATH(6.5) +\`\`\` + +\`\`\`formula +FLOOR.MATH(6.5, 2) +\`\`\` + +\`\`\`formula +FLOOR.MATH(-12, 5) +\`\`\` + +\`\`\`formula +FLOOR.MATH(-12, 5, -1) +\`\`\` + +Rounds a number down or toward zero to the next multiple of \`increment\`. If \`increment\` is omitted, it is assumed to be \`1\`. The sign of \`increment\` is ignored. + +If \`negative_mode\` is positive or zero, then \`number\` is rounded down, toward negative infinity. If \`negative_mode\` is negative, then \`number\` is rounded toward zero. These are equivalent when \`number\` is positive, so in this case \`negative_mode\` has no effect. + +If \`increment\` is zero, returns zero. + +### MOD + +\`\`\`formula +MOD(number, divisor) +\`\`\` + +Examples: + +\`\`\`formula +MOD(3.9, 3) +\`\`\` + +\`\`\`formula +MOD(-2.1, 3) +\`\`\` + +Returns the remainder after dividing \`number\` by \`divisor\`. The result always has the same sign as \`divisor\`. + +Note that \`INT(n / d) * d + MOD(n, d)\` always equals \`n\` (up to floating-point precision). + +### EXP + +\`\`\`formula +EXP(exponent) +\`\`\` + +Examples: + +\`\`\`formula +EXP(1), EXP(2/3), EXP(C9) +\`\`\` + +Returns the result of raising [Euler's number] _e_ to the power +of \`exponent\`. + +[Euler's number]: https://en.wikipedia.org/wiki/E_(mathematical_constant) + +### LOG + +\`\`\`formula +LOG(number, [base]) +\`\`\` + +Examples: + +\`\`\`formula +LOG(100) +\`\`\` + +\`\`\`formula +LOG(144, 12) +\`\`\` + +\`\`\`formula +LOG(144, 10) +\`\`\` + +Returns the [logarithm] of \`number\` to the base \`base\`. If \`base\` is omitted, it is assumed to be 10, the base of the [common logarithm]. + +[logarithm]: https://en.wikipedia.org/wiki/Logarithm +[common logarithm]: https://en.wikipedia.org/wiki/Common_logarithm + +### LOG10 + +\`\`\`formula +LOG10(number) +\`\`\` + +Examples: + +\`\`\`formula +LOG10(100) +\`\`\` + +Returns the [base-10 logarithm] of \`number\`. + +[base-10 logarithm]:https://en.wikipedia.org/wiki/Common_logarithm + +### LN + +\`\`\`formula +LN(number) +\`\`\` + +Examples: + +\`\`\`formula +LN(50) +\`\`\` + +Returns the [natural logarithm] of \`number\`. [natural logarithm]:https://en.wikipedia.org/wiki/Natural_logarithm + +## Trigonometric functions + +| **Function** | **Description** | +| ------------ | --------------- | +| \`DEGREES(radians)\` | Converts radians to degrees. | +| \`RADIANS(degrees)\` | Converts degrees to radians. | +| \`SIN(radians)\` | Returns the [sine](https://en.wikipedia.org/wiki/Trigonometric_functions) of an angle in radians. | +| \`ASIN(number)\` | Returns the [inverse sine](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) of a number, in radians, ranging from 0 to π. | +| \`COS(radians)\` | Returns the [cosine](https://en.wikipedia.org/wiki/Trigonometric_functions) of an angle in radians. | +| \`ACOS(number)\` | Returns the [inverse cosine](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) of a number, in radians, ranging from 0 to π. | +| \`TAN(radians)\` | Returns the [tangent](https://en.wikipedia.org/wiki/Trigonometric_functions) of an angle in radians. | +| \`ATAN(number)\` | Returns the [inverse tangent](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) of a number, in radians, ranging from -π/2 to π/2. | +| \`CSC(radians)\` | Returns the [cosecant](https://en.wikipedia.org/wiki/Trigonometric_functions) of an angle in radians. | +| \`ACSC(number)\` | Returns the [inverse cosecant](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) of a number, in radians, ranging from -π/2 to π/2. | +| \`SEC(radians)\` | Returns the [secant](https://en.wikipedia.org/wiki/Trigonometric_functions) of an angle in radians. | +| \`ASEC(number)\` | Returns the [inverse secant](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) of a number, in radians, ranging from 0 to π. | +| \`COT(radians)\` | Returns the [cotangent](https://en.wikipedia.org/wiki/Trigonometric_functions) of an angle in radians. | +| \`ACOT(number)\` | Returns the [inverse cotangent](https://en.wikipedia.org/wiki/Inverse_trigonometric_functions) of a number, in radians, ranging from 0 to π. | +| \`SINH(radians)\` | Returns the [hyperbolic sine](https://en.wikipedia.org/wiki/Hyperbolic_functions) of an angle in radians. | +| \`ASINH(number)\` | Returns the [inverse hyperbolic sine](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) of a number, in radians. | +| \`COSH(radians)\` | Returns the [hyperbolic cosine](https://en.wikipedia.org/wiki/Hyperbolic_functions) of an angle in radians. | +| \`ACOSH(number)\` | Returns the [inverse hyperbolic cosine](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) of a number, in radians. | +| \`TANH(radians)\` | Returns the [hyperbolic tangent](https://en.wikipedia.org/wiki/Hyperbolic_functions) of an angle in radians. | +| \`ATANH(number)\` | Returns the [inverse hyperbolic tangent](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) of a number, in radians. | +| \`CSCH(radians)\` | Returns the [hyperbolic cosecant](https://en.wikipedia.org/wiki/Hyperbolic_functions) of an angle in radians. | +| \`ACSCH(number)\` | Returns the [inverse hyperbolic cosecant](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) of a number, in radians. | +| \`SECH(radians)\` | Returns the [hyperbolic secant](https://en.wikipedia.org/wiki/Hyperbolic_functions) of an angle in radians. | +| \`ASECH(number)\` | Returns the [inverse hyperbolic secant](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) of a number, in radians. | +| \`COTH(radians)\` | Returns the [hyperbolic cotangent](https://en.wikipedia.org/wiki/Hyperbolic_functions) of an angle in radians. | +| \`ACOTH(number)\` | Returns the [inverse hyperbolic cotangent](https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions) of a number, in radians. | + +### ATAN2 + +\`\`\`formula +ATAN2(x, y) +\`\`\` + +Examples: + +\`\`\`formula +ATAN2(2, 1) +\`\`\` + +Returns the counterclockwise angle, in radians, from the X axis to the point \`(x, y)\`. Note that the argument order is reversed compared to the [typical \`atan2()\` function](https://en.wikipedia.org/wiki/Atan2). + +If both arguments are zero, returns zero. + +## Statistics functions + +| **Function** | **Description** | +| ------------ | --------------- | +| \`AVERAGE([numbers...])\` | Returns the arithmetic mean of all values. | +| \`AVERAGEIF(eval_range, criteria, [numbers_range])\` | Evaluates each value based on some criteria, and then computes the arithmetic mean of the ones that meet those criteria. If \`range_to_average\` is given, then values in \`range_to_average\` are averaged instead wherever the corresponding value in \`range_to_evaluate\` meets the criteria. See [the documentation](https://docs.quadratichq.com/formulas) for more details about how criteria work in formulas. | +| \`COUNTIF(range, criteria)\` | Evaluates each value based on some criteria, and then counts how many values meet those criteria. See [the documentation](https://docs.quadratichq.com/formulas) for more details about how criteria work in formulas. | +| \`COUNTIFS(eval_range1, criteria1, [more_eval_ranges_and_criteria...])\` | Evaluates multiple values on they're respective criteria, and then counts how many sets of values met all their criteria. See [the documentation](https://docs.quadratichq.com/formulas) for more details about how criteria work in formulas. | +| \`MIN([numbers...])\` | Returns the smallest value. Returns +∞ if given no values. | +| \`MAX([numbers...])\` | Returns the largest value. Returns -∞ if given no values. | + +### COUNT + +\`\`\`formula +COUNT([numbers...]) +\`\`\` + +Examples: + +\`\`\`formula +COUNT(A1:C42, E17) +\`\`\` + +\`\`\`formula +SUM(A1:A10) / COUNT(A1:A10) +\`\`\` + +Returns the number of numeric values. + +- Blank cells are not counted. +- Cells containing an error are not counted. + +### COUNTA + +\`\`\`formula +COUNTA([range...]) +\`\`\` + +Examples: + +\`\`\`formula +COUNTA(A1:A10) +\`\`\` + +Returns the number of non-blank values. + +- Cells with formula or code output of an empty string are counted. +- Cells containing zero are counted. +- Cells with an error are counted. + +### COUNTBLANK + +\`\`\`formula +COUNTBLANK([range...]) +\`\`\` + +Examples: + +\`\`\`formula +COUNTBLANK(A1:A10) +\`\`\` + +Counts how many values in the range are empty. + +- Cells with formula or code output of an empty string are counted. +- Cells containing zero are not counted. +- Cells with an error are not counted. + +## Logic functions + +These functions treat \`FALSE\` and \`0\` as "falsey" and all other values are "truthy." + +When used as a number, \`TRUE\` is equivalent to \`1\` and \`FALSE\` is equivalent to \`0\`. + +| **Function** | **Description** | +| ------------ | --------------- | +| \`TRUE()\` | Returns \`TRUE\`. | +| \`FALSE()\` | Returns \`FALSE\`. | +| \`NOT(boolean)\` | Returns \`TRUE\` if \`a\` is falsey and \`FALSE\` if \`a\` is truthy. | +| \`IF(condition, t, f)\` | Returns \`t\` if \`condition\` is truthy and \`f\` if \`condition\` is falsey. | +| \`IFERROR(value, fallback)\` | Returns \`fallback\` if there was an error computing \`value\`; otherwise returns \`value\`. | + +### AND + +\`\`\`formula +AND([booleans...]) +\`\`\` + +Examples: + +\`\`\`formula +AND(A1:C1) +\`\`\` + +\`\`\`formula +AND(A1, B12) +\`\`\` + +Returns \`TRUE\` if all values are truthy and \`FALSE\` if any value is falsey. + +Returns \`TRUE\` if given no values. + +### OR + +\`\`\`formula +OR([booleans...]) +\`\`\` + +Examples: + +\`\`\`formula +OR(A1:C1) +\`\`\` + +\`\`\`formula +OR(A1, B12) +\`\`\` + +Returns \`TRUE\` if any value is truthy and \`FALSE\` if all values are falsey. + +Returns \`FALSE\` if given no values. + +### XOR + +\`\`\`formula +XOR([booleans...]) +\`\`\` + +Examples: + +\`\`\`formula +XOR(A1:C1) +\`\`\` + +\`\`\`formula +XOR(A1, B12) +\`\`\` + +Returns \`TRUE\` if an odd number of values are truthy and \`FALSE\` if an even number of values are truthy. + +Returns \`FALSE\` if given no values. + +## String functions + +| **Function** | **Description** | +| ------------ | --------------- | +| \`CONCATENATE([strings...])\` | Same as \`CONCAT\`, but kept for compatibility. | +| \`LEN(s)\` | Returns half the length of the string in [Unicode code-points](https://tonsky.me/blog/unicode/). This is often the same as the number of characters in a string, but not for certain diacritics, emojis, or other cases. | +| \`LENB(s)\` | Returns half the length of the string in bytes, using UTF-8 encoding. | +| \`CODE(s)\` | Same as \`UNICODE\`. Prefer \`UNICODE\`. | +| \`CHAR(code_point)\` | Same as \`UNICHAR\`. Prefer \`UNICHAR\`. | +| \`LOWER(s)\` | Returns the lowercase equivalent of a string. | +| \`UPPER(s)\` | Returns the uppercase equivalent of a string. | +| \`PROPER(s)\` | Capitalizes letters that do not have another letter before them, and lowercases the rest. | +| \`T(v)\` | Returns a string value unmodified, or returns the empty string if passed a value other than a string. | +| \`EXACT(s1, s2)\` | Returns whether two strings are exactly equal, using case-sensitive comparison (but ignoring formatting). | + +### ARRAYTOTEXT + +\`\`\`formula +ARRAYTOTEXT(array, [format]) +\`\`\` + +Examples: + +\`\`\`formula +ARRAYTOTEXT({"Apple", "banana"; 42, "Hello, world!"}) +\`\`\` + +\`\`\`formula +ARRAYTOTEXT({"Apple", "banana"; 42, "Hello, world!"}, 1) +\`\`\` + +Converts an array of values to a string. + +If \`format\` is 0 or omitted, returns a human-readable representation such as \`Apple, banana, 42, hello, world!\`. +If \`format\` is 1, returns a machine-readable representation in valid formula syntax such as \`{"Apple", "banana", 42, "Hello, world!"}\`. If \`format\` is any other value, returns an error. + +### CONCAT + +\`\`\`formula +CONCAT([strings...]) +\`\`\` + +Examples: + +\`\`\`formula +CONCAT("Hello, ", C0, "!") +\`\`\` + +\`\`\`formula +"Hello, " & C0 & "!" +\`\`\` + +[Concatenates](https://en.wikipedia.org/wiki/Concatenation) all +values as strings. + +\`&\` can also be used to concatenate text. + +### LEFT + +\`LEFT(s, [char_count])\` + +Examples: + +\`\`\`formula +LEFT("Hello, world!") = "H" +\`\`\` + +\`\`\`formula +LEFT("Hello, world!", 6) = "Hello," +\`\`\` + +\`\`\`formula +LEFT("抱歉,我不懂普通话") = "抱" +\`\`\` + +\`\`\`formula +LEFT("抱歉,我不懂普通话", 6) = "抱歉,我不懂" +\`\`\` + +Returns the first \`char_count\` characters from the beginning of the string \`s\`. + +Returns an error if \`char_count\` is less than 0. + +If \`char_count\` is omitted, it is assumed to be 1. + +If \`char_count\` is greater than the number of characters in \`s\`, then the entire string is returned. + +### LEFTB + +\`\`\`formula +LEFTB(s, [byte_count]) +\`\`\` + +Examples: + +\`\`\`formula +LEFTB("Hello, world!") = "H" +\`\`\` + +\`\`\`formula +LEFTB("Hello, world!", 6) = "Hello," +\`\`\` + +\`\`\`formula +LEFTB("抱歉,我不懂普通话") = "" +\`\`\` + +\`\`\`formula +LEFTB("抱歉,我不懂普通话", 6) = "抱歉" +\`\`\` + +\`\`\`formula +LEFTB("抱歉,我不懂普通话", 8) = "抱歉" +\`\`\` + +Returns the first \`byte_count\` bytes from the beginning of the string \`s\`, encoded using UTF-8. + +Returns an error if \`byte_count\` is less than 0. + +If \`byte_count\` is omitted, it is assumed to be 1. If \`byte_count\` is greater than the number of bytes in \`s\`, then the entire string is returned. + +If the string would be split in the middle of a character, then \`byte_count\` is rounded down to the previous character boundary so the the returned string takes at most \`byte_count\` bytes. + +### RIGHT + +\`RIGHT(s, [char_count])\` + +Examples: + +\`\`\`formula +RIGHT("Hello, world!") = "!" +\`\`\` + +\`\`\`formula +RIGHT("Hello, world!", 6) = "world!" +\`\`\` + +\`\`\`formula +RIGHT("抱歉,我不懂普通话") = "话" +\`\`\` + +\`\`\`formula +RIGHT("抱歉,我不懂普通话", 6) = "我不懂普通话" +\`\`\` + +Returns the last \`char_count\` characters from the end of the string \`s\`. + +Returns an error if \`char_count\` is less than 0. + +If \`char_count\` is omitted, it is assumed to be 1. + +If \`char_count\` is greater than the number of characters in \`s\`, then the entire string is returned. + +### RIGHTB + +\`\`\`formula +RIGHTB(s, [byte_count]) +\`\`\` + +Examples: + +\`\`\`formula +RIGHTB("Hello, world!") = "!" +\`\`\` + +\`\`\`formula +RIGHTB("Hello, world!", 6) = "world!" +\`\`\` + +\`\`\`formula +RIGHTB("抱歉,我不懂普通话") = "" +\`\`\` + +\`\`\`formula +RIGHTB("抱歉,我不懂普通话", 6) = "通话" +\`\`\` + +\`\`\`formula +RIGHTB("抱歉,我不懂普通话", 7) = "通话" +\`\`\` + +Returns the last \`byte_count\` bytes from the end of the string \`s\`, encoded using UTF-8. + +Returns an error if \`byte_count\` is less than 0. + +If \`byte_count\` is omitted, it is assumed to be 1. + +If \`byte_count\` is greater than the number of bytes in \`s\`, then the entire string is returned. + +If the string would be split in the middle of a character, then \`byte_count\` is rounded down to the next character boundary so that the returned string takes at most \`byte_count\` bytes. + +### MID + +\`\`\`formula +MID(s, start_char, char_count) +\`\`\` + +Examples: + +\`\`\`formula +MID("Hello, world!", 4, 6) = "lo, wo" +\`\`\` + +\`\`\`formula +MID("Hello, world!", 1, 5) = "Hello" +\`\`\` + +\`\`\`formula +MID("抱歉,我不懂普通话", 4, 4) = "我不懂普" +\`\`\` + +Returns the substring of a string \`s\` starting at the \`start_char\`th character and with a length of \`char_count\`. + +Returns an error if \`start_char\` is less than 1 or if \`char_count\` is less than 0. + +If \`start_char\` is past the end of the string, returns an empty string. If \`start_char + char_count\` is past the end of the string, returns the rest of the string starting at \`start_char\`. + +### MIDB + +\`\`\`formula +MIDB(s, start_byte, byte_count) +\`\`\` + +Examples: + +\`\`\`formula +MIDB("Hello, world!", 4, 6) = "lo, wo" +\`\`\` + +\`\`\`formula +MIDB("Hello, world!", 1, 5) = "Hello" +\`\`\` + +\`\`\`formula +MIDB("抱歉,我不懂普通话", 10, 12) = "我不懂普" +\`\`\` + +\`\`\`formula +MIDB("抱歉,我不懂普通话", 8, 16) = "我不懂普" +\`\`\` + +Returns the substring of a string \`s\` starting at the \`start_byte\`th byte and with a length of \`byte_count\` bytes, encoded using UTF-8. + +Returns an error if \`start_byte\` is less than 1 or if \`byte_count\` is less than 0. + +If \`start_byte\` is past the end of the string, returns an empty string. If \`start_byte + byte_count\` is past the end of the string, returns the rest of the string starting at \`start_byte\`. + +If the string would be split in the middle of a character, then \`start_byte\` is rounded up to the next character boundary and \`byte_count\` is rounded down to the previous character boundary so that the returned string takes at most \`byte_count\` bytes. + +### UNICODE + +\`\`\`formula +UNICODE(s) +\`\`\` + +Examples: + +\`\`\`formula +UNICODE("a")=97 +\`\`\` + +\`\`\`formula +UNICODE("Alpha")=65 +\`\`\` + +Returns the first [Unicode] code point in a string as a number. If the first character is part of standard (non-extended) [ASCII], then this is the same as its ASCII number. + +[Unicode]: https://en.wikipedia.org/wiki/Unicode +[ASCII]: https://en.wikipedia.org/wiki/ASCII + +### UNICHAR + +\`\`\`formula +UNICHAR(code_point) +\`\`\` + +Examples: + +\`\`\`formula +UNICHAR(97) = "a" +\`\`\` + +\`\`\`formula +UNICHAR(65) = "A" +\`\`\` + +Returns a string containing the given [Unicode] code unit. For numbers in the range 0-127, this converts from a number to its corresponding [ASCII] character. + +[Unicode]: https://en.wikipedia.org/wiki/Unicode +[ASCII]: https://en.wikipedia.org/wiki/ASCII + +### CLEAN + +\`\`\`formula +CLEAN(s) +\`\`\` + +Examples: + +\`\`\`formula +CLEAN(CHAR(9) & "(only the parenthetical will survive)" & CHAR(10)) +\`\`\` + +Removes nonprintable [ASCII] characters 0-31 (0x00-0x1F) from a string. This removes tabs and newlines, but not spaces. + +[ASCII]: https://en.wikipedia.org/wiki/ASCII + +### TRIM + +\`\`\`formula +TRIM(s) +\`\`\` + +Examples: + +\`\`\`formula +TRIM(" a b c ")="a b c" +\`\`\` + +Removes spaces from the beginning and end of a string \`s\`, and replaces each run of consecutive space within the string with a single space. + +[Other forms of whitespace][whitespace], including tabs and newlines, are preserved. + +[whitespace]: https://en.wikipedia.org/wiki/Whitespace_character + +### NUMBERVALUE + +\`\`\`formula +NUMBERVALUE(s, [decimal_sep], [group_sep]) +\`\`\` + +Examples: + +\`\`\`formula +NUMBERVALUE("4,000,096.25") +\`\`\` + +\`\`\`formula +NUMBERVALUE("4.000.096,25") +\`\`\` + +Parses a number from a string \`s\`, using \`decimal_sep\` as the decimal separator and \`group_sep\` as the group separator. + +If \`decimal_sep\` is omitted, it is assumed to be \`.\`. If \`group_sep\` is omitted, it is assumed to be \`,\`. Only the first character of each is considered. +If the decimal separator and the group separator are the same or if either is an empty string, an error is returned. + +The decimal separator must appear at most once in the string. The group separator must not appear at any point after a decimal separator. +Whitespace may appear anywhere in the string.Whitespace and group separators are ignored and have no effect on the returned number. + +## Lookup functions + +| **Function** | **Description** | +| ------------ | --------------- | +| \`INDIRECT(cellref_string)\` | Returns the value of the cell at a given location. | + +### VLOOKUP + +\`\`\`formula +VLOOKUP(search_key, search_range, output_col, [is_sorted]) +\`\`\` + +Examples: + +\`\`\`formula +VLOOKUP(17, A1:C10, 3) +\`\`\` + +\`\`\`formula +VLOOKUP(17, A1:C10, 2, FALSE) +\`\`\` + +Searches for a value in the first vertical column of a range and return the corresponding cell in another vertical column, or an error if no match is found. + +If \`is_sorted\` is \`TRUE\`, this function uses a [binary search algorithm](https://en.wikipedia.org/wiki/Binary_search_algorithm), so the first column of \`search_range\` must be sorted, +with smaller values at the top and larger values at the bottom; otherwise the result of this function will be meaningless. If \`is_sorted\` is omitted, it is assumed to be \`false\`. + +If any of \`search_key\`, \`output_col\`, or \`is_sorted\` is an array, then they must be compatible sizes and a lookup will be performed for each corresponding set of elements. + +### HLOOKUP + +\`\`\`formula +HLOOKUP(search_key, search_range, output_row, [is_sorted]) +\`\`\` + +Examples: + +\`\`\`formula +HLOOKUP(17, A1:Z3, 3) +\`\`\` + +\`\`\`formula +HLOOKUP(17, A1:Z3, 2, FALSE) +\`\`\` + +Searches for a value in the first horizontal row of a range and return the corresponding cell in another horizontal row, or an error if no match is found. + +If \`is_sorted\` is \`TRUE\`, this function uses a [binary search algorithm](https://en.wikipedia.org/wiki/Binary_search_algorithm), so the first row of \`search_range\` must be sorted, +with smaller values at the left and larger values at the right; otherwise the result of this function will be meaningless. If \`is_sorted\` is omitted, it is assumed to be \`false\`. + +If any of \`search_key\`, \`output_col\`, or \`is_sorted\` is an array, then they must be compatible sizes and a lookup will be performed for each corresponding set of elements. + +### XLOOKUP + +\`\`\`formula +XLOOKUP(search_key, search_range, output_range, [fallback], [match_mode], [search_mode]) +\`\`\` + +Examples: + +\`\`\`formula +XLOOKUP("zebra", A1:Z1, A4:Z6) +\`\`\` + +\`\`\`formula +XLOOKUP({"zebra"; "aardvark"}, A1:Z1, A4:Z6) +\`\`\` + +\`\`\`formula +XLOOKUP(50, C4:C834, B4:C834, {-1, 0, "not found"}, -1, 2) +\`\`\` + +Searches for a value in a linear range and returns a row or column from another range. + +\`search_range\` must be either a single row or a single column. + +#### Match modes + +There are four match modes: + +- 0 = exact match (default) +- -1 = next smaller +- 1 = next larger +- 2 = wildcard + +See [the documentation](https://docs.quadratichq.com/formulas#31e708d41a1a497f8677ff01dddff38b) for more details about how wildcards work in formulas. + +#### Search modes + +There are four search modes: + +- 1 = linear search (default) +- -1 = reverse linear search +- 2 = [binary search](https://en.wikipedia.org/wiki/Binary_search_algorithm) +- -2 = reverse binary search + +Linear search finds the first matching value, while reverse linear search finds the last matching value. + +Binary search may be faster than linear search, but binary search requires that values are sorted, with smaller values at the top or left and larger values at the bottom or right. +Reverse binary search requires that values are sorted in the opposite direction. If \`search_range\` is not sorted, then the result of this function will be meaningless. + +Binary search is not compatible with the wildcard match mode. + +#### Result + +If \`search_range\` is a row, then it must have the same width as \`output_range\` so that each value in \`search_range\` corresponds to a column in \`output_range\`. In this case, the **search axis** is vertical. + +If \`search_range\` is a column, then it must have the same height as \`output_range\` so that each value in \`search_range\` corresponds to a row in \`output_range\`. In this case, the **search axis** is horizontal. + +If a match is not found, then \`fallback\` is returned instead. If there is no match and \`fallback\` is omitted, then returns an error. + +If any of \`search_key\`, \`fallback\`, \`match_mode\`, or \`search_mode\` is an array, then they must be compatible sizes and a lookup will be performed for each corresponding set of elements. +These arrays must also have compatible size with the non-search axis of \`output_range\`. + +### MATCH + +\`\`\`formula +MATCH(search_key, search_range, [match_mode]) +\`\`\` + +Examples: + +\`\`\`formula +MATCH(12, {10, 20, 30}) +\`\`\` + +\`\`\`formula +MATCH(19, {10, 20, 30}, -1) +\`\`\` + +\`\`\`formula +MATCH("A", {"a"; "b"; "c"}, 0) +\`\`\` + +Searches for a value in a range and returns the index of the first match, starting from 1. + +If \`match_mode\` is \`1\` (the default), then the index of the _greatest value less than_ \`search_key\` will be returned. In +this mode, \`search_range\` must be sorted in ascending order, with smaller values at the top or left and larger values at the bottom or right; otherwise the result of this function will be meaningless. + +If \`match_mode\` is \`-1\`, then the index of the _smallest value greater than_ \`search_key\` will be returned. +In this mode, \`search_range\` must be sorted in ascending order, with larger values at the top or left and smaller values at the bottom or right; otherwise the result of this function will be meaningless. + +If \`match_mode\` is \`0\`, then the index of the first value _equal_ to \`search_key\` will be returned. In this mode, \`search_range\` may be in any order. \`search_key\` may also be a wildcard. + +See [the documentation](https://docs.quadratichq.com/formulas#31e708d41a1a497f8677ff01dddff38b) for more details about how wildcards work in formulas. + +### INDEX + +\`\`\`formula +INDEX(range, [row], [column], [range_num]) +\`\`\` + +Examples: + +\`\`\`formula +INDEX({1, 2, 3; 4, 5, 6}, 1, 3) +\`\`\` + +\`\`\`formula +INDEX(A1:A100, 42) +\`\`\` + +\`\`\`formula +INDEX(A6:Q6, 12) +\`\`\` + +\`\`\`formula +INDEX((A1:B6, C1:D6, D1:D100), 1, 5, C6) +\`\`\` + +\`\`\`formula +E1:INDEX((A1:B6, C1:D6, D1:D100), 1, 5, C6) +\`\`\` + +\`\`\`formula +INDEX((A1:B6, C1:D6, D1:D100), 1, 5, C6):E1 +\`\`\` + +\`\`\`formula +INDEX(A3:Q3, A2):INDEX(A6:Q6, A2) +\`\`\` + +Returns the element in \`range\` at a given \`row\` and \`column\`. If the array is a single row, then \`row\` may be omitted; otherwise it is required. +If the array is a single column, then \`column\` may be omitted; otherwise it is required. + +If \`range\` is a group of multiple range references, then the extra parameter \`range_num\` indicates which range to index from. + +When \`range\` is a range references or a group of range references, \`INDEX\` may be used as part of a new range reference. + +## Arrays + +An array can be written using \`{}\`, with \`,\` between values within a row and \`;\` between rows. For example, \`{1, 2, 3; 4, 5, 6}\` is an array with two rows and three columns: + +| 1 | 2 | 3 | +| - | - | - | +| 4 | 5 | 6 | + +Arrays cannot be empty and every row must be the same length. + +Numeric ranges (such as \`1..10\`) and cell ranges (such as \`A1:A10\`) also produce arrays. All operators and most functions can operate on arrays, following these rules: + +1. Operators always operate element-wise. For example, \`{1, 2, 3} + {10, 20, 30}\` produces \`{11, 22, 33}\`. +2. Functions that take a fixed number of values operate element-wise. For example, \`NOT({TRUE, TRUE, FALSE})\` produces \`{FALSE, FALSE, TRUE}\`. +3. Functions that can take any number of values expand the array into individual values. For example, \`SUM({1, 2, 3})\` is the same as \`SUM(1, 2, 3)\`. + +When arrays are used element-wise, they must be the same size. For example, \`{1, 2} + {10, 20, 30}\` produces an error. + +When an array is used element-wise with a single value, the value is expanded into an array of the same size. For example, \`{1, 2, 3} + 10\` produces \`{11, 12, 13}\`. + +## Criteria + +Some functions, such as \`SUMIF()\`, take a **criteria** parameter that other values are compared to. A criteria value can be a literal value, such as \`1\`, \`FALSE\`, \`"blue"\`, etc. A literal value checks for equality (case-insensitive). However, starting a string with a comparison operator enables more complex criteria: + +| **Symbol** | **Description** | +| -------------------- | -------------------------------- | +| "=blue" or "==blue" | Equal comparison | +| "<>blue" or "!=blue" | Not-equal comparison | +| "blue" | Greater-than comparison | +| "<=blue" | Less-than-or-equal comparison | +| ">=blue" | Greater-than-or-equal comparison | + +For example, \`COUNTIF(A1:A10, ">=3")\` counts all values greater than or equal to three, and \`COUNTIF(A1:A10, "<>blue")\` counts all values _not_ equal to the text \`"blue"\` (excluding quotes). + +Numbers and booleans are compared by value (with \`TRUE\`=1 and \`FALSE\`=0), while strings are compared case-insensitive lexicographically. For example, \`"aardvark"\` is less than \`"Camel"\` which is less than \`"zebra"\`.\`"blue"\` and \`"BLUE"\` are considered equal. + +## Wildcards + +Wildcard patterns can be used … + +* … When using a criteria parameter with an equality-based comparison (\`=\`, \`==\`, \`<>\`, \`!=\`, or no operator) +* … When using the \`XLOOKUP\` function with a \`match_mode\` of \`2\` (wildcard) + +In wildcards, the special symbols \`?\` and \`*\` can be used to match certain text patterns: \`?\` matches any single character and \`*\` matches any sequence of zero or more characters. For example, \`DEFEN?E\` matches the strings \`"defence"\` and \`"defense"\` but not \`"defenestrate"\`. \`*ATE\` matches the strings \`"ate"\`,\`"inflate"\`,\`"late"\` but not \`"wait"\`. Multiple \`?\` and \`*\` are also allowed. + +To match a literal \`?\` or \`*\`, prefix it with a tilde \`~\`: for example, \`COUNTIF(A1:A10, "HELLO~?")\` matches only the string \`"Hello?"\` (and uppercase/lowercase variants). + +To match a literal tilde \`~\` in a string with \`?\` or \`*\`, replace it with a double tilde \`~~\`. For example, \`COUNTIF(A1:A10, "HELLO ~~?")\` matches the strings \`"hello ~Q"\`,\`"hello ~R"\` etc. If the string does not contain any \`?\` or \`*\`, then tildes do not need to be escaped. +`; diff --git a/quadratic-client/src/app/ai/docs/JavascriptDocs.ts b/quadratic-client/src/app/ai/docs/JavascriptDocs.ts new file mode 100644 index 0000000000..baa753425f --- /dev/null +++ b/quadratic-client/src/app/ai/docs/JavascriptDocs.ts @@ -0,0 +1,389 @@ +export const JavascriptDocs = `# Javascript Docs + +With Javascript in Quadratic, the world's most popular programming language meets the world's most popular tool for working with data - spreadsheets. + +Below are a bunch of quick links to find more details on how to write Javascript in Quadratic. + +# Reference cells + +Reference cells from JavaScript. + +In Quadratic, reference individual cells from Javascript for single values or reference a range of cells for multiple values. + +Referencing individual cells + +To reference an individual cell, use the global function \`q.cells\` which returns the cell value. + +\`\`\`javascript +// NOTE: uses the same A1 notation as Formulas +// Following function reads the value in cell A1 and places in variable x +let x = q.cells('A1') + +# return statement gets returned to the sheet +return x; +\`\`\` + +Referencing a range of cells + +To reference a range of cells, use the global function \`q.cells\`. This returns an array. + +\`\`\`javascript +let let x = q.cells('A1:A5') // Returns a 1x5 array spanning from A1 to A5 + +let let x = q.cells('A1:C7') // Returns a 3x7 array of arrays spanning from A1 to C7 + +let let x = q.cells('A') // Returns all values in column A into a single-column DataFrame + +let let x = q.cells('A:C') // Returns all values in columns A to C into a three-column DataFrame + +let let x = q.cells('A5:A') // Returns all values in column A starting at A5 and going down + +let let x = q.cells('A5:C') // Returns all values in column A to C, starting at A5 and going down +\`\`\` + +Referencing another sheet + +To reference another sheet's cells or range of cells use the following: + +\`\`\`javascript +// Use the sheet name as an argument for referencing range of cells +let x = q.cells("'Sheet_name_here'!A1:C9") + +// For individual cell reference +let x = q.cells("'Sheet_name_here'!A1") +\`\`\` + +Column references + +To reference all the data in a column or set of columns without defining the range, use the following syntax. + +Column references span from row 1 to wherever the content in that column ends. + +\`\`\`javascript +// references all values in the column from row 1 to the end of the content +let x = q.cells('A') // returns all the data in the column starting from row 1 to end of data + +let x = q.cells('A:D') // returns all the data in columns A to D starting from row 1 to end of data in longest column + +let x = q.cells('A5:A') // returns all values from A5 to the end of the content in column A + +let x = q.cells('A5:C') // returns all values from A5 to end of content in C + +let x = q.cells("'Sheet2'!A:C") // same rules to reference in other sheets apply +\`\`\` + +Relative vs absolute references + +By default when you copy paste a reference it will update the row reference unless you use $ notation in your references. + +// Copy pasting this one row down will change reference to A2 +let x = q.cells('A1') + +// Copy pasting this one row down will keep reference as A1 +let x = q.cells('A$1') + +// Example using ranges - row references will not change +let x = q.cells('A$1:B$20) + +// Only A reference will change when copied down +let x = q.cells('A1:B$20') + +# Return data to the sheet + +Single values, arrays, and charts are the Javascript types that can be returned to the sheet. Any data can be structured as an array and returned to the sheet. + +Single value + +\`\`\`javascript +// from variable with assigned value +let data = 5; + +// return this value to the sheet +return data; +\`\`\` + +1-d array + +\`\`\`javascript +let data = [1, 2, 3, 4, 5]; + +return data; +\`\`\` + +2-d array + +\`\`\`javascript +let data = [[1,2,3,4,5],[1,2,3,4,5]]; + +return data; +\`\`\` + +Charts + +\`\`\`javascript +import Chart from 'https://esm.run/chart.js/auto'; + +let canvas = new OffscreenCanvas(800, 450); +let context = canvas.getContext('2d'); + +// create data +let data = [['Africa', 'Asia', 'Europe', 'Latin America', 'North America'],[2478, 5267, 734, 784, 433]] + +// print data to console +console.log(data); + +// Create chart +new Chart(canvas, { + type: 'bar', + data: { + labels: data[0], + datasets: [ + { + label: "Population (millions)", + backgroundColor: ["#3e95cd", "#8e5ea2","#3cba9f","#e8c3b9","#c45850"], + data: data[1] + } + ] + }, + options: { + legend: { display: false }, + title: { + display: true, + text: 'Predicted world population (millions) in 2050' + } + } +}); + +// return chart to the sheet +return canvas; +\`\`\` + +# API Requests + +How to make API requests in JavaScript. + +GET request + +Perform API requests using the standard Javascript approach of Fetch. javascript + +\`\`\`javascript +// API for get requests +let res = await fetch("https://jsonplaceholder.typicode.com/todos/1"); +let json = await res.json(); + +console.log(json); + +return [Object.keys(json), Object.values(json)]; +\`\`\` + +GET request with error handling + +\`\`\`javascript +async function getData() { + const url = "https://jsonplaceholder.typicode.com/todos/1"; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(\`Response status: \${response.status}\`); + } + + const json = await response.json(); + // Return the JSON object as a 2D array + return [Object.keys(json), Object.values(json)]; + } catch (error) { + console.error(error.message); + // Return the error message to the sheet + return \`Error: \${error.message}\`; + } +} + +// Call the function and return its result to the sheet +return await getData(); +\`\`\` + +POST request with body + +\`\`\`javascript +async function getData() { + // replace with your API URL and body parameters + const url = "https://example.org/products.json"; + const requestBody = { + key1: "value1", + key2: "value2" + }; + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + throw new Error(\`Response status: \${response.status}\`); + } + + const json = await response.json(); + // Return the JSON object as a 2D array + return [Object.keys(json), Object.values(json)]; + } catch (error) { + console.error(error.message); + // Return the error message to the sheet + return \`Error: \${error.message}\`; + } +} + +// Call the function and return its result to the sheet +return await getData(); +\`\`\` + +If you ever get stuck with Javascript code (especially requests) that doesn't seem to be working but is showing no error, you may be missing an \`await\` somewhere that it is needed. + +# Charts/visualizations + +Charts are supported in Javascript using charts.js. No other libraries are currently supported. + +Bar chart + +\`\`\`javascript +import Chart from 'https://esm.run/chart.js/auto'; + +let canvas = new OffscreenCanvas(800, 450); +let context = canvas.getContext('2d'); + +// create data +let data = [['Africa', 'Asia', 'Europe', 'Latin America', 'North America'],[2478, 5267, 734, 784, 433]] + +// print data to console +console.log(data) + +// Create chart +new Chart(canvas, { + type: 'bar', + data: { + labels: data[0], + datasets: [ + { + label: "Population (millions)", + backgroundColor: ["#3e95cd", "#8e5ea2","#3cba9f","#e8c3b9","#c45850"], + data: data[1] + } + ] + }, + options: { + legend: { display: false }, + title: { + display: true, + text: 'Predicted world population (millions) in 2050' + } + } +}); + +// return chart to the sheet +return canvas; +\`\`\` + +Line chart + +\`\`\`javascript +import Chart from 'https://esm.run/chart.js/auto'; + +let canvas = new OffscreenCanvas(800, 450); +let context = canvas.getContext('2d'); + +// create data +let data = [['1999', '2000', '2001', '2002', '2003'],[2478, 5267, 734, 784, 433]] + +// print data to console +console.log(data) + +// Create chart +new Chart(canvas, { + type: 'line', + data: { + labels: data[0], + datasets: [ + { + label: "Population (millions)", + backgroundColor: ["#3e95cd", "#8e5ea2","#3cba9f","#e8c3b9","#c45850"], + data: data[1] + } + ] + }, + options: { + legend: { display: false }, + title: { + display: true, + text: 'Predicted world population (millions) in 2050' + } + } +}); + +// return chart to the sheet +return canvas; +\`\`\` + +# Packages + +Packages in Javascript are supported using ESM. You can use a third-party JS CDN to load third-party packages. Some possible CDNs include: + +* We recommend using esm.run from https://www.jsdelivr.com/esm +* https://www.unpkg.com +* https://esm.sh + +Below are examples on how to correctly use esm.run to import packages in JavaScript. + +Examples + +Below are some common examples of libraries, imported using esm.run. Many more libraries are available for use in Quadratic and you can use the JS CDN of your choice. Below is how to use esm.run, which we recommend as a top option. + +Charting + +Chart.js is the only charting library in Javascript supported in Quadratic. + +\`\`\`javascript +import Chart from 'https://esm.run/chart.js/auto'; +\`\`\` + +Analytics + +D3.js is a common analytics library for JavaScript. + +\`\`\`javascript +import * as d3 from 'https://esm.run/d3'; + +let my_data = [1,2,3] +let sum = d3.sum(my_data) +return sum +\`\`\` + +Brain.js is a Machine Learning library that works in Quadratic + +\`\`\`javascript +import * as brain from 'https://esm.run/brain.js'; + +// provide optional config object (or undefined). Defaults shown. +const config = { + binaryThresh: 0.5, + hiddenLayers: [3], // array of ints for the sizes of the hidden layers in the network + activation: 'sigmoid', // supported activation types: ['sigmoid', 'relu', 'leaky-relu', 'tanh'], + leakyReluAlpha: 0.01, // supported for activation type 'leaky-relu' +}; + +// create a simple feed-forward neural network with backpropagation +const net = new brain.NeuralNetwork(config); + +await net.train([ + { input: [0, 0], output: [0] }, + { input: [0, 1], output: [1] }, + { input: [1, 0], output: [1] }, + { input: [1, 1], output: [0] }, +]); + +const output = net.run([1, 0]); // [0.987] + +return output[0] +\`\`\` +`; diff --git a/quadratic-client/src/app/ai/docs/PythonDocs.ts b/quadratic-client/src/app/ai/docs/PythonDocs.ts new file mode 100644 index 0000000000..b2f69264d0 --- /dev/null +++ b/quadratic-client/src/app/ai/docs/PythonDocs.ts @@ -0,0 +1,663 @@ +export const PythonDocs = `# Python Docs + +Quadratic is very focused on a rich developer experience. +This means focusing on features that enable you to have a streamlined development workflow inside Quadratic, with Python as a first-class citizen that integrates seamlessly with the spreadsheet. +Below is a quick start to get going with Python in Quadratic. + +In Quadratic you can reference cells in the spreadsheet to use in your code, and you can return results from your Python analysis back to the spreadsheet. By default, the last line of code is returned to the spreadsheet. +In Quadratic python does not support conditional returns. Only the last line of code is returned to the sheet. Also there can be only one type of return from a code cell, data or chart. + +Single referenced cells are put in a variable with the appropriate data type. Multi-line references are placed in a DataFrame. + +The following is a list of essential information for learning how Python works in Quadratic. + +1. Reference cells - get data from the sheet into Python with cell references +2. Return data to the sheet - return Python outputs from code to the sheet +3. Import packages - import packages for use in your Python code +4. Make API requests - use the Requests library to query APIs +5. Visualize data - turn your data into beautiful charts and graphs with Plotly + +The most common starting point is learning how to reference spreadsheet cells from Python in Quadratic. + +# Reference cells + +Reference cells from Python. + +In Quadratic, reference individual cells from Python for single values or reference a range of cells for multiple values. + +Referencing individual cells + +To reference an individual cell, use the global function \`q.cells\` which returns the cell value. + +\`\`\`python +# NOTE: uses the same A1 notation as Formulas +# Reads the value in cell A1 and places in variable x +x = q.cells('A1') + +q.cells('A3') # Returns the value of the cell at A3 +\`\`\` + +You can reference cells and use them directly in a Pythonic fashion. + +\`\`\`python +q.cells('A1') + q.cells('A2') # Adds the values in cells A1 and A2 +\`\`\` + +Any time cells dependent on other cells update the dependent cell will also update. This means your code will execute in one cell if it is dependent on another. +This is the behavior you want in almost all situations, including user inputs in the sheet that cause calculation in a Python cell. + +Referencing a range of cells + +To reference a range of cells, use the global function \`q.cells\` which returns a Pandas DataFrame. + +\`\`\`python +q.cells('A1:A5') # Returns a 1x5 DataFrame spanning from A1 to A5 + +q.cells('A1:C7') # Returns a 3x7 DataFrame spanning from A1 to C7 + +q.cells('A') # Returns all values in column A into a single-column DataFrame + +q.cells('A:C') # Returns all values in columns A to C into a three-column DataFrame + +q.cells('A5:A') # Returns all values in column A starting at A5 and going down + +q.cells('A5:C') # Returns all values in column A to C, starting at A5 and going down +\`\`\` + +If the first row of cells is a header, you should set \`first_row_header\` as an argument. This makes the first row of your DataFrame the column names, otherwise will default to integer column names as 0, 1, 2, 3, etc. + +Use first_row_header when you have column names that you want as the header of the DataFrame. This should be used commonly. You can tell when a column name should be a header when the column name describes the data below. + +\`\`\`python +# first_row_header=True will be used any time the first row is the intended header for that data. +q.cells('A1:B9', first_row_header=True) # returns a 2x9 DataFrame with first rows as DataFrame headers +\`\`\` + +As an example, this code references a table of expenses, filters it based on a user-specified column, and returns the resulting DataFrame to the spreadsheet. + +\`\`\`python +# Pull the full expenses table in as a DataFrame +expenses_table = q.cells('B2:F54', first_row_header=True) + +# Take user input at a cell (Category = "Gas") +category = q.cells('A2') + +# Filter the full expenses table to the "Gas" category, return the resulting DataFrame +expenses_table[expenses_table["Category"] == category] +\`\`\` + +Referencing another sheet + +To reference another sheet's cells or range of cells use the following: + +\`\`\`python +# Use the sheet name as an argument for referencing range of cells +q.cells("'Sheet_name_here'!A1:C9") + +# For individual cell reference +q.cells("'Sheet_name_here'!A1") +\`\`\` + +Column references + +To reference all the data in a column or set of columns without defining the range, use the following syntax. + +Column references span from row 1 to wherever the content in that column ends. + +\`\`\`python +# references all values in the column from row 1 to the end of the content +q.cells('A') # returns all the data in the column starting from row 1 to end of data + +q.cells('A:D') # returns all the data in columns A to D starting from row 1 to end of data in longest column + +q.cells('A5:A') # returns all values from A5 to the end of the content in column A + +q.cells('A5:C') # returns all values from A5 to end of content in C + +q.cells('A:C', first_row_header=True) # same rules with first_row_header apply + +q.cells("'Sheet2'!A:C", first_row_header=True) # same rules to reference in other sheets apply +\`\`\` + + +Relative vs absolute references + +By default when you copy paste a reference it will update the row reference unless you use $ notation in your references. + +\`\`\`python +# Copy pasting this one row down will change reference to A2 +q.cells('A1') + +# Copy pasting this one row down will keep reference as A1 +q.cells('A$1') + +# Example using ranges - row references will not change +q.cells('A$1:B$20) + +# Only A reference will change when copied down +q.cells('A1:B$20') +\`\`\` + +# Return data to the sheet + +Return the data from your Python code to the spreadsheet. + +Quadratic is built to seamlessly integrate Python to the spreadsheet. This means being able to manipulate data in code and very simply output that data into the sheet. + +By default, the last line of code is output to the spreadsheet. This should be one of the four basic types: + +1. Single value: for displaying the single number result of a computation +2. List of values: for displaying a list of values from a computation +3. DataFrame:for displaying the workhorse data type of Quadratic +4. Chart: for displaying Plotly charts in Quadratic +5. Function outputs: return the results of functions to the sheet + +You can expect to primarily use DataFrames as Quadratic is heavily built around Pandas DataFrames due to widespread Pandas adoption in almost all data science communities! + +1. Single Value + +Note the simplest possible example, where we set \`x = 5\` and then return \`x\` to the spreadsheet by placing \`x\` in the last line of code. + +\`\`\`python +# create variable +x = 5 + +# last line of code gets returned to the sheet, so x of value 5 gets returned +x +\`\`\` + +2. List of values + +Lists can be returned directly to the sheet. They'll be returned value by value into corresponding cells as you can see below. + +\`\`\`python +# create a list that has the numbers 1 through 5 +my_list = [1, 2, 3, 4, 5] + +# returns the list to the spreadsheet +my_list +\`\`\` + +3. DataFrame + +You can return your DataFrames directly to the sheet by putting the DataFrame's variable name as the last line of code. + +\`\`\`python +# import pandas +import pandas as pd + +# create some sample data +data = [['tom', 30], ['nick', 19], ['julie', 42]] + +# Create the DataFrame +df = pd.DataFrame(data, columns=['Name', 'Age']) + +# return DataFrame to the sheet +df +\`\`\` + +Note that if your DataFrame has an index it will not be returned to the sheet. If you want to return the index to the sheet use the following code: + +\`\`\`python +# use reset_index() method where df is the dataframe name +df.reset_index() +\`\`\` + +An example of when this is necessary is any time you use the describe() method in Pandas. +This creates an index so you'll need to use reset_index() if you want to correctly display the index in the sheet when you return the DataFrame. + +4. Charts + +Build your chart and return it to the spreadsheet by using the \`fig\` variable name or \`.show()\` + +\`\`\`python +# import plotly +import plotly.express as px + +# replace this df with your data +df = px.data.gapminder().query("country=='Canada'") + +# create your chart type, for more chart types: https://plotly.com/python/ +fig = px.line(df, x="year", y="lifeExp", title='Life expectancy in Canada') + +# display chart, alternatively can just put fig without the .show() +fig.show() +\`\`\` + +5. Function outputs + +You can not use the \`return\` keyword to return data to the sheet, as that keyword only works inside of Python functions. +Here is an example of using a Python function to return data to the sheet. + +\`\`\`python +def do_some_math(x): + return x+1 + +# returns the result of do_some_math(), which in this case is 6 +do_some_math(5) +\`\`\` + +# Packages + +Using and installing Python packages. + +Default Packages + +Many libraries are included by default, here are some examples: + +* Pandas (https://pandas.pydata.org/) +* NumPy (https://numpy.org/) +* SciPy (https://scipy.org/) + +Default packages can be imported like any other native Python package. + +\`\`\`python +import pandas as pd +import numpy as np +import scipy +\`\`\` + +Micropip can be used to install additional Python packages that aren't automatically supported (and their dependencies). + +\`\`\`python +import micropip + +# \`await\` is necessary to wait until the package is available +await micropip.install("faker") + +# Import installed package +from faker import Faker + +# Use the package! +fake = Faker() +fake.name() +\`\`\` + +This only works for packages that are either pure Python or for packages with C extensions that are built in Pyodide. +If a pure Python package is not found in the Pyodide repository, it will be loaded from PyPI. Learn more about how packages work in Pyodide. + +# Make an API request + +Get the data you want, when you want it. + +API requests are made seamless in Quadratic by allowing you to use Python and then display the result of the request directly to the sheet. + +Query API - GET request + +Let's break our GET request down into a few different pieces. + +Import the basic requests library you're familiar with, query the API, and get the data into a Pandas DataFrame. + +\`\`\`python +# Imports +import requests +import pandas as pd + +# Request +response = requests.get('your_API_url_here') + +# JSON to DataFrame +df = pd.DataFrame(response.json()) + +# Display DataFrame in the sheet +df +\`\`\` + +**Query API - POST request** + +\`\`\`python +import requests + +# API url +url = 'your_API_url_here' +# API call body +obj = {'somekey': 'somevalue'} + +# create request +x = requests.post(url, json = myobj) + +# return the API response to the sheet +x.text +\`\`\` + +**Going from CSV to DataFrame** + +Bringing your CSV to Quadratic is as simple as a drag and drop. Once your CSV is in the spreadsheet, reference the range of cells in Python to get your data into a DatarFrame. + +You use the argument \`first_row_header=True\` to set the first row of DataFrame to be your headers as column names. +Note that the output, in this case, is printed to the console since you already have your initial CSV in the sheet. +After some manipulation of the data, perhaps you would want to display your new DataFrame. In that case, leave \`df\` as the last line of code. + +In this case, the spreadsheet reflects \`q.cells('A1:B160')\` since we want the full span of data in both columns A and B spanning from rows 1 to 160. + +\`\`\`python +df = q.cells('A1:B160'), first_row_header=True) +\`\`\` + +# Clean data + +Get your data ready for analysis. + +Cleaning data in Quadratic is more seamless than you may be used to, as your data is viewable in the sheet as you step through your DataFrame. +Every change to your DataFrame can be reflected in the sheet in real-time. Some data cleaning steps you may be interested in taking (very much non-exhaustive!): + +1. View select sections of your DataFrame in the sheet +2. Drop specified columns +3. Field-specific changes +4. Clean columns +5. Delete select rows +6. Delete empty rows +7. Change data types +8. Remove duplicates + +1. View select sections of your DataFrame in the sheet + +Assume DataFrame named \`df\`. With \`df.head()\` you can display the first x rows of your spreadsheet. +With this as your last line the first x rows will display in the spreadsheet. You can do the same except with the last x rows via \`df.tail()\` + +\`\`\`python +# Display first five rows +df.head(5) + +# Display last five rows +df.tail(5) +\`\`\` + +2. Drop specified columns + +Deleting columns point and click can be done by highlighting the entire column and pressing \`Delete\`. Alternatively, do this programmatically with the code below. + +\`\`\`python +# Assuming DataFrame df, pick the columns you want to drop +columns_to_drop = ['Average viewers', 'Followers'] + +df.drop(columns_to_drop, inplace=True, axis=1) +\`\`\` + +3. Field-specific changes + +There are many ways to make field-specific changes, but this list will give you some ideas. + +\`\`\`python +# Replace row 7 in column 'Duration' with the value of 45 + df.loc[7, 'Duration'] = 45 +\`\`\` + +4. Clean columns + +Going column by column to clean specific things is best done programmatically. + +\`\`\`python +# Specify things to replace empty strings to prep drop +df['col1'].replace(things_to_replace, what_to_replace_with, inplace=True) +\`\`\` + +5. Delete select rows + +With the beauty of Quadratic, feel free to delete rows via point and click; in other cases, you may need to do this programmatically. + +\`\`\`python +# Knowing your row, you can directly drop via +df.drop(x) + +# Select a specific index, then drop that index +x = df[((df.Name == 'bob') &( df.Age == 25) & (df.Grade == 'A'))].index +df.drop(x) +\`\`\` + +6. Delete empty rows + +Identifying empty rows should be intuitive in the spreadsheet via point-and-click; in other cases, you may need to do this programmatically. + +\`\`\`python +# Replace empty strings to prep drop +df['col1'].replace('', np.nan, inplace=True) + +# Delete where specific columns are empty +df.dropna(subset=['Tenant'], inplace=True) +\`\`\` + +7. Change data types + +By default, Quadratic inputs will be read as strings by Python code. Manipulate these data types as you see fit in your DataFrame. + +\`\`\`python +# Specify column(s) to change data type +df.astype({'col1': 'int', 'col2': 'float'}).dtypes + +# Common types: float, int, datetime, string +\`\`\` + +8. Remove duplicates + +Duplicates are likely best removed programmatically, not visually. Save some time with the code below. + +\`\`\`python +# Drop duplicates across DataFrame +df.drop_duplicates() + +# Drop duplicates on specific columns +df.drop_duplicates(subset=['col1']) + +# Drop duplicates; keep the last +df.drop_duplicates(subset=['col1', 'col2'], keep='last') +\`\`\` + +# Charts/visualizations + +Glean insights from your data, visually. + +Create beautiful visualizations using our in-app Plotly support. Plotly support works just as you're used to in Python, displaying your chart straight to the spreadsheet. + +Getting started + +Building charts in Quadratic is centered around Python charting libraries, starting with Plotly. Building charts in Plotly is broken down into 3 simple steps: + +1. Create and display a chart +2. Style your chart +3. Chart controls + +1. Create and display a chart + +Line charts + +\`\`\`python +# import plotly +import plotly.express as px + +# replace this df with your data +df = px.data.gapminder().query("country=='Canada'") + +# create your chart type, for more chart types: https://plotly.com/python/ +fig = px.line(df, x="year", y="lifeExp", title='Life expectancy in Canada') + +# make chart prettier +fig.update_layout( + plot_bgcolor="White", +) + +# display chart +fig.show() +\`\`\` + +Bar charts + +\`\`\`python +import plotly.express as px + +# replace this df with your data +df = px.data.gapminder().query("country == 'Canada'") + +# create your chart type, for more chart types: https://plotly.com/python/ +fig = px.bar(df, x='year', y='pop') + +# make chart prettier +fig.update_layout( + plot_bgcolor="White", +) + +# display chart +fig.show() +\`\`\` + +Histograms + +\`\`\`python +# Import Plotly +import plotly.express as px + +# Create figure - replace df with your data +fig = px.histogram(df, x = 'output') + +# Display to sheet +fig.show() +\`\`\` + +Scatter plots + +\`\`\`python +import plotly.express as px + +# replace df, x, and y and color with your data +fig = px.scatter(df, x="col1", y="col2", color="col3") +fig.update_traces(marker_size=10) +fig.update_layout(scattermode="group") +fig.show() +\`\`\` + +Heatmaps + +\`\`\`python +# Import library +import plotly.express as px + +# Assumes 2d array Z +fig = px.imshow(Z, text_auto=True) + +# Display chart +fig.show() +\`\`\` + +2. Styling + +\`\`\`python +# Example chart styling options to get started +fig.update_layout( + xaxis=dict( + showline=True, + showgrid=False, + showticklabels=True, + linecolor='rgb(204, 204, 204)', + linewidth=2, + ticks='outside', + tickfont=dict( + family='Arial', + size=12, + color='rgb(82, 82, 82)', + ), + ), + yaxis=dict( + showgrid=False, + zeroline=False, + showline=False, + showticklabels=True, + ), + autosize=False, + showlegend=False, + plot_bgcolor='white', + title='Historical power usage by month (1985-2018)' +) +\`\`\` + +3. Chart controls + +Resize by dragging the edges of the chart. + +# Manipulate data + +Perform novel analysis on your data. + +Manipulating data in Quadratic is easier than ever as you can view your changes in the sheet in real-time. Here is a non-exhaustive list of ways to manipulate your data in Quadratic: + +1. Find correlations +2. Basic stats - max, min, average, selections, etc. +3. DataFrame math +4. Data selections + +1. Find correlations + +\`\`\`python +# Get the correlation and show the value in the sheet +data['col1'].corr(data['col2'], method='pearson') + +# possible methods: pearson, kendall, spearman +\`\`\` + +2. Basic stats - max, min, mean, selections, etc. + +\`\`\`python +# Get the max value of a column +df["col1"].max() +\`\`\` + +\`\`\`python +# Get the min value of a column +df["col1"].min() +\`\`\` + +\`\`\`python +# Get the mean value of a column +df["col1"].mean() +\`\`\` + +\`\`\`python +# Get the median value of a column +df["col1"].median() +\`\`\` + +\`\`\`python +# Get the skew for all columns +df.skew() +\`\`\` + +\`\`\`python +# Count the values in a column +df["col1"].value_counts() +\`\`\` + +\`\`\`python +# Get the summary of a column +df["col1"].describe() +\`\`\` + +3. DataFrame math + +Do math on data in the DataFrame. Alternatively, use formulas in the sheet on the values. + +\`\`\`python +# Add, subtract, multiply, divide, etc., will all work on all values in a column +df['col1'] + 1 +df['col1'] - 1 +df['col1'] * 2 +df['col1'] / 2 +\`\`\` + +\`\`\`python +# Do any arbitrary math column-wise with the above or do DataFrame-wise via +df + 1 +\`\`\` + +4. Data selections + +Alternatively, cut/copy/paste specific values in the sheet. + +\`\`\`python +# get a column +df['col1'] +\`\`\` + +\`\`\`python +# get multiple columns +df[['col1', 'col2']] +\`\`\` +`; diff --git a/quadratic-client/src/app/ai/docs/QuadraticDocs.ts b/quadratic-client/src/app/ai/docs/QuadraticDocs.ts new file mode 100644 index 0000000000..536c062913 --- /dev/null +++ b/quadratic-client/src/app/ai/docs/QuadraticDocs.ts @@ -0,0 +1,8 @@ +export const QuadraticDocs = `# Quadratic Docs + +Quadratic is a modern AI-enabled spreadsheet. Quadratic is purpose built to make working with data easier and faster than ever, for users of any technical level. + +Quadratic combines a familiar spreadsheet and formulas with the power of AI and modern coding languages like Python, SQL, and JavaScript. + +Ingest data from any source (csv, excel, parquet or sql) or add data directly to the spreadsheet, analyze it with Python, Javascript and Formulas, and speed that whole process up with the power of AI. All in the most familiar interface for working with data - spreadsheets. +`; diff --git a/quadratic-client/src/app/ai/hooks/useAIModel.tsx b/quadratic-client/src/app/ai/hooks/useAIModel.tsx new file mode 100644 index 0000000000..19e2c1398e --- /dev/null +++ b/quadratic-client/src/app/ai/hooks/useAIModel.tsx @@ -0,0 +1,23 @@ +import useLocalStorage, { SetValue } from '@/shared/hooks/useLocalStorage'; +import { DEFAULT_MODEL, DEFAULT_MODEL_VERSION, MODEL_OPTIONS } from 'quadratic-shared/AI_MODELS'; +import { AIModel } from 'quadratic-shared/typesAndSchemasAI'; + +export function useAIModel(): [AIModel, SetValue] { + const [model, setModel] = useLocalStorage('aiModel', DEFAULT_MODEL); + const [version, setVersion] = useLocalStorage('aiModelVersion', 0); + + // This is to update model stored in local storage to the current default model + if (version !== DEFAULT_MODEL_VERSION) { + setModel(DEFAULT_MODEL); + setVersion(DEFAULT_MODEL_VERSION); + return [DEFAULT_MODEL, setModel]; + } + + // If the model is removed from the MODELS object or is not enabled, set the model to the current default model + if (!MODEL_OPTIONS[model] || !MODEL_OPTIONS[model].enabled) { + setModel(DEFAULT_MODEL); + return [DEFAULT_MODEL, setModel]; + } + + return [model, setModel]; +} diff --git a/quadratic-client/src/app/ai/hooks/useAIRequestToAPI.tsx b/quadratic-client/src/app/ai/hooks/useAIRequestToAPI.tsx new file mode 100644 index 0000000000..0287ea26d7 --- /dev/null +++ b/quadratic-client/src/app/ai/hooks/useAIRequestToAPI.tsx @@ -0,0 +1,528 @@ +import { AITool } from '@/app/ai/tools/aiTools'; +import { getAIProviderEndpoint } from '@/app/ai/tools/endpoint.helper'; +import { isAnthropicBedrockModel, isAnthropicModel, isBedrockModel, isOpenAIModel } from '@/app/ai/tools/model.helper'; +import { getToolChoice, getTools } from '@/app/ai/tools/tool.helpers'; +import { authClient } from '@/auth/auth'; +import { MODEL_OPTIONS } from 'quadratic-shared/AI_MODELS'; +import { AIMessagePrompt, AIModel, AIPromptMessage, ChatMessage } from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback } from 'react'; +import { SetterOrUpdater } from 'recoil'; + +type HandleAIPromptProps = { + model: AIModel; + system?: string | { text: string }[]; + messages: AIPromptMessage[]; + setMessages?: SetterOrUpdater | ((value: React.SetStateAction) => void); + signal: AbortSignal; + useStream?: boolean; + useTools?: boolean; + toolChoice?: AITool; +}; + +export function useAIRequestToAPI() { + const parseBedrockStream = useCallback( + async ( + reader: ReadableStreamDefaultReader, + responseMessage: AIMessagePrompt, + setMessages?: (value: React.SetStateAction) => void + ): Promise<{ error?: boolean; content: AIMessagePrompt['content']; toolCalls: AIMessagePrompt['toolCalls'] }> => { + const decoder = new TextDecoder(); + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + // tool use start + if ( + data && + data.contentBlockStart && + data.contentBlockStart.start && + data.contentBlockStart.start.toolUse + ) { + const toolCalls = [...responseMessage.toolCalls]; + const toolCall = { + id: data.contentBlockStart.start.toolUse.toolUseId, + name: data.contentBlockStart.start.toolUse.name, + arguments: '', + loading: true, + }; + toolCalls.push(toolCall); + responseMessage.toolCalls = toolCalls; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } + // tool use stop + else if (data && data.contentBlockStop) { + const toolCalls = [...responseMessage.toolCalls]; + let toolCall = toolCalls.pop(); + if (toolCall) { + toolCall = { ...toolCall, loading: false }; + toolCalls.push(toolCall); + responseMessage.toolCalls = toolCalls; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } + } else if (data && data.contentBlockDelta && data.contentBlockDelta.delta) { + // text delta + if ('text' in data.contentBlockDelta.delta) { + responseMessage.content += data.contentBlockDelta.delta.text; + } + // tool use delta + else if ('toolUse' in data.contentBlockDelta.delta) { + const toolCalls = [...responseMessage.toolCalls]; + const toolCall = { + ...(toolCalls.pop() ?? { + id: '', + name: '', + arguments: '', + loading: true, + }), + }; + toolCall.arguments += data.contentBlockDelta.delta.toolUse.input; + toolCalls.push(toolCall); + responseMessage.toolCalls = toolCalls; + } + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } else if (data && data.messageStart) { + // message start + } else if (data && data.messageStop) { + // message stop + } + // error + else if ( + data && + (data.internalServerException || + data.modelStreamErrorException || + data.validationException || + data.throttlingException || + data.serviceUnavailableException) + ) { + responseMessage.content += '\n\nAn error occurred while processing the response.'; + responseMessage.toolCalls = []; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + console.error('Error in AI prompt handling:', data.error); + return { error: true, content: 'An error occurred while processing the response.', toolCalls: [] }; + } + } catch (error) { + console.error('Error in AI prompt handling:', error); + // Not JSON or unexpected format, skip + } + } + } + } + + if (!responseMessage.content) { + responseMessage.content = + responseMessage.toolCalls.length > 0 ? '' : "I'm sorry, I don't have a response for that."; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } + + return { content: responseMessage.content, toolCalls: responseMessage.toolCalls }; + }, + [] + ); + + const parseAnthropicStream = useCallback( + async ( + reader: ReadableStreamDefaultReader, + responseMessage: AIMessagePrompt, + setMessages?: (value: React.SetStateAction) => void + ): Promise<{ error?: boolean; content: AIMessagePrompt['content']; toolCalls: AIMessagePrompt['toolCalls'] }> => { + const decoder = new TextDecoder(); + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + if (data.type === 'content_block_start') { + if (data.content_block.type === 'text') { + responseMessage.content += data.content_block.text; + } else if (data.content_block.type === 'tool_use') { + const toolCalls = [...responseMessage.toolCalls]; + const toolCall = { + id: data.content_block.id, + name: data.content_block.name, + arguments: '', + loading: true, + }; + toolCalls.push(toolCall); + responseMessage.toolCalls = toolCalls; + } + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } else if (data.type === 'content_block_delta') { + if (data.delta.type === 'text_delta') { + responseMessage.content += data.delta.text; + } else if (data.delta.type === 'input_json_delta') { + const toolCalls = [...responseMessage.toolCalls]; + const toolCall = { + ...(toolCalls.pop() ?? { + id: '', + name: '', + arguments: '', + loading: true, + }), + }; + toolCall.arguments += data.delta.partial_json; + toolCalls.push(toolCall); + responseMessage.toolCalls = toolCalls; + } + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } else if (data.type === 'content_block_stop') { + const toolCalls = [...responseMessage.toolCalls]; + let toolCall = toolCalls.pop(); + if (toolCall) { + toolCall = { ...toolCall, loading: false }; + toolCalls.push(toolCall); + responseMessage.toolCalls = toolCalls; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } + } else if (data.type === 'message_start') { + // message start + } else if (data.type === 'message_stop') { + // message stop + } else if (data.type === 'error') { + responseMessage.content += '\n\nAn error occurred while processing the response.'; + responseMessage.toolCalls = []; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + console.error('Error in AI prompt handling:', data.error); + return { error: true, content: 'An error occurred while processing the response.', toolCalls: [] }; + } + } catch (error) { + console.error('Error in AI prompt handling:', error); + // Not JSON or unexpected format, skip + } + } + } + } + + if (!responseMessage.content) { + responseMessage.content = + responseMessage.toolCalls.length > 0 ? '' : "I'm sorry, I don't have a response for that."; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } + + return { content: responseMessage.content, toolCalls: responseMessage.toolCalls }; + }, + [] + ); + + const parseOpenAIStream = useCallback( + async ( + reader: ReadableStreamDefaultReader, + responseMessage: AIMessagePrompt, + setMessages?: (value: React.SetStateAction) => void + ): Promise<{ error?: boolean; content: AIMessagePrompt['content']; toolCalls: AIMessagePrompt['toolCalls'] }> => { + const decoder = new TextDecoder(); + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + if (data.choices && data.choices[0] && data.choices[0].delta) { + // text delta + if (data.choices[0].delta.content) { + responseMessage.content += data.choices[0].delta.content; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } + // tool use delta + else if (data.choices[0].delta.tool_calls) { + data.choices[0].delta.tool_calls.forEach( + (tool_call: { id: string; function: { name?: string; arguments: string } }) => { + const toolCalls = [...responseMessage.toolCalls]; + let toolCall = toolCalls.pop(); + if (toolCall) { + toolCall = { + ...toolCall, + loading: true, + }; + toolCalls.push(toolCall); + } + if (tool_call.function.name) { + toolCall = { + id: tool_call.id, + name: tool_call.function.name, + arguments: tool_call.function.arguments, + loading: true, + }; + toolCalls.push(toolCall); + } else { + const toolCall = { + ...(toolCalls.pop() ?? { + id: '', + name: '', + arguments: '', + loading: true, + }), + }; + toolCall.arguments += tool_call?.function?.arguments ?? ''; + toolCalls.push(toolCall); + } + responseMessage.toolCalls = toolCalls; + } + ); + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } + // tool use stop + else if (data.choices[0].finish_reason === 'tool_calls') { + responseMessage.toolCalls = responseMessage.toolCalls.map((toolCall) => ({ + ...toolCall, + loading: false, + })); + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } else if (data.choices[0].delta.refusal) { + console.warn('Invalid AI response: ', data.choices[0].delta.refusal); + } + } else if (data.error) { + responseMessage.content += '\n\nAn error occurred while processing the response.'; + responseMessage.toolCalls = []; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + console.error('Error in AI prompt handling:', data.error); + return { error: true, content: 'An error occurred while processing the response.', toolCalls: [] }; + } + } catch (error) { + console.error('Error in AI prompt handling:', error); + // Not JSON or unexpected format, skip + } + } + } + } + + if (!responseMessage.content) { + responseMessage.content = + responseMessage.toolCalls.length > 0 ? '' : "I'm sorry, I don't have a response for that."; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } + + return { content: responseMessage.content, toolCalls: responseMessage.toolCalls }; + }, + [] + ); + + const handleAIRequestToAPI = useCallback( + async ({ + model, + system, + messages, + setMessages, + signal, + useStream, + useTools, + toolChoice, + }: HandleAIPromptProps): Promise<{ + error?: boolean; + content: AIMessagePrompt['content']; + toolCalls: AIMessagePrompt['toolCalls']; + }> => { + const responseMessage: AIMessagePrompt = { + role: 'assistant', + content: '', + contextType: 'userPrompt', + toolCalls: [], + }; + setMessages?.((prev) => [...prev, { ...responseMessage, content: '' }]); + + try { + const token = await authClient.getTokenOrRedirect(); + const { canStream, canStreamWithToolCalls } = MODEL_OPTIONS[model]; + const stream = canStream + ? useTools + ? canStreamWithToolCalls && (useStream ?? canStream) + : useStream ?? canStream + : false; + const tools = !useTools ? undefined : getTools(model, toolChoice); + const tool_choice = !useTools ? undefined : getToolChoice(model, toolChoice); + const endpoint = getAIProviderEndpoint(model, stream); + const response = await fetch(endpoint, { + method: 'POST', + signal, + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ model, system, messages, tools, tool_choice }), + }); + + if (!response.ok) { + const data = await response.json(); + const error = + response.status === 429 + ? 'You have exceeded the maximum number of requests. Please try again later.' + : `Looks like there was a problem. Error: ${data}`; + setMessages?.((prev) => [ + ...prev.slice(0, -1), + { role: 'assistant', content: error, contextType: 'userPrompt', model, toolCalls: [] }, + ]); + console.error(`Error retrieving data from AI API. Error: ${data}`); + return { error: true, content: error, toolCalls: [] }; + } + + const isBedrock = isBedrockModel(model); + const isBedrockAnthropic = isAnthropicBedrockModel(model); + const isAnthropic = isAnthropicModel(model); + const isOpenAI = isOpenAIModel(model); + + // handle streaming response + if (stream) { + const reader = response.body?.getReader(); + if (!reader) throw new Error('Response body is not readable'); + + // handle streaming Bedrock response + if (isBedrock) { + return parseBedrockStream(reader, responseMessage, setMessages); + } + + // handle streaming Anthropic response + else if (isAnthropic || isBedrockAnthropic) { + return parseAnthropicStream(reader, responseMessage, setMessages); + } + + // handle streaming OpenAI response + else if (isOpenAI) { + return parseOpenAIStream(reader, responseMessage, setMessages); + } + + // should never happen + else { + throw new Error(`Unknown model: ${model}`); + } + } + + // handle non-streaming response + else { + const data = await response.json(); + // handle non-streaming Bedrock response + if (isBedrock) { + if ( + !data || + !data.message || + data.message.role !== 'assistant' || + !data.message.content || + !data.message.content.length + ) { + throw new Error('No data returned from AI API'); + } + data.message.content.forEach( + (contentBlock: { text: string } | { toolUse: { toolUseId: string; name: string; input: unknown } }) => { + if ('text' in contentBlock) { + responseMessage.content += contentBlock.text; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } else if ('toolUse' in contentBlock) { + responseMessage.toolCalls = [ + ...responseMessage.toolCalls, + { + id: contentBlock.toolUse.toolUseId, + name: contentBlock.toolUse.name, + arguments: JSON.stringify(contentBlock.toolUse.input), + loading: false, + }, + ]; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } else { + console.error(`Invalid AI response: ${JSON.stringify(contentBlock)}`); + } + } + ); + } + // handle non-streaming Anthropic response + else if (isAnthropic || isBedrockAnthropic) { + data?.forEach( + ( + message: { type: 'text'; text: string } | { type: 'tool_use'; id: string; name: string; input: unknown } + ) => { + switch (message.type) { + case 'text': + responseMessage.content += message.text; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + break; + case 'tool_use': + responseMessage.toolCalls = [ + ...responseMessage.toolCalls, + { + id: message.id, + name: message.name, + arguments: JSON.stringify(message.input), + loading: false, + }, + ]; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + break; + default: + console.error(`Invalid AI response: ${JSON.stringify(message)}`); + } + } + ); + } + // handle non-streaming OpenAI response + else if (isOpenAI) { + if (data) { + if (data.content) { + responseMessage.content += data.content; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } else if (data.tool_calls) { + data.tool_calls.forEach( + (toolCall: { type: string; id: string; function: { name: string; arguments: string } }) => { + switch (toolCall.type) { + case 'function': + responseMessage.toolCalls = [ + ...responseMessage.toolCalls, + { + id: toolCall.id, + name: toolCall.function.name, + arguments: toolCall.function.arguments, + loading: false, + }, + ]; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + break; + default: + throw new Error(`Invalid AI response: ${data}`); + } + } + ); + } else if (data.refusal) { + throw new Error(`Invalid AI response: ${data}`); + } + } + } + // should never happen + else { + throw new Error(`Unknown model: ${model}`); + } + + if (!responseMessage.content) { + responseMessage.content = + responseMessage.toolCalls.length > 0 ? '' : "I'm sorry, I don't have a response for that."; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + } + + return { content: responseMessage.content, toolCalls: responseMessage.toolCalls }; + } + } catch (err: any) { + if (err.name === 'AbortError') { + return { error: false, content: 'Aborted by user', toolCalls: [] }; + } else { + responseMessage.content += '\n\nAn error occurred while processing the response.'; + setMessages?.((prev) => [...prev.slice(0, -1), { ...responseMessage }]); + console.error('Error in AI prompt handling:', err); + return { error: true, content: 'An error occurred while processing the response.', toolCalls: [] }; + } + } + }, + [parseBedrockStream, parseAnthropicStream, parseOpenAIStream] + ); + + return { handleAIRequestToAPI }; +} diff --git a/quadratic-client/src/app/ai/hooks/useCodeCellContextMessages.tsx b/quadratic-client/src/app/ai/hooks/useCodeCellContextMessages.tsx new file mode 100644 index 0000000000..da8954fbcc --- /dev/null +++ b/quadratic-client/src/app/ai/hooks/useCodeCellContextMessages.tsx @@ -0,0 +1,98 @@ +import { CodeCell } from '@/app/gridGL/types/codeCell'; +import { getConnectionInfo, getConnectionKind } from '@/app/helpers/codeCellLanguage'; +import { xyToA1 } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { connectionClient } from '@/shared/api/connectionClient'; +import { ChatMessage } from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback } from 'react'; + +export function useCodeCellContextMessages() { + const getCodeCellContext = useCallback( + async ({ codeCell }: { codeCell: CodeCell | undefined }): Promise => { + if (!codeCell) return []; + const { sheetId, pos, language: cellLanguage } = codeCell; + const codeCellCore = await quadraticCore.getCodeCell(sheetId, pos.x, pos.y); + const codeString = codeCellCore?.code_string ?? ''; + const consoleOutput = { + std_out: codeCellCore?.std_out ?? '', + std_err: codeCellCore?.std_err ?? '', + }; + + let schemaData; + const connection = getConnectionInfo(cellLanguage); + if (connection) { + schemaData = await connectionClient.schemas.get( + connection.kind.toLowerCase() as 'postgres' | 'mysql' | 'mssql', + connection.id + ); + } + const schemaJsonForAi = schemaData ? JSON.stringify(schemaData) : undefined; + + const a1Pos = xyToA1(pos.x, pos.y); + const language = getConnectionKind(cellLanguage); + const consoleHasOutput = consoleOutput.std_out !== '' || consoleOutput.std_err !== ''; + + return [ + { + role: 'user', + content: `Note: This is an internal message for context. Do not quote it in your response.\n\n +Currently, you are in a code cell that is being edited.\n +The code cell type is ${language}. The code cell is located at ${a1Pos}.\n +${ + schemaJsonForAi + ? `The schema for the database is: +\`\`\`json +${schemaJsonForAi} +\`\`\` +${ + language === 'POSTGRES' + ? 'When generating postgres queries, put schema and table names in quotes, e.g. "schema"."TableName".' + : '' +} +${ + language === 'MYSQL' + ? 'When generating mysql queries, put schema and table names in backticks, e.g. `schema`.`TableName`.' + : '' +} +${ + language === 'MSSQL' + ? 'When generating mssql queries, put schema and table names in square brackets, e.g. [schema].[TableName].' + : '' +} +${ + language === 'SNOWFLAKE' + ? 'When generating Snowflake queries, put schema and table names in double quotes, e.g. "SCHEMA"."TABLE_NAME".' + : '' +}\n` + : `Add imports to the top of the code cell and do not use any libraries or functions that are not listed in the Quadratic documentation.\n +Use any functions that are part of the ${language} library.\n +A code cell can return only one type of value as specified in the Quadratic documentation.\n +A code cell cannot display both a chart and return a data frame at the same time.\n +A code cell cannot display multiple charts at the same time.\n +Do not use conditional returns in code cells.\n +Do not use any markdown syntax besides triple backticks for ${language} code blocks.\n +Do not reply code blocks in plain text, use markdown with triple backticks and language name ${language}.` +} +The code in the code cell is:\n +\`\`\`${language}\n${codeString}\n\`\`\` + +${ + consoleHasOutput + ? `Code was run recently and the console output is:\n +\`\`\`json\n${JSON.stringify(consoleOutput)}\n\`\`\`` + : `` +}`, + contextType: 'codeCell', + }, + { + role: 'assistant', + content: `How can I help you?`, + contextType: 'codeCell', + }, + ]; + }, + [] + ); + + return { getCodeCellContext }; +} diff --git a/quadratic-client/src/app/ai/hooks/useCurrentSheetContextMessages.tsx b/quadratic-client/src/app/ai/hooks/useCurrentSheetContextMessages.tsx new file mode 100644 index 0000000000..aa118f5fd0 --- /dev/null +++ b/quadratic-client/src/app/ai/hooks/useCurrentSheetContextMessages.tsx @@ -0,0 +1,77 @@ +import { sheets } from '@/app/grid/controller/Sheets'; +import { getAllSelection } from '@/app/grid/sheet/selection'; +import { rectToA1 } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { maxRects } from '@/app/ui/menus/AIAnalyst/const/maxRects'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { ChatMessage } from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback } from 'react'; + +export function useCurrentSheetContextMessages() { + const getCurrentSheetContext = useCallback( + async ({ currentSheetName }: { currentSheetName: string }): Promise => { + const sheet = sheets.getSheetByName(currentSheetName); + if (!sheet) return []; + const sheetBounds = sheet.boundsWithoutFormatting; + const selection: string | undefined = sheetBounds.type === 'empty' ? undefined : getAllSelection(sheet.id); + const currentSheetContext = selection + ? await quadraticCore.getAIContextRectsInSelections([selection], maxRects) + : undefined; + + return [ + { + role: 'user', + content: `Note: This is an internal message for context. Do not quote it in your response.\n\n +I have an open sheet, with sheet name '${currentSheetName}', with the following data: +${ + sheetBounds.type === 'nonEmpty' + ? `- Data range: ${rectToA1(sheetBounds)} +- Note: This range may contain empty cells.` + : '- The sheet is currently empty.' +}\n\n + +${ + currentSheetContext && currentSheetContext.length === 1 + ? ` +Data in the currently open sheet:\n + +I am sharing current sheet data as an array of tabular data rectangles, each tabular data rectangle in this array has following properties:\n +- sheet_name: This is the name of the sheet.\n +- rect_origin: This is the position of the top left cell of the data rectangle in A1 notation. Columns are represented by letters and rows are represented by numbers.\n +- rect_width: This is the width of the rectangle in number of columns.\n +- rect_height: This is the height of the rectangle in number of rows.\n +- starting_rect_values: This is a 2D array of cell values (json object format described below). This 2D array contains the starting 3 rows of data in the rectangle. This includes headers, if present, and data.\n + +Each cell value is a JSON object having the following properties:\n +- value: The value of the cell. This is a string representation of the value in the cell.\n +- kind: The kind of the value. This can be blank, text, number, logical, time instant, duration, error, html, code, image, date, time, date time, null or undefined.\n +- pos: This is the position of the cell in A1 notation. Columns are represented by letters and rows are represented by numbers.\n\n + +This is being shared so that you can understand the table format, size and value types inside the data rectangle.\n + +Data from cells can be referenced by Formulas, Python, Javascript or SQL code. In Python and Javascript use the cell reference function \`q.cells\`, i.e. \`q.cells(a1_notation_selection_string)\`, to reference data cells. Always use sheet name in a1 notation to reference cells. Sheet name is always enclosed in single quotes. In Python and Javascript, the complete a1 notation selection string is enclosed in double quotes. Example: \`q.cells("'Sheet 1'!A1:B2")\`. In formula, string quotes are not to be used. Example: \`=SUM('Sheet 1'!A1:B2)\`.\n +To reference data from different tabular data rectangles, use multiple \`q.cells\` functions.\n +Use this sheet data in the context of following messages. Refer to cells if required in code.\n\n + +Current sheet data is:\n +\`\`\`json +${JSON.stringify(currentSheetContext[0])} +\`\`\` +Note: All this data is only for your reference to data on the sheet. This data cannot be used directly in code. Use the cell reference function \`q.cells\`, i.e. \`q.cells(a1_notation_selection_string)\`, to reference data cells in code. Always use sheet name in a1 notation to reference cells. Sheet name is always enclosed in single quotes. In Python and Javascript, the complete a1 notation selection string is enclosed in double quotes. Example: \`q.cells("'Sheet 1'!A1:B2")\`. In formula, string quotes are not to be used. Example: \`=SUM('Sheet 1'!A1:B2)\`\n\n +` + : `This currently open sheet is empty.\n` +}\n +`, + contextType: 'currentSheet', + }, + { + role: 'assistant', + content: `I understand the current sheet data, I will reference it to answer following messages. How can I help you?`, + contextType: 'currentSheet', + }, + ]; + }, + [] + ); + + return { getCurrentSheetContext }; +} diff --git a/quadratic-client/src/app/ai/hooks/useGetChatName.tsx b/quadratic-client/src/app/ai/hooks/useGetChatName.tsx new file mode 100644 index 0000000000..1f7bac956a --- /dev/null +++ b/quadratic-client/src/app/ai/hooks/useGetChatName.tsx @@ -0,0 +1,60 @@ +import { useAIModel } from '@/app/ai/hooks/useAIModel'; +import { useAIRequestToAPI } from '@/app/ai/hooks/useAIRequestToAPI'; +import { AITool } from '@/app/ai/tools/aiTools'; +import { aiToolsSpec } from '@/app/ai/tools/aiToolsSpec'; +import { getMessagesForModel, getPromptMessages } from '@/app/ai/tools/message.helper'; +import { aiAnalystCurrentChatMessagesAtom } from '@/app/atoms/aiAnalystAtom'; +import { useRecoilCallback } from 'recoil'; + +export const useGetChatName = () => { + const { handleAIRequestToAPI } = useAIRequestToAPI(); + const [model] = useAIModel(); + + const getChatName = useRecoilCallback( + ({ snapshot }) => + async () => { + const chatMessages = await snapshot.getPromise(aiAnalystCurrentChatMessagesAtom); + const chatPromptMessages = getPromptMessages(chatMessages); + const { system, messages: prevMessages } = getMessagesForModel(model, chatPromptMessages); + const { messages } = getMessagesForModel(model, [ + { + role: 'user', + content: `Use set_chat_name tool to set the name for this chat based on the following chat messages between AI assistant and the user.\n + Previous messages:\n + \`\`\`json + ${JSON.stringify(prevMessages)} + \`\`\` + `, + contextType: 'userPrompt', + }, + ]); + + const abortController = new AbortController(); + const response = await handleAIRequestToAPI({ + model, + system, + messages, + signal: abortController.signal, + useStream: false, + useTools: true, + toolChoice: AITool.SetChatName, + }); + + const setChatNameToolCall = response.toolCalls.find((toolCall) => toolCall.name === AITool.SetChatName); + if (setChatNameToolCall) { + try { + const argsObject = JSON.parse(setChatNameToolCall.arguments); + const args = aiToolsSpec[AITool.SetChatName].responseSchema.parse(argsObject); + return args.chat_name; + } catch (error) { + console.error('[useGetChatName] toolCall: ', error); + } + } + + return ''; + }, + [handleAIRequestToAPI, model] + ); + + return { getChatName }; +}; diff --git a/quadratic-client/src/app/ai/hooks/useOtherSheetsContextMessages.tsx b/quadratic-client/src/app/ai/hooks/useOtherSheetsContextMessages.tsx new file mode 100644 index 0000000000..c872d61e49 --- /dev/null +++ b/quadratic-client/src/app/ai/hooks/useOtherSheetsContextMessages.tsx @@ -0,0 +1,83 @@ +import { sheets } from '@/app/grid/controller/Sheets'; +import { getAllSelection } from '@/app/grid/sheet/selection'; +import { maxRects } from '@/app/ui/menus/AIAnalyst/const/maxRects'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { ChatMessage } from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback } from 'react'; + +export function useOtherSheetsContextMessages() { + const getOtherSheetsContext = useCallback( + async ({ sheetNames }: { sheetNames: string[] }): Promise => { + if (sheetNames.length === 0) return []; + + const selections = sheetNames.reduce((acc, sheetName) => { + const sheet = sheets.getSheetByName(sheetName); + if (!sheet) return acc; + + const sheetBounds = sheet.boundsWithoutFormatting; + if (sheetBounds.type === 'empty') return acc; + + const selection = getAllSelection(sheet.id); + + return [...acc, selection]; + }, []); + if (selections.length === 0) return []; + + const sheetsRectContext = await quadraticCore.getAIContextRectsInSelections(selections, maxRects); + if (!sheetsRectContext || sheetsRectContext.length === 0 || sheetsRectContext[0].length === 0) return []; + + return [ + { + role: 'user', + content: `Note: This is an internal message for context. Do not quote it in your response.\n\n +I have following other sheets in the same file having the following data: + +Data in the currently open file in other sheets:\n + +I am sharing other sheets data as an array of tabular data rectangles, each tabular data rectangle in this array has following properties:\n +- sheet_name: This is the name of the sheet.\n +- rect_origin: This is the position of the top left cell of the data rectangle in A1 notation. Columns are represented by letters and rows are represented by numbers.\n +- rect_width: This is the width of the rectangle in number of columns.\n +- rect_height: This is the height of the rectangle in number of rows.\n +- starting_rect_values: This is a 2D array of cell values (json object format described below). This 2D array contains the starting 3 rows of data in the rectangle. This includes headers, if present, and data.\n + +Each cell value is a JSON object having the following properties:\n +- value: The value of the cell. This is a string representation of the value in the cell.\n +- kind: The kind of the value. This can be blank, text, number, logical, time instant, duration, error, html, code, image, date, time, date time, null or undefined.\n +- pos: This is the position of the cell in A1 notation. Columns are represented by letters and rows are represented by numbers.\n\n + +This is being shared so that you can understand the table format, size and value types inside the data rectangle.\n + +Data from cells can be referenced by Formulas, Python, Javascript or SQL code. In Python and Javascript use the cell reference function \`q.cells\`, i.e. \`q.cells(a1_notation_selection_string)\`, to reference data cells. Always use sheet name in a1 notation to reference cells. Sheet name is always enclosed in single quotes. In Python and Javascript, the complete a1 notation selection string is enclosed in double quotes. Example: \`q.cells("'Sheet 1'!A1:B2")\`. In formula, string quotes are not to be used. Example: \`=SUM('Sheet 1'!A1:B2)\`\n +Sheet name is optional, if not provided, it is assumed to be the currently open sheet.\n +Sheet name is case sensitive, and is required to be enclosed in single quotes.\n +To reference data from different tabular data rectangles, use multiple \`q.cells\` functions.\n +Use this sheet data in the context of following messages. Refer to cells if required in code.\n\n + +${sheetsRectContext.map((sheetRectContext) => { + if (sheetRectContext.length === 0) return ''; + return ` +Data in sheet '${sheetRectContext[0].sheet_name}': + +\`\`\`json +${JSON.stringify(sheetRectContext)} +\`\`\` +`; +})} + +Note: All this data is only for your reference to data on the sheet. This data cannot be used directly in code. Use the cell reference function \`q.cells\`, i.e. \`q.cells(a1_notation_selection_string)\`, to reference data cells in code. Always use sheet name in a1 notation to reference cells. Sheet name is always enclosed in single quotes. In Python and Javascript, the complete a1 notation selection string is enclosed in double quotes. Example: \`q.cells("'Sheet 1'!A1:B2")\`. In formula, string quotes are not to be used. Example: \`=SUM('Sheet 1'!A1:B2)\`\n\n +`, + contextType: 'otherSheets', + }, + { + role: 'assistant', + content: `I understand the other sheets data, I will reference it to answer following messages. How can I help you?`, + contextType: 'otherSheets', + }, + ]; + }, + [] + ); + + return { getOtherSheetsContext }; +} diff --git a/quadratic-client/src/app/ai/hooks/useQuadraticContextMessages.tsx b/quadratic-client/src/app/ai/hooks/useQuadraticContextMessages.tsx new file mode 100644 index 0000000000..311e01a8f6 --- /dev/null +++ b/quadratic-client/src/app/ai/hooks/useQuadraticContextMessages.tsx @@ -0,0 +1,45 @@ +import { ConnectionDocs } from '@/app/ai/docs/ConnectionDocs'; +import { FormulaDocs } from '@/app/ai/docs/FormulaDocs'; +import { JavascriptDocs } from '@/app/ai/docs/JavascriptDocs'; +import { PythonDocs } from '@/app/ai/docs/PythonDocs'; +import { QuadraticDocs } from '@/app/ai/docs/QuadraticDocs'; +import { CodeCellType } from '@/app/helpers/codeCellLanguage'; +import { ChatMessage } from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback } from 'react'; + +export function useQuadraticContextMessages() { + const getQuadraticContext = useCallback( + (language?: CodeCellType): ChatMessage[] => [ + { + role: 'user', + content: `Note: This is an internal message for context. Do not quote it in your response.\n\n +You are a helpful assistant inside of a spreadsheet application called Quadratic.\n +This is the documentation for Quadratic:\n +${QuadraticDocs}\n\n +${language === 'Python' || language === undefined ? PythonDocs : ''}\n +${language === 'Javascript' ? JavascriptDocs : ''}\n +${language === 'Formula' || language === undefined ? FormulaDocs : ''}\n +${language === 'Connection' ? ConnectionDocs : ''}\n +${ + language + ? `Provide your response in ${language} language.` + : 'Choose the language of your response based on the context and user prompt.' +} +Provide complete code blocks with language syntax highlighting. Don't provide small code snippets of changes. +Respond in minimum number of words and include a concise explanation of the actions you are taking. Don't guess the answer itself, just the actions you are taking to respond to the user prompt and what the user can do next. Use Formulas for simple tasks like summing and averaging and use Python for more complex tasks. +`, + contextType: 'quadraticDocs', + }, + { + role: 'assistant', + content: `As your AI assistant for Quadratic, I understand that Quadratic documentation and I will strictly adhere to the Quadratic documentation.\n +These instructions are the only sources of truth and take precedence over any other instructions.\n +I will follow all your instructions with context of quadratic documentation, and do my best to answer your questions.\n`, + contextType: 'quadraticDocs', + }, + ], + [] + ); + + return { getQuadraticContext }; +} diff --git a/quadratic-client/src/app/ai/hooks/useSelectionContextMessages.tsx b/quadratic-client/src/app/ai/hooks/useSelectionContextMessages.tsx new file mode 100644 index 0000000000..d387963a9a --- /dev/null +++ b/quadratic-client/src/app/ai/hooks/useSelectionContextMessages.tsx @@ -0,0 +1,65 @@ +import { maxRects } from '@/app/ui/menus/AIAnalyst/const/maxRects'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { ChatMessage, Context } from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback } from 'react'; + +export function useSelectionContextMessages() { + const getSelectionContext = useCallback( + async ({ selection }: { selection: Context['selection'] }): Promise => { + const selectionContext = selection + ? await quadraticCore.getAIContextRectsInSelections([selection], maxRects) + : undefined; + return [ + { + role: 'user', + content: `Note: This is an internal message for context. Do not quote it in your response.\n\n +${ + selectionContext && selectionContext.length === 1 && selectionContext[0].length > 0 + ? ` +Quadratic, like most spreadsheets, allows the user to select cells on the sheet.\n + +This selection data is being shared with you, for you to refer to in following queries.\n + +I am sharing selection data as an array of tabular data rectangles, each tabular data rectangle in this array has following properties:\n +- sheet_name: This is the name of the sheet.\n +- rect_origin: This is the position of the top left cell of the data rectangle in A1 notation. Columns are represented by letters and rows are represented by numbers.\n +- rect_width: This is the width of the rectangle in number of columns.\n +- rect_height: This is the height of the rectangle in number of rows.\n +- starting_rect_values: This is a 2D array of cell values (json object format described below). This 2D array contains the starting 3 rows of data in the rectangle. This includes headers, if present, and data.\n + +Each cell value is a JSON object having the following properties:\n +- value: The value of the cell. This is a string representation of the value in the cell.\n +- kind: The kind of the value. This can be blank, text, number, logical, time instant, duration, error, html, code, image, date, time, date time, null or undefined.\n +- pos: This is the position of the cell in A1 notation. Columns are represented by letters and rows are represented by numbers.\n\n + +This is being shared so that you can understand the table format, size and value types inside the data rectangle.\n + +Data from cells can be referenced by Formulas, Python, Javascript or SQL code. In Python and Javascript use the cell reference function \`q.cells\`, i.e. \`q.cells(a1_notation_selection_string)\`, to reference data cells. Always use sheet name in a1 notation to reference cells. Sheet name is always enclosed in single quotes. In Python and Javascript, the complete a1 notation selection string is enclosed in double quotes. Example: \`q.cells("'Sheet 1'!A1:B2")\`. In formula, string quotes are not to be used. Example: \`=SUM('Sheet 1'!A1:B2)\`\n +To reference data from different tabular data rectangles, use multiple \`q.cells\` functions.\n +Use this selection data in the context of following messages. Refer to cells if required in code.\n\n + +Current selection data is:\n +\`\`\`json +${JSON.stringify(selectionContext[0])} +\`\`\` + +Note: All this data is only for your reference to data on the sheet. This data cannot be used directly in code. Use the cell reference function \`q.cells\`, i.e. \`q.cells(a1_notation_selection_string)\`, to reference data cells in code. Always use sheet name in a1 notation to reference cells. Sheet name is always enclosed in single quotes. In Python and Javascript, the complete a1 notation selection string is enclosed in double quotes. Example: \`q.cells("'Sheet 1'!A1:B2")\`. In formula, string quotes are not to be used. Example: \`=SUM('Sheet 1'!A1:B2)\`\n\n +` + : `` +} +`, + + contextType: 'selection', + }, + { + role: 'assistant', + content: `I understand the cursor selection data, I will reference it to answer following messages. How can I help you?`, + contextType: 'selection', + }, + ]; + }, + [] + ); + + return { getSelectionContext }; +} diff --git a/quadratic-client/src/app/ai/hooks/useToolUseMessages.tsx b/quadratic-client/src/app/ai/hooks/useToolUseMessages.tsx new file mode 100644 index 0000000000..8719e5cdda --- /dev/null +++ b/quadratic-client/src/app/ai/hooks/useToolUseMessages.tsx @@ -0,0 +1,38 @@ +import { aiToolsSpec } from '@/app/ai/tools/aiToolsSpec'; +import { ChatMessage } from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback } from 'react'; + +export function useToolUseMessages() { + const getToolUsePrompt = useCallback((): ChatMessage[] => { + return [ + { + role: 'user', + content: `Note: This is an internal message for context. Do not quote it in your response.\n\n +Following are the tools you should use to do actions in the spreadsheet, use them to respond to the user prompt.\n + +Include a concise explanation of the actions you are taking to respond to the user prompt. Never guess the answer itself, just the actions you are taking to respond to the user prompt and what the user can do next.\n + +Don't include tool details in your response. Reply in layman's terms what actions you are taking.\n + +Use multiple tools in a single response if required, use same tool multiple times in a single response if required. Try to reduce tool call iterations.\n + +${Object.entries(aiToolsSpec) + .filter(([_, { internalTool }]) => !internalTool) + .map(([name, { prompt }]) => `#${name}\n${prompt}`) + .join('\n\n')} + +All tool actions take place in the currently open sheet only.\n +`, + contextType: 'toolUse', + }, + { + role: 'assistant', + content: + 'I understand these tools are available to me for taking actions on the spreadsheet. How can I help you?', + contextType: 'toolUse', + }, + ]; + }, []); + + return { getToolUsePrompt }; +} diff --git a/quadratic-client/src/app/ai/hooks/useVisibleContextMessages.tsx b/quadratic-client/src/app/ai/hooks/useVisibleContextMessages.tsx new file mode 100644 index 0000000000..fae63e4bfa --- /dev/null +++ b/quadratic-client/src/app/ai/hooks/useVisibleContextMessages.tsx @@ -0,0 +1,106 @@ +import { sheets } from '@/app/grid/controller/Sheets'; +import { rectToA1, xyToA1 } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { maxRects } from '@/app/ui/menus/AIAnalyst/const/maxRects'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { ChatMessage } from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback } from 'react'; + +export function useVisibleContextMessages() { + const getVisibleContext = useCallback(async (): Promise => { + const sheetBounds = sheets.sheet.boundsWithoutFormatting; + const visibleSelection = sheets.getVisibleSelection(); + const [visibleContext, erroredCodeCells] = visibleSelection + ? await Promise.all([ + quadraticCore.getAIContextRectsInSelections([visibleSelection], maxRects), + quadraticCore.getErroredCodeCellsInSelections([visibleSelection]), + ]) + : [undefined, undefined]; + + return [ + { + role: 'user', + content: `Note: This is an internal message for context. Do not quote it in your response.\n\n +I have an open sheet with the following data: +${ + sheetBounds.type === 'nonEmpty' + ? `- Data range: ${rectToA1(sheetBounds)} +- Note: This range may contain empty cells.` + : '- The sheet is currently empty.' +}\n\n + +${ + visibleContext && visibleContext.length === 1 && visibleContext[0].length > 0 + ? ` +Visible data in the viewport:\n + +I am sharing visible data as an array of tabular data rectangles, each tabular data rectangle in this array has following properties:\n +- sheet_name: This is the name of the sheet.\n +- rect_origin: This is the position of the top left cell of the data rectangle in A1 notation. Columns are represented by letters and rows are represented by numbers.\n +- rect_width: This is the width of the rectangle in number of columns.\n +- rect_height: This is the height of the rectangle in number of rows.\n +- starting_rect_values: This is a 2D array of cell values (json object format described below). This 2D array contains the starting 3 rows of data in the rectangle. This includes headers, if present, and data.\n + +Each cell value is a JSON object having the following properties:\n +- value: The value of the cell. This is a string representation of the value in the cell.\n +- kind: The kind of the value. This can be blank, text, number, logical, time instant, duration, error, html, code, image, date, time, date time, null or undefined.\n +- pos: This is the position of the cell in A1 notation. Columns are represented by letters and rows are represented by numbers.\n\n + +This is being shared so that you can understand the table format, size and value types inside the data rectangle.\n + +Data from cells can be referenced by Formulas, Python, Javascript or SQL code. In Python and Javascript use the cell reference function \`q.cells\`, i.e. \`q.cells(a1_notation_selection_string)\`, to reference data cells. Always use sheet name in a1 notation to reference cells. Sheet name is always enclosed in single quotes. In Python and Javascript, the complete a1 notation selection string is enclosed in double quotes. Example: \`q.cells("'Sheet 1'!A1:B2")\`. In formula, string quotes are not to be used. Example: \`=SUM('Sheet 1'!A1:B2)\`\n +To reference data from different tabular data rectangles, use multiple \`q.cells\` functions.\n +Use this visible data in the context of following messages. Refer to cells if required in code.\n\n + +Current visible data is:\n +\`\`\`json +${JSON.stringify(visibleContext[0])} +\`\`\` +Note: All this data is only for your reference to data on the sheet. This data cannot be used directly in code. Use the cell reference function \`q.cells\`, i.e. \`q.cells(a1_notation_selection_string)\`, to reference data cells in code. Always use sheet name in a1 notation to reference cells. Sheet name is always enclosed in single quotes. In Python and Javascript, the complete a1 notation selection string is enclosed in double quotes. Example: \`q.cells("'Sheet 1'!A1:B2")\`. In formula, string quotes are not to be used. Example: \`=SUM('Sheet 1'!A1:B2)\`\n\n +` + : `This visible part of the sheet has no data.\n` +}\n + +${ + erroredCodeCells && erroredCodeCells.length === 1 && erroredCodeCells[0].length > 0 + ? ` +Note: There are code cells in the visible part of the sheet that have errors. Use this to understand if the code cell has any errors and take action when prompted by user to specifically solve the error.\n\n + +Add imports to the top of the code cell and do not use any libraries or functions that are not listed in the Quadratic documentation.\n +Use any functions that are part of the code cell language library.\n +A code cell can return only one type of value as specified in the Quadratic documentation.\n +A code cell cannot display both a chart and return a data frame at the same time.\n +Do not use conditional returns in code cells.\n +A code cell cannot display multiple charts at the same time.\n +Do not use any markdown syntax besides triple backticks for code cell language code blocks.\n +Do not reply code blocks in plain text, use markdown with triple backticks and language name code cell language.\n + +${erroredCodeCells[0].map(({ x, y, language, code_string, std_out, std_err }) => { + const consoleOutput = { + std_out: std_out ?? '', + std_err: std_err ?? '', + }; + return ` +The code cell type is ${language}. The code cell is located at ${xyToA1(Number(x), Number(y))}.\n + +The code in the code cell is:\n +\`\`\`${language}\n${code_string}\n\`\`\` + +Code was run recently and the console output is:\n +\`\`\`json\n${JSON.stringify(consoleOutput)}\n\`\`\` +`; +})}` + : '' +} +`, + contextType: 'visibleData', + }, + { + role: 'assistant', + content: `I understand the visible data, I will reference it to answer following messages. How can I help you?`, + contextType: 'visibleData', + }, + ]; + }, []); + + return { getVisibleContext }; +} diff --git a/quadratic-client/src/app/ai/offline/aiAnalystChats.test.ts b/quadratic-client/src/app/ai/offline/aiAnalystChats.test.ts new file mode 100644 index 0000000000..0ab4116f10 --- /dev/null +++ b/quadratic-client/src/app/ai/offline/aiAnalystChats.test.ts @@ -0,0 +1,173 @@ +import { defaultAIAnalystContext } from '@/app/ui/menus/AIAnalyst/const/defaultAIAnalystContext'; +import 'fake-indexeddb/auto'; +import { Chat } from 'quadratic-shared/typesAndSchemasAI'; +import { v4 } from 'uuid'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { aiAnalystOfflineChats } from './aiAnalystChats'; + +describe('aiAnalystOfflineChats', () => { + beforeAll(async () => { + await aiAnalystOfflineChats.init('test@example.com', 'test-fileId'); + }); + + beforeEach(async () => { + await aiAnalystOfflineChats.testClear(); + }); + + it('properly defines user email and fileId', () => { + expect(aiAnalystOfflineChats).toBeDefined(); + expect(aiAnalystOfflineChats.userEmail).toBe('test@example.com'); + expect(aiAnalystOfflineChats.fileId).toBe('test-fileId'); + }); + + it('loads empty chats', async () => { + expect(await aiAnalystOfflineChats.loadChats()).toStrictEqual([]); + }); + + it('saves and loads chats', async () => { + const testChats: Chat[] = [ + { + id: v4(), + name: 'Chat 1', + lastUpdated: Date.now(), + messages: [ + { role: 'user', content: 'test1', contextType: 'quadraticDocs' }, + { + role: 'assistant', + content: 'response1', + contextType: 'quadraticDocs', + }, + ], + }, + { + id: v4(), + name: 'Chat 2', + lastUpdated: Date.now(), + messages: [ + { role: 'user', content: 'test2', contextType: 'userPrompt', context: defaultAIAnalystContext }, + { + role: 'assistant', + content: 'response2', + contextType: 'userPrompt', + toolCalls: [], + }, + ], + }, + ]; + + await aiAnalystOfflineChats.saveChats(testChats); + const loadedChats = await aiAnalystOfflineChats.loadChats(); + + expect(loadedChats.length).toBe(2); + const testChat1 = loadedChats.find((chat) => chat.id === testChats[0].id); + const testChat2 = loadedChats.find((chat) => chat.id === testChats[1].id); + expect(testChat1).toBeDefined(); + expect(testChat2).toBeDefined(); + expect(testChat1?.name).toBe('Chat 1'); + expect(testChat1?.messages.length).toBe(0); // Only userPrompt messages are stored + expect(testChat2?.name).toBe('Chat 2'); + expect(testChat2?.messages[0].content).toBe('test2'); + expect(testChat2?.messages[1].content).toBe('response2'); + }); + + it('deletes chats', async () => { + const testChats: Chat[] = [ + { + id: v4(), + name: 'Chat 1', + lastUpdated: Date.now(), + messages: [{ role: 'user', content: 'test1', contextType: 'userPrompt', context: defaultAIAnalystContext }], + }, + { + id: v4(), + name: 'Chat 2', + lastUpdated: Date.now(), + messages: [{ role: 'user', content: 'test2', contextType: 'userPrompt', context: defaultAIAnalystContext }], + }, + { + id: v4(), + name: 'Chat 3', + lastUpdated: Date.now(), + messages: [{ role: 'user', content: 'test3', contextType: 'userPrompt', context: defaultAIAnalystContext }], + }, + ]; + + await aiAnalystOfflineChats.saveChats(testChats); + expect((await aiAnalystOfflineChats.loadChats()).length).toBe(3); + + await aiAnalystOfflineChats.deleteChats([testChats[1].id]); + + const loadedChats = await aiAnalystOfflineChats.loadChats(); + expect(loadedChats.length).toBe(2); + const testChat1 = loadedChats.find((chat) => chat.id === testChats[0].id); + const testChat2 = loadedChats.find((chat) => chat.id === testChats[2].id); + expect(testChat1).toBeDefined(); + expect(testChat2).toBeDefined(); + expect(testChat1?.id).toBe(testChats[0].id); + expect(testChat2?.id).toBe(testChats[2].id); + }); + + it('deletes file', async () => { + const testChats: Chat[] = [ + { + id: v4(), + name: 'Chat 1', + lastUpdated: Date.now(), + messages: [{ role: 'user', content: 'test1', contextType: 'userPrompt', context: defaultAIAnalystContext }], + }, + { + id: v4(), + name: 'Chat 2', + lastUpdated: Date.now(), + messages: [{ role: 'user', content: 'test2', contextType: 'userPrompt', context: defaultAIAnalystContext }], + }, + { + id: v4(), + name: 'Chat 3', + lastUpdated: Date.now(), + messages: [{ role: 'user', content: 'test3', contextType: 'userPrompt', context: defaultAIAnalystContext }], + }, + ]; + await aiAnalystOfflineChats.saveChats(testChats); + expect((await aiAnalystOfflineChats.loadChats()).length).toBe(3); + + await aiAnalystOfflineChats.deleteFile('test@example.com', 'test-fileId'); + expect((await aiAnalystOfflineChats.loadChats()).length).toBe(0); + }); + + it('filters chats by userEmail', async () => { + // Save chats with current userEmail + const testChats: Chat[] = [ + { + id: v4(), + name: 'Chat 1', + lastUpdated: Date.now(), + messages: [{ role: 'user', content: 'test1', contextType: 'userPrompt', context: defaultAIAnalystContext }], + }, + ]; + + await aiAnalystOfflineChats.saveChats(testChats); + expect((await aiAnalystOfflineChats.loadChats()).length).toBe(1); + + await aiAnalystOfflineChats.init('different@example.com', 'test-fileId'); + expect((await aiAnalystOfflineChats.loadChats()).length).toBe(0); + }); + + it('filters chats by fileId', async () => { + // Save chats with current fileId + const testChats: Chat[] = [ + { + id: v4(), + name: 'Chat 1', + lastUpdated: Date.now(), + messages: [{ role: 'user', content: 'test1', contextType: 'userPrompt', context: defaultAIAnalystContext }], + }, + ]; + await aiAnalystOfflineChats.saveChats(testChats); + expect((await aiAnalystOfflineChats.loadChats()).length).toBe(1); + + // Init with different fileId + await aiAnalystOfflineChats.init('test@example.com', 'different-fileId'); + expect((await aiAnalystOfflineChats.loadChats()).length).toBe(0); + }); +}); diff --git a/quadratic-client/src/app/ai/offline/aiAnalystChats.ts b/quadratic-client/src/app/ai/offline/aiAnalystChats.ts new file mode 100644 index 0000000000..e2f91e6a90 --- /dev/null +++ b/quadratic-client/src/app/ai/offline/aiAnalystChats.ts @@ -0,0 +1,158 @@ +import { getPromptMessages } from '@/app/ai/tools/message.helper'; +import { Chat, ChatSchema } from 'quadratic-shared/typesAndSchemasAI'; + +const DB_NAME = 'Quadratic-AI'; +const DB_VERSION = 1; +const DB_STORE = 'aiAnalystChats'; + +class AIAnalystOfflineChats { + private db?: IDBDatabase; + userEmail?: string; + fileId?: string; + + init = (userEmail: string, fileId: string): Promise => { + return new Promise((resolve, reject) => { + this.userEmail = userEmail; + this.fileId = fileId; + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = (event) => { + console.error('Error opening indexedDB', event); + reject(new Error('Failed to open database')); + }; + + request.onsuccess = () => { + this.db = request.result; + resolve(undefined); + }; + + request.onupgradeneeded = (event) => { + try { + const db = request.result; + const objectStore = db.createObjectStore(DB_STORE, { + keyPath: ['userEmail', 'fileId', 'id'], + }); + objectStore.createIndex('userEmail', 'userEmail'); + objectStore.createIndex('fileId', 'fileId'); + } catch (error) { + console.error('Error during database upgrade:', error); + reject(error); + } + }; + }); + }; + + // Helper method to validate required properties + private validateState = (methodName: string) => { + if (!this.db || !this.userEmail || !this.fileId) { + throw new Error(`Expected db, userEmail and fileId to be set in ${methodName} method.`); + } + return { db: this.db, userEmail: this.userEmail, fileId: this.fileId }; + }; + + // Helper method to create transactions + private createTransaction = (mode: IDBTransactionMode, operation: (store: IDBObjectStore) => void): Promise => { + const { db } = this.validateState('transaction'); + + return new Promise((resolve, reject) => { + const tx = db.transaction(DB_STORE, mode); + const store = tx.objectStore(DB_STORE); + + tx.oncomplete = () => resolve(undefined); + tx.onerror = () => reject(tx.error); + + operation(store); + }); + }; + + // Load all chats for current user + loadChats = (): Promise => { + const { db, userEmail, fileId } = this.validateState('loadChats'); + + return new Promise((resolve, reject) => { + try { + const tx = db.transaction(DB_STORE, 'readonly'); + const store = tx.objectStore(DB_STORE).index('userEmail'); + const keyRange = IDBKeyRange.only(userEmail); + const getAll = store.getAll(keyRange); + + getAll.onsuccess = () => { + let chats = getAll.result + .filter((chat) => chat.fileId === fileId) + .map(({ userEmail, fileId, ...chat }) => chat); + chats = chats.filter((chat) => { + if (ChatSchema.safeParse(chat).success) { + return true; + } else { + // delete chat if it is not valid or schema has changed + this.deleteChats([chat.id]).catch((error) => { + console.error('[AIAnalystOfflineChats] loadChats: ', error); + }); + return false; + } + }); + resolve(chats); + }; + getAll.onerror = () => { + console.error('Error loading chats:', getAll.error); + reject(new Error('Failed to load chats')); + }; + + tx.onerror = () => { + console.error('Transaction error:', tx.error); + reject(new Error('Transaction failed while loading chats')); + }; + } catch (error) { + console.error('Unexpected error in loadChats:', error); + reject(error); + } + }); + }; + + // Save or update a chat + saveChats = async (chats: Chat[]) => { + const { userEmail, fileId } = this.validateState('saveChats'); + await this.createTransaction('readwrite', (store) => { + chats.forEach((chat) => { + const chatEntry = { + userEmail, + fileId, + ...{ + ...chat, + messages: getPromptMessages(chat.messages), + }, + }; + store.put(chatEntry); + }); + }); + }; + + // Delete a list of chats + deleteChats = async (chatIds: string[]) => { + const { userEmail, fileId } = this.validateState('deleteChats'); + await this.createTransaction('readwrite', (store) => { + chatIds.forEach((chatId) => { + store.delete([userEmail, fileId, chatId]); + }); + }); + }; + + // Delete all chats for a file + deleteFile = async (userEmail: string, fileId: string) => { + if (this.userEmail !== userEmail || this.fileId !== fileId) { + await this.init(userEmail, fileId); + } + const chats = await this.loadChats(); + const chatIds = chats.map((chat) => chat.id); + await this.deleteChats(chatIds); + }; + + // Used by tests to clear all entries from the indexedDb for this fileId + testClear = async () => { + await this.createTransaction('readwrite', (store) => { + store.clear(); + }); + }; +} + +export const aiAnalystOfflineChats = new AIAnalystOfflineChats(); diff --git a/quadratic-client/src/app/ai/tools/aiTools.ts b/quadratic-client/src/app/ai/tools/aiTools.ts new file mode 100644 index 0000000000..d48936609c --- /dev/null +++ b/quadratic-client/src/app/ai/tools/aiTools.ts @@ -0,0 +1,7 @@ +export enum AITool { + SetChatName = 'set_chat_name', + SetCellValues = 'set_cell_values', + SetCodeCellValue = 'set_code_cell_value', + MoveCells = 'move_cells', + DeleteCells = 'delete_cells', +} diff --git a/quadratic-client/src/app/ai/tools/aiToolsSpec.ts b/quadratic-client/src/app/ai/tools/aiToolsSpec.ts new file mode 100644 index 0000000000..67c218c7b6 --- /dev/null +++ b/quadratic-client/src/app/ai/tools/aiToolsSpec.ts @@ -0,0 +1,334 @@ +import { AITool } from '@/app/ai/tools/aiTools'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { ensureRectVisible } from '@/app/gridGL/interaction/viewportHelper'; +import { SheetRect } from '@/app/quadratic-core-types'; +import { stringToSelection } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { AIToolArgs } from 'quadratic-shared/typesAndSchemasAI'; +import { z } from 'zod'; + +type AIToolSpec = { + internalTool: boolean; // tools meant to get structured data from AI, but not meant to be used during chat + description: string; // this is sent with tool definition, has a maximum character limit + parameters: { + type: 'object'; + properties: Record; + required: string[]; + additionalProperties: boolean; + }; + responseSchema: (typeof AIToolsArgsSchema)[T]; + action: (args: z.infer<(typeof AIToolsArgsSchema)[T]>) => Promise; + prompt: string; // this is sent as internal message to AI, no character limit +}; + +export const AIToolsArgsSchema = { + [AITool.SetChatName]: z.object({ + chat_name: z.string(), + }), + [AITool.SetCodeCellValue]: z.object({ + code_cell_language: z.enum(['Python', 'Javascript', 'Formula']), + code_cell_position: z.string(), + code_string: z.string(), + output_width: z.number(), + output_height: z.number(), + }), + [AITool.SetCellValues]: z.object({ + top_left_position: z.string(), + cell_values: z.array(z.array(z.string())), + }), + [AITool.MoveCells]: z.object({ + source_selection_rect: z.string(), + target_top_left_position: z.string(), + }), + [AITool.DeleteCells]: z.object({ + selection: z.string(), + }), +} as const; + +export type AIToolSpecRecord = { + [K in AITool]: AIToolSpec; +}; + +export const aiToolsSpec: AIToolSpecRecord = { + [AITool.SetChatName]: { + internalTool: true, + description: ` +Set the name of the user chat with AI assistant, this is the name of the chat in the chat history\n +You should use the set_chat_name function to set the name of the user chat with AI assistant, this is the name of the chat in the chat history.\n +This function requires the name of the chat, this should be concise and descriptive of the conversation, and should be easily understandable by a non-technical user.\n +The chat name should be based on user's messages and should reflect his/her queries and goals.\n +This name should be from user's perspective, not the assistant's.\n +`, + parameters: { + type: 'object', + properties: { + chat_name: { + type: 'string', + description: 'The name of the chat', + }, + }, + required: ['chat_name'], + additionalProperties: false, + }, + responseSchema: AIToolsArgsSchema[AITool.SetChatName], + action: async (args) => { + // no action as this tool is only meant to get structured data from AI + return `Executed set chat name tool successfully with name: ${args.chat_name}`; + }, + prompt: ` +You can use the set_chat_name function to set the name of the user chat with AI assistant, this is the name of the chat in the chat history.\n +This function requires the name of the chat, this should be concise and descriptive of the conversation, and should be easily understandable by a non-technical user.\n +The chat name should be based on user's messages and should reflect his/her queries and goals.\n +This name should be from user's perspective, not the assistant's.\n +`, + }, + [AITool.SetCellValues]: { + internalTool: false, + description: ` +Sets the values of the currently open sheet cells to a 2d array of strings, requires the top left cell position (in a1 notation) and the 2d array of strings representing the cell values to set.\n +Use set_cell_values function to add data to the currently open sheet. Don't use code cell for adding data. Always add data using this function.\n +Values are string representation of text, number, logical, time instant, duration, error, html, code, image, date, time or blank.\n +top_left_position is the position of the top left corner of the 2d array of values on the currently open sheet, in a1 notation. This should be a single cell, not a range. Each sub array represents a row of values.\n +All values can be referenced in the code cells immediately. Always refer to the cell by its position on respective sheet, in a1 notation. Don't add values manually in code cells.\n +To clear the values of a cell, set the value to an empty string.\n +`, + parameters: { + type: 'object', + properties: { + top_left_position: { + type: 'string', + description: + 'The position of the top left cell, in a1 notation, in the currently open sheet. This is the top left corner of the added 2d array of values on the currently open sheet. This should be a single cell, not a range.', + }, + cell_values: { + type: 'array', + items: { + type: 'array', + items: { + type: 'string', + description: 'The string that is the value to set in the cell', + }, + }, + }, + }, + required: ['top_left_position', 'cell_values'], + additionalProperties: false, + }, + responseSchema: AIToolsArgsSchema[AITool.SetCellValues], + action: async (args) => { + const { top_left_position, cell_values } = args; + try { + const selection = stringToSelection(top_left_position, sheets.current, sheets.getSheetIdNameMap()); + if (!selection.isSingleSelection()) { + return 'Invalid code cell position, this should be a single cell, not a range'; + } + const { x, y } = selection.getCursor(); + + quadraticCore.setCellValues(sheets.current, x, y, cell_values, sheets.getCursorPosition()); + ensureRectVisible({ x, y }, { x: x + cell_values[0].length - 1, y: y + cell_values.length - 1 }); + + return 'Executed set cell values tool successfully'; + } catch (e) { + return `Error executing set cell values tool: ${e}`; + } + }, + prompt: ` +You should use the set_cell_values function to set the values of the currently open sheet cells to a 2d array of strings.\n +Use this function to add data to the currently open sheet. Don't use code cell for adding data. Always add data using this function.\n +This function requires the top left cell position (in a1 notation) and the 2d array of strings representing the cell values to set. Values are string representation of text, number, logical, time instant, duration, error, html, code, image, date, time or blank.\n +Values set using this function will replace the existing values in the cell and can be referenced in the code cells immediately. Always refer to the cell by its position on respective sheet, in a1 notation. Don't add these in code cells.\n +To clear the values of a cell, set the value to an empty string.\n +`, + }, + [AITool.SetCodeCellValue]: { + internalTool: false, + description: ` +Sets the value of a code cell and run it in the currently open sheet, requires the cell position (in a1 notation), codeString and language\n +You should use the set_code_cell_value function to set this code cell value. Use this function instead of responding with code.\n +Never use set_code_cell_value function to set the value of a cell to a value that is not a code. Don't add static data to the currently open sheet using set_code_cell_value function, use set_cell_values instead. set_code_cell_value function is only meant to set the value of a cell to a code.\n +Always refer to the data from cell by its position in a1 notation from respective sheet. Don't add values manually in code cells.\n +`, + parameters: { + type: 'object', + properties: { + code_cell_language: { + type: 'string', + description: + 'The language of the code cell, this can be one of Python, Javascript or Formula. This is case sensitive.', + }, + code_cell_position: { + type: 'string', + description: + 'The position of the code cell in the currently open sheet, in a1 notation. This should be a single cell, not a range.', + }, + code_string: { + type: 'string', + description: 'The code which will run in the cell', + }, + output_width: { + type: 'number', + description: + 'The width, i.e. number of columns, of the code output on running this Code in the currently open spreadsheet', + }, + output_height: { + type: 'number', + description: + 'The height, i.e. number of rows, of the code output on running this Code in the currently open spreadsheet', + }, + }, + required: ['code_cell_language', 'code_cell_position', 'code_string', 'output_width', 'output_height'], + additionalProperties: false, + }, + responseSchema: AIToolsArgsSchema[AITool.SetCodeCellValue], + action: async (args) => { + let { code_cell_language, code_string, code_cell_position, output_width, output_height } = args; + try { + const selection = stringToSelection(code_cell_position, sheets.current, sheets.getSheetIdNameMap()); + if (!selection.isSingleSelection()) { + return 'Invalid code cell position, this should be a single cell, not a range'; + } + const { x, y } = selection.getCursor(); + + if (code_cell_language === 'Formula' && code_string.startsWith('=')) { + code_string = code_string.slice(1); + } + + quadraticCore.setCodeCellValue({ + sheetId: sheets.current, + x, + y, + codeString: code_string, + language: code_cell_language, + cursor: sheets.getCursorPosition(), + }); + ensureRectVisible({ x, y }, { x: x + output_width - 1, y: y + output_height - 1 }); + + return 'Executed set code cell value tool successfully'; + } catch (e) { + return `Error executing set code cell value tool: ${e}`; + } + }, + prompt: ` +You should use the set_code_cell_value function to set this code cell value. Use set_code_cell_value function instead of responding with code.\n +Never use set_code_cell_value function to set the value of a cell to a value that is not a code. Don't add data to the currently open sheet using set_code_cell_value function, use set_cell_values instead. set_code_cell_value function is only meant to set the value of a cell to a code.\n +set_code_cell_value function requires language, codeString, the cell position (single cell in a1 notation) and the width and height of the code output on running this Code in the currently open sheet.\n +Always refer to the cells on sheet by its position in a1 notation, using q.cells function. Don't add values manually in code cells.\n +The required location code_cell_position for this code cell is one which satisfies the following conditions:\n + - The code cell location should be empty and should have enough space to the right and below to accommodate the code result. If there is a value in a single cell where the code result is suppose to go, it will result in spill error. Use currently open sheet context to identify empty space.\n + - The code cell should be near the data it references, so that it is easy to understand the code in the context of the data. Identify the data being referred from code and use a cell close to it. If multiple data references are being made, choose the one which is most used or most important. This will make it easy to understand the code in the context of the table.\n + - If the referenced data is portrait in a table format, the code cell should be next to the top right corner of the table.\n + - If the referenced data is landscape in a table format, the code cell should be below the bottom left corner of the table.\n + - Always leave a blank row / column between the code cell and the data it references.\n + - In case there is not enough empty space near the referenced data, choose a distant empty cell which is in the same row as the top right corner of referenced data and to the right of this data.\n + - If there are multiple tables or data sources being referenced, place the code cell in a location that provides a good balance between proximity to all referenced data and maintaining readability of the currently open sheet.\n + - Consider the overall layout and organization of the currently open sheet when placing the code cell, ensuring it doesn't disrupt existing data or interfere with other code cells.\n + - A plot returned by the code cell occupies just one cell, the plot overlay is larger but the code cell is always just one cell.\n + - Do not use conditional returns in python code cells.\n + - Don't prefix formulas with \`=\` in code cells.\n + `, + }, + [AITool.MoveCells]: { + internalTool: false, + description: ` +Moves a rectangular selection of cells from one location to another on the currently open sheet, requires the source and target locations.\n +You should use the move_cells function to move a rectangular selection of cells from one location to another on the currently open sheet.\n +move_cells function requires the source and target locations. Source location is the top left and bottom right corners of the selection rectangle to be moved.\n +Target location is the top left corner of the target location on the currently open sheet.\n +`, + parameters: { + type: 'object', + properties: { + source_selection_rect: { + type: 'string', + description: + 'The selection of cells, in a1 notation, to be moved in the currently open sheet. This is string representation of the rectangular selection of cells to be moved', + }, + target_top_left_position: { + type: 'string', + description: + 'The top left position of the target location on the currently open sheet, in a1 notation. This should be a single cell, not a range. This will be the top left corner of the source selection rectangle after moving.', + }, + }, + required: ['source_selection_rect', 'target_top_left_position'], + additionalProperties: false, + }, + responseSchema: AIToolsArgsSchema[AITool.MoveCells], + action: async (args) => { + const { source_selection_rect, target_top_left_position } = args; + try { + const sourceSelection = stringToSelection(source_selection_rect, sheets.current, sheets.getSheetIdNameMap()); + const sourceRect = sourceSelection.getSingleRectangle(); + if (!sourceRect) { + return 'Invalid source selection, this should be a single rectangle, not a range'; + } + const sheetRect: SheetRect = { + min: { + x: sourceRect.min.x, + y: sourceRect.min.y, + }, + max: { + x: sourceRect.max.x, + y: sourceRect.max.y, + }, + sheet_id: { + id: sheets.current, + }, + }; + + const targetSelection = stringToSelection(target_top_left_position, sheets.current, sheets.getSheetIdNameMap()); + if (!targetSelection.isSingleSelection()) { + return 'Invalid code cell position, this should be a single cell, not a range'; + } + const { x, y } = targetSelection.getCursor(); + + quadraticCore.moveCells(sheetRect, x, y, sheets.current); + + return `Executed move cells tool successfully.`; + } catch (e) { + return `Error executing move cells tool: ${e}`; + } + }, + prompt: ` +You should use the move_cells function to move a rectangular selection of cells from one location to another on the currently open sheet.\n +move_cells function requires the source selection and target position. Source selection is the string representation (in a1 notation) of a selection rectangle to be moved.\n +Target position is the top left corner of the target position on the currently open sheet, in a1 notation. This should be a single cell, not a range.\n +`, + }, + [AITool.DeleteCells]: { + internalTool: false, + description: ` +Deletes the value(s) of a selection of cells, requires a string representation of a selection of cells to delete. Selection can be a single cell or a range of cells or multiple ranges in a1 notation.\n +You should use the delete_cells function to delete the value(s) of a selection of cells on the currently open sheet.\n +delete_cells functions requires a string representation (in a1 notation) of a selection of cells to delete. Selection can be a single cell or a range of cells or multiple ranges in a1 notation.\n +`, + parameters: { + type: 'object', + properties: { + selection: { + type: 'string', + description: + 'The string representation (in a1 notation) of the selection of cells to delete, this can be a single cell or a range of cells or multiple ranges in a1 notation', + }, + }, + required: ['selection'], + additionalProperties: false, + }, + responseSchema: AIToolsArgsSchema[AITool.DeleteCells], + action: async (args) => { + const { selection } = args; + try { + const sourceSelection = stringToSelection(selection, sheets.current, sheets.getSheetIdNameMap()); + + quadraticCore.deleteCellValues(sourceSelection.save(), sheets.getCursorPosition()); + + return `Executed delete cells tool successfully.`; + } catch (e) { + return `Error executing delete cells tool: ${e}`; + } + }, + prompt: ` +You should use the delete_cells function to delete value(s) on the currently open sheet.\n +delete_cells functions requires a string representation (in a1 notation) of a selection of cells to delete. Selection can be a single cell or a range of cells or multiple ranges in a1 notation.\n +`, + }, +} as const; diff --git a/quadratic-client/src/app/ai/tools/endpoint.helper.ts b/quadratic-client/src/app/ai/tools/endpoint.helper.ts new file mode 100644 index 0000000000..26980a58d8 --- /dev/null +++ b/quadratic-client/src/app/ai/tools/endpoint.helper.ts @@ -0,0 +1,19 @@ +import { AI } from '@/shared/constants/routes'; +import { AIModel } from 'quadratic-shared/typesAndSchemasAI'; +import { isAnthropicBedrockModel, isAnthropicModel, isBedrockModel, isOpenAIModel } from './model.helper'; + +export function getAIProviderEndpoint(model: AIModel, stream: boolean): string { + if (isBedrockModel(model)) { + return stream ? AI.BEDROCK.STREAM : AI.BEDROCK.CHAT; + } + if (isAnthropicBedrockModel(model)) { + return stream ? AI.BEDROCK.ANTHROPIC.STREAM : AI.BEDROCK.ANTHROPIC.CHAT; + } + if (isAnthropicModel(model)) { + return stream ? AI.ANTHROPIC.STREAM : AI.ANTHROPIC.CHAT; + } + if (isOpenAIModel(model)) { + return stream ? AI.OPENAI.STREAM : AI.OPENAI.CHAT; + } + throw new Error(`Unknown model: ${model}`); +} diff --git a/quadratic-client/src/app/ai/tools/message.helper.ts b/quadratic-client/src/app/ai/tools/message.helper.ts new file mode 100644 index 0000000000..0af8d7cbd8 --- /dev/null +++ b/quadratic-client/src/app/ai/tools/message.helper.ts @@ -0,0 +1,206 @@ +import { + AIModel, + AIPromptMessage, + AnthropicPromptMessage, + BedrockPromptMessage, + ChatMessage, + OpenAIPromptMessage, + SystemMessage, +} from 'quadratic-shared/typesAndSchemasAI'; +import { isAnthropicBedrockModel, isAnthropicModel, isBedrockModel, isOpenAIModel } from './model.helper'; + +export const getSystemMessages = (messages: ChatMessage[]): string[] => { + const systemMessages: SystemMessage[] = messages.filter( + (message): message is SystemMessage => + message.role === 'user' && message.contextType !== 'userPrompt' && message.contextType !== 'toolResult' + ); + return systemMessages.map((message) => message.content); +}; + +export const getPromptMessages = (messages: ChatMessage[]): ChatMessage[] => { + return messages.filter((message) => message.contextType === 'userPrompt' || message.contextType === 'toolResult'); +}; + +export const getMessagesForModel = ( + model: AIModel, + messages: ChatMessage[] +): { system?: string | { text: string }[]; messages: AIPromptMessage[] } => { + // send internal context messages as system messages + const systemMessages: string[] = getSystemMessages(messages); + const promptMessages = getPromptMessages(messages); + + // send all messages as prompt messages + // const systemMessages: string[] = []; + // const promptMessages = messages; + + if (isBedrockModel(model)) { + const bedrockMessages: BedrockPromptMessage[] = promptMessages.map((message) => { + if (message.role === 'assistant' && message.contextType === 'userPrompt' && message.toolCalls.length > 0) { + const bedrockMessage: BedrockPromptMessage = { + role: message.role, + content: [ + ...(message.content + ? [ + { + text: message.content, + }, + ] + : []), + ...message.toolCalls.map((toolCall) => ({ + toolUse: { + toolUseId: toolCall.id, + name: toolCall.name, + input: JSON.parse(toolCall.arguments), + }, + })), + ], + }; + return bedrockMessage; + } else if (message.role === 'user' && message.contextType === 'toolResult') { + const bedrockMessage: BedrockPromptMessage = { + role: message.role, + content: [ + ...message.content.map((toolResult) => ({ + toolResult: { + toolUseId: toolResult.id, + content: [ + { + text: toolResult.content, + }, + ], + status: 'success' as const, + }, + })), + ], + }; + return bedrockMessage; + } else { + const bedrockMessage: BedrockPromptMessage = { + role: message.role, + content: [ + { + text: message.content, + }, + ], + }; + return bedrockMessage; + } + }); + + return { messages: bedrockMessages, system: systemMessages.map((message) => ({ text: message })) }; + } + + if (isAnthropicModel(model) || isAnthropicBedrockModel(model)) { + const anthropicMessages: AnthropicPromptMessage[] = promptMessages.reduce( + (acc, message) => { + if (message.role === 'assistant' && message.contextType === 'userPrompt' && message.toolCalls.length > 0) { + const anthropicMessages: AnthropicPromptMessage[] = [ + ...acc, + { + role: message.role, + content: [ + ...(message.content + ? [ + { + type: 'text' as const, + text: message.content, + }, + ] + : []), + ...message.toolCalls.map((toolCall) => ({ + type: 'tool_use' as const, + id: toolCall.id, + name: toolCall.name, + input: JSON.parse(toolCall.arguments), + })), + ], + }, + ]; + return anthropicMessages; + } else if (message.role === 'user' && message.contextType === 'toolResult') { + const anthropicMessages: AnthropicPromptMessage[] = [ + ...acc, + { + role: message.role, + content: [ + ...message.content.map((toolResult) => ({ + type: 'tool_result' as const, + tool_use_id: toolResult.id, + content: toolResult.content, + })), + { + type: 'text' as const, + text: 'Given the above tool calls results, please provide your final answer to the user.', + }, + ], + }, + ]; + return anthropicMessages; + } else { + const anthropicMessages: AnthropicPromptMessage[] = [ + ...acc, + { + role: message.role, + content: message.content, + }, + ]; + return anthropicMessages; + } + }, + [] + ); + + return { messages: anthropicMessages, system: systemMessages.join('\n\n') }; + } + + if (isOpenAIModel(model)) { + const messages: OpenAIPromptMessage[] = promptMessages.reduce((acc, message) => { + if (message.role === 'assistant' && message.contextType === 'userPrompt' && message.toolCalls.length > 0) { + const openaiMessages: OpenAIPromptMessage[] = [ + ...acc, + { + role: message.role, + content: message.content, + tool_calls: message.toolCalls.map((toolCall) => ({ + id: toolCall.id, + type: 'function' as const, + function: { + name: toolCall.name, + arguments: toolCall.arguments, + }, + })), + }, + ]; + return openaiMessages; + } else if (message.role === 'user' && message.contextType === 'toolResult') { + const openaiMessages: OpenAIPromptMessage[] = [ + ...acc, + ...message.content.map((toolResult) => ({ + role: 'tool' as const, + tool_call_id: toolResult.id, + content: toolResult.content, + })), + ]; + return openaiMessages; + } else { + const openaiMessages: OpenAIPromptMessage[] = [ + ...acc, + { + role: message.role, + content: message.content, + }, + ]; + return openaiMessages; + } + }, []); + + const openaiMessages: OpenAIPromptMessage[] = [ + { role: 'system', content: systemMessages.map((message) => ({ type: 'text', text: message })) }, + ...messages, + ]; + + return { messages: openaiMessages }; + } + + throw new Error(`Unknown model: ${model}`); +}; diff --git a/quadratic-client/src/app/ai/tools/model.helper.ts b/quadratic-client/src/app/ai/tools/model.helper.ts new file mode 100644 index 0000000000..367eaa1f63 --- /dev/null +++ b/quadratic-client/src/app/ai/tools/model.helper.ts @@ -0,0 +1,24 @@ +import { MODEL_OPTIONS } from 'quadratic-shared/AI_MODELS'; +import { + AIModel, + AnthropicModel, + BedrockAnthropicModel, + BedrockModel, + OpenAIModel, +} from 'quadratic-shared/typesAndSchemasAI'; + +export function isBedrockModel(model: AIModel): model is BedrockModel { + return MODEL_OPTIONS[model].provider === 'bedrock'; +} + +export function isAnthropicBedrockModel(model: AIModel): model is BedrockAnthropicModel { + return MODEL_OPTIONS[model].provider === 'bedrock-anthropic'; +} + +export function isAnthropicModel(model: AIModel): model is AnthropicModel { + return MODEL_OPTIONS[model].provider === 'anthropic'; +} + +export function isOpenAIModel(model: AIModel): model is OpenAIModel { + return MODEL_OPTIONS[model].provider === 'openai'; +} diff --git a/quadratic-client/src/app/ai/tools/tool.helpers.ts b/quadratic-client/src/app/ai/tools/tool.helpers.ts new file mode 100644 index 0000000000..7f411c9f15 --- /dev/null +++ b/quadratic-client/src/app/ai/tools/tool.helpers.ts @@ -0,0 +1,82 @@ +import { AITool as AIToolName } from '@/app/ai/tools/aiTools'; +import { aiToolsSpec } from '@/app/ai/tools/aiToolsSpec'; +import { + AIModel, + AITool, + AIToolChoice, + AnthropicTool, + AnthropicToolChoice, + BedrockTool, + BedrockToolChoice, + OpenAITool, + OpenAIToolChoice, +} from 'quadratic-shared/typesAndSchemasAI'; +import { isAnthropicBedrockModel, isAnthropicModel, isBedrockModel, isOpenAIModel } from './model.helper'; + +export const getTools = (model: AIModel, toolChoice?: AIToolName): AITool[] => { + const tools = Object.entries(aiToolsSpec).filter(([name, toolSpec]) => { + if (toolChoice === undefined) { + return !toolSpec.internalTool; + } + return name === toolChoice; + }); + + if (isBedrockModel(model)) { + return tools.map( + ([name, { description, parameters: input_schema }]): BedrockTool => ({ + toolSpec: { + name, + description, + inputSchema: { + json: input_schema, + }, + }, + }) + ); + } + + if (isAnthropicModel(model) || isAnthropicBedrockModel(model)) { + return tools.map( + ([name, { description, parameters: input_schema }]): AnthropicTool => ({ + name, + description, + input_schema, + }) + ); + } + + if (isOpenAIModel(model)) { + return tools.map( + ([name, { description, parameters }]): OpenAITool => ({ + type: 'function' as const, + function: { + name, + description, + parameters, + strict: true, + }, + }) + ); + } + + throw new Error(`Unknown model: ${model}`); +}; + +export const getToolChoice = (model: AIModel, name?: AIToolName): AIToolChoice => { + if (isBedrockModel(model)) { + const toolChoice: BedrockToolChoice = name === undefined ? { auto: {} } : { tool: { name } }; + return toolChoice; + } + + if (isAnthropicModel(model) || isAnthropicBedrockModel(model)) { + const toolChoice: AnthropicToolChoice = name === undefined ? { type: 'auto' } : { type: 'tool', name }; + return toolChoice; + } + + if (isOpenAIModel(model)) { + const toolChoice: OpenAIToolChoice = name === undefined ? 'auto' : { type: 'function', function: { name } }; + return toolChoice; + } + + throw new Error(`Unknown model: ${model}`); +}; diff --git a/quadratic-client/src/app/atoms/aiAnalystAtom.ts b/quadratic-client/src/app/atoms/aiAnalystAtom.ts new file mode 100644 index 0000000000..4ba67d8dbc --- /dev/null +++ b/quadratic-client/src/app/atoms/aiAnalystAtom.ts @@ -0,0 +1,287 @@ +import { aiAnalystOfflineChats } from '@/app/ai/offline/aiAnalystChats'; +import { getPromptMessages } from '@/app/ai/tools/message.helper'; +import { editorInteractionStateUserAtom, editorInteractionStateUuidAtom } from '@/app/atoms/editorInteractionStateAtom'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { focusGrid } from '@/app/helpers/focusGrid'; +import { Chat, ChatMessage } from 'quadratic-shared/typesAndSchemasAI'; +import { atom, DefaultValue, selector } from 'recoil'; +import { v4 } from 'uuid'; + +export interface AIAnalystState { + showAIAnalyst: boolean; + showChatHistory: boolean; + abortController?: AbortController; + loading: boolean; + chats: Chat[]; + currentChat: Chat; +} + +export const defaultAIAnalystState: AIAnalystState = { + showAIAnalyst: false, + showChatHistory: false, + abortController: undefined, + loading: false, + chats: [], + currentChat: { + id: '', + name: '', + lastUpdated: Date.now(), + messages: [], + }, +}; + +export const aiAnalystAtom = atom({ + key: 'aiAnalystAtom', + default: defaultAIAnalystState, + effects: [ + async ({ getPromise, setSelf, trigger }) => { + if (trigger === 'get') { + // Determine if we want to override the default showAIAnalyst value on initialization + const aiAnalystOpenCount = getAiAnalystOpenCount(); + const isSheetEmpty = sheets.sheet.bounds.type === 'empty'; + const showAIAnalyst = aiAnalystOpenCount <= 3 ? true : isSheetEmpty; + if (showAIAnalyst) { + setSelf({ + ...defaultAIAnalystState, + showAIAnalyst, + }); + } + + const user = await getPromise(editorInteractionStateUserAtom); + const uuid = await getPromise(editorInteractionStateUuidAtom); + if (!!user?.email && uuid) { + try { + await aiAnalystOfflineChats.init(user.email, uuid); + const chats = await aiAnalystOfflineChats.loadChats(); + setSelf({ + ...defaultAIAnalystState, + showAIAnalyst, + chats, + }); + } catch (error) { + console.error('[AIAnalystOfflineChats]: ', error); + } + } + } + }, + ({ onSet }) => { + onSet((newValue, oldValue) => { + if (oldValue instanceof DefaultValue) { + return; + } + + if (oldValue.showAIAnalyst && !newValue.showAIAnalyst) { + focusGrid(); + } + }); + }, + ], +}); + +const createSelector = (key: T) => + selector({ + key: `aiAnalyst${key.charAt(0).toUpperCase() + key.slice(1)}Atom`, + get: ({ get }) => get(aiAnalystAtom)[key], + set: ({ set }, newValue) => { + set(aiAnalystAtom, (prev) => ({ + ...prev, + [key]: newValue instanceof DefaultValue ? prev[key] : newValue, + })); + }, + }); +export const showAIAnalystAtom = createSelector('showAIAnalyst'); +export const aiAnalystShowChatHistoryAtom = createSelector('showChatHistory'); +export const aiAnalystAbortControllerAtom = createSelector('abortController'); + +export const aiAnalystLoadingAtom = selector({ + key: 'aiAnalystLoadingAtom', + get: ({ get }) => get(aiAnalystAtom).loading, + set: ({ set }, newValue) => { + set(aiAnalystAtom, (prev) => { + if (newValue instanceof DefaultValue) { + return prev; + } + + if (prev.loading && !newValue) { + // save chat after new message is finished loading + const currentChat = prev.currentChat; + if (currentChat.id) { + aiAnalystOfflineChats.saveChats([currentChat]).catch((error) => { + console.error('[AIAnalystOfflineChats]: ', error); + }); + } + } + + return { + ...prev, + loading: newValue, + }; + }); + }, +}); + +export const aiAnalystChatsAtom = selector({ + key: 'aiAnalystChatsAtom', + get: ({ get }) => get(aiAnalystAtom).chats, + set: ({ set }, newValue) => { + set(aiAnalystAtom, (prev) => { + if (newValue instanceof DefaultValue) { + return prev; + } + + // find deleted chats that are not in the new value + const deletedChatIds = prev.chats + .filter((chat) => !newValue.some((newChat) => newChat.id === chat.id)) + .map((chat) => chat.id); + // delete offline chats + if (deletedChatIds.length > 0) { + aiAnalystOfflineChats.deleteChats(deletedChatIds).catch((error) => { + console.error('[AIAnalystOfflineChats]: ', error); + }); + } + + // find changed chats + const changedChats = newValue.reduce((acc, chat) => { + const prevChat = prev.chats.find((prevChat) => prevChat.id === chat.id); + if (!prevChat) { + acc.push(chat); + } else if ( + prevChat.name !== chat.name || + prevChat.lastUpdated !== chat.lastUpdated || + prevChat.messages !== chat.messages + ) { + acc.push(chat); + } + + return acc; + }, []); + // save changed chats + if (changedChats.length > 0) { + aiAnalystOfflineChats.saveChats(changedChats).catch((error) => { + console.error('[AIAnalystOfflineChats]: ', error); + }); + } + + return { + ...prev, + showChatHistory: newValue.length > 0 ? prev.showChatHistory : false, + chats: newValue, + currentChat: deletedChatIds.includes(prev.currentChat.id) + ? { + id: '', + name: '', + lastUpdated: Date.now(), + messages: [], + } + : prev.currentChat, + }; + }); + }, +}); + +export const aiAnalystChatsCountAtom = selector({ + key: 'aiAnalystChatsCountAtom', + get: ({ get }) => get(aiAnalystChatsAtom).length, +}); + +export const aiAnalystCurrentChatAtom = selector({ + key: 'aiAnalystCurrentChatAtom', + get: ({ get }) => get(aiAnalystAtom).currentChat, + set: ({ set }, newValue) => { + set(aiAnalystAtom, (prev) => { + if (newValue instanceof DefaultValue) { + return prev; + } + + let chats = prev.chats; + if (newValue.id) { + chats = [...chats.filter((chat) => chat.id !== newValue.id), newValue]; + } + + return { + ...prev, + showChatHistory: false, + chats, + currentChat: newValue, + }; + }); + }, +}); + +export const aiAnalystCurrentChatNameAtom = selector({ + key: 'aiAnalystCurrentChatNameAtom', + get: ({ get }) => get(aiAnalystCurrentChatAtom).name, + set: ({ set }, newValue) => { + set(aiAnalystAtom, (prev) => { + if (newValue instanceof DefaultValue) { + return prev; + } + + // update current chat + const currentChat: Chat = { + ...prev.currentChat, + id: !!prev.currentChat.id ? prev.currentChat.id : v4(), + name: newValue, + }; + + // update chats + const chats = [...prev.chats.filter((chat) => chat.id !== currentChat.id), currentChat]; + + // save chat with new name + aiAnalystOfflineChats.saveChats([currentChat]).catch((error) => { + console.error('[AIAnalystOfflineChats]: ', error); + }); + + return { + ...prev, + chats, + currentChat, + }; + }); + }, +}); + +export const aiAnalystCurrentChatMessagesAtom = selector({ + key: 'aiAnalystCurrentChatMessagesAtom', + get: ({ get }) => get(aiAnalystCurrentChatAtom).messages, + set: ({ set }, newValue) => { + set(aiAnalystAtom, (prev) => { + if (newValue instanceof DefaultValue) { + return prev; + } + + // update current chat + const currentChat: Chat = { + id: prev.currentChat.id ? prev.currentChat.id : v4(), + name: prev.currentChat.name, + lastUpdated: Date.now(), + messages: newValue, + }; + + // update chats + const chats = [...prev.chats.filter((chat) => chat.id !== currentChat.id), currentChat]; + + return { + ...prev, + chats, + currentChat, + }; + }); + }, +}); + +export const aiAnalystCurrentChatMessagesCountAtom = selector({ + key: 'aiAnalystCurrentChatMessagesCountAtom', + get: ({ get }) => getPromptMessages(get(aiAnalystCurrentChatAtom).messages).length, +}); + +const STORAGE_KEY = 'aiAnalystOpenCount'; +export function getAiAnalystOpenCount() { + const count = window.localStorage.getItem(STORAGE_KEY); + return count ? parseInt(count) : 0; +} +export function incrementAiAnalystOpenCount() { + const count = getAiAnalystOpenCount(); + const newCount = count + 1; + window.localStorage.setItem(STORAGE_KEY, newCount.toString()); +} diff --git a/quadratic-client/src/app/atoms/codeEditorAtom.ts b/quadratic-client/src/app/atoms/codeEditorAtom.ts index eea4c06c1c..ba00a5a62d 100644 --- a/quadratic-client/src/app/atoms/codeEditorAtom.ts +++ b/quadratic-client/src/app/atoms/codeEditorAtom.ts @@ -1,11 +1,12 @@ +import { getPromptMessages } from '@/app/ai/tools/message.helper'; +import { events } from '@/app/events/events'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { CodeCell } from '@/app/gridGL/types/codeCell'; -import { Coordinate } from '@/app/gridGL/types/size'; import { focusGrid } from '@/app/helpers/focusGrid'; -import { SheetRect } from '@/app/quadratic-core-types'; +import { JsCellsAccessed, JsCoordinate } from '@/app/quadratic-core-types'; import { PanelTab } from '@/app/ui/menus/CodeEditor/panels/CodeEditorPanelBottom'; import { EvaluationResult } from '@/app/web-workers/pythonWebWorker/pythonTypes'; -import { AIMessage, UserMessage } from 'quadratic-shared/typesAndSchemasAI'; +import { ChatMessage } from 'quadratic-shared/typesAndSchemasAI'; import { atom, DefaultValue, selector } from 'recoil'; export interface ConsoleOutput { @@ -17,8 +18,7 @@ export interface CodeEditorState { aiAssistant: { abortController?: AbortController; loading: boolean; - messages: (UserMessage | AIMessage)[]; - prompt: string; + messages: ChatMessage[]; }; showCodeEditor: boolean; escapePressed: boolean; @@ -27,13 +27,17 @@ export interface CodeEditorState { codeString?: string; evaluationResult?: EvaluationResult; consoleOutput?: ConsoleOutput; - spillError?: Coordinate[]; + spillError?: JsCoordinate[]; panelBottomActiveTab: PanelTab; showSnippetsPopover: boolean; initialCode?: string; editorContent?: string; + diffEditorContent?: { + editorContent?: string; + isApplied: boolean; + }; showSaveChangesAlert: boolean; - cellsAccessed: SheetRect[] | undefined | null; + cellsAccessed: JsCellsAccessed[] | null; waitingForEditorClose?: { codeCell: CodeCell; showCellTypeMenu: boolean; @@ -47,7 +51,6 @@ export const defaultCodeEditorState: CodeEditorState = { abortController: undefined, loading: false, messages: [], - prompt: '', }, showCodeEditor: false, escapePressed: false, @@ -65,35 +68,65 @@ export const defaultCodeEditorState: CodeEditorState = { showSnippetsPopover: false, initialCode: undefined, editorContent: undefined, + diffEditorContent: undefined, showSaveChangesAlert: false, - cellsAccessed: undefined, + cellsAccessed: null, waitingForEditorClose: undefined, }; export const codeEditorAtom = atom({ key: 'codeEditorAtom', default: defaultCodeEditorState, + effects: [ + ({ onSet, resetSelf }) => { + onSet((newValue, oldValue) => { + if (oldValue instanceof DefaultValue) { + return; + } + + if (newValue.showCodeEditor) { + if ( + newValue.codeCell.sheetId !== oldValue.codeCell.sheetId || + newValue.codeCell.pos.x !== oldValue.codeCell.pos.x || + newValue.codeCell.pos.y !== oldValue.codeCell.pos.y || + newValue.codeCell.language !== oldValue.codeCell.language + ) { + events.emit('codeEditorCodeCell', newValue.codeCell); + } + } + + if (oldValue.showCodeEditor && !newValue.showCodeEditor) { + events.emit('codeEditorCodeCell', undefined); + resetSelf(); + focusGrid(); + } + }); + }, + ], }); -export const codeEditorShowCodeEditorAtom = selector({ - key: 'codeEditorShowCodeEditorAtom', - get: ({ get }) => get(codeEditorAtom)['showCodeEditor'], - set: ({ set }, newValue) => - set(codeEditorAtom, (prev) => { - if (prev.showCodeEditor && !newValue) { - focusGrid(); - } - if (!newValue) { - return defaultCodeEditorState; - } - return { +const createAIAssistantSelector = (key: T) => + selector({ + key: `codeEditorAIAssistant${key.charAt(0).toUpperCase() + key.slice(1)}Atom`, + get: ({ get }) => get(codeEditorAtom).aiAssistant[key], + set: ({ set }, newValue) => + set(codeEditorAtom, (prev) => ({ ...prev, - showCodeEditor: newValue instanceof DefaultValue ? prev['showCodeEditor'] : newValue, - }; - }), + aiAssistant: { + ...prev.aiAssistant, + [key]: newValue instanceof DefaultValue ? prev.aiAssistant[key] : newValue, + }, + })), + }); +export const aiAssistantAbortControllerAtom = createAIAssistantSelector('abortController'); +export const aiAssistantLoadingAtom = createAIAssistantSelector('loading'); +export const aiAssistantMessagesAtom = createAIAssistantSelector('messages'); +export const aiAssistantCurrentChatMessagesCountAtom = selector({ + key: 'aiAssistantCurrentChatMessagesCountAtom', + get: ({ get }) => getPromptMessages(get(aiAssistantMessagesAtom)).length, }); -const createSelector = (key: T) => +const createCodeEditorSelector = (key: T) => selector({ key: `codeEditor${key.charAt(0).toUpperCase() + key.slice(1)}Atom`, get: ({ get }) => get(codeEditorAtom)[key], @@ -103,41 +136,85 @@ const createSelector = (key: T) => [key]: newValue instanceof DefaultValue ? prev[key] : newValue, })), }); -export const codeEditorEscapePressedAtom = createSelector('escapePressed'); -export const codeEditorLoadingAtom = createSelector('loading'); -export const codeEditorCodeCellAtom = createSelector('codeCell'); -export const codeEditorCodeStringAtom = createSelector('codeString'); -export const codeEditorEvaluationResultAtom = createSelector('evaluationResult'); -export const codeEditorConsoleOutputAtom = createSelector('consoleOutput'); -export const codeEditorSpillErrorAtom = createSelector('spillError'); -export const codeEditorPanelBottomActiveTabAtom = createSelector('panelBottomActiveTab'); -export const codeEditorShowSnippetsPopoverAtom = createSelector('showSnippetsPopover'); -export const codeEditorInitialCodeAtom = createSelector('initialCode'); -export const codeEditorEditorContentAtom = createSelector('editorContent'); -export const codeEditorShowSaveChangesAlertAtom = createSelector('showSaveChangesAlert'); -export const codeEditorCellsAccessedAtom = createSelector('cellsAccessed'); -export const codeEditorWaitingForEditorClose = createSelector('waitingForEditorClose'); +export const codeEditorShowCodeEditorAtom = createCodeEditorSelector('showCodeEditor'); +export const codeEditorEscapePressedAtom = createCodeEditorSelector('escapePressed'); +export const codeEditorLoadingAtom = createCodeEditorSelector('loading'); +export const codeEditorCodeCellAtom = createCodeEditorSelector('codeCell'); +export const codeEditorCodeStringAtom = createCodeEditorSelector('codeString'); +export const codeEditorEvaluationResultAtom = createCodeEditorSelector('evaluationResult'); +export const codeEditorConsoleOutputAtom = createCodeEditorSelector('consoleOutput'); +export const codeEditorSpillErrorAtom = createCodeEditorSelector('spillError'); +export const codeEditorPanelBottomActiveTabAtom = createCodeEditorSelector('panelBottomActiveTab'); +export const codeEditorShowSnippetsPopoverAtom = createCodeEditorSelector('showSnippetsPopover'); +export const codeEditorInitialCodeAtom = createCodeEditorSelector('initialCode'); +export const codeEditorDiffEditorContentAtom = createCodeEditorSelector('diffEditorContent'); +export const codeEditorShowSaveChangesAlertAtom = createCodeEditorSelector('showSaveChangesAlert'); +export const codeEditorCellsAccessedAtom = createCodeEditorSelector('cellsAccessed'); +export const codeEditorWaitingForEditorClose = createCodeEditorSelector('waitingForEditorClose'); + +export const codeEditorEditorContentAtom = selector({ + key: 'codeEditorEditorContentAtom', + get: ({ get }) => get(codeEditorAtom).editorContent, + set: ({ set }, newValue) => + set(codeEditorAtom, (prev) => { + if (newValue instanceof DefaultValue) { + return { ...prev, diffEditorContent: undefined }; + } + + return { + ...prev, + diffEditorContent: undefined, + editorContent: newValue, + }; + }), +}); + +export const codeEditorShowDiffEditorAtom = selector({ + key: 'codeEditorShowDiffEditorAtom', + get: ({ get }) => { + const { waitingForEditorClose, diffEditorContent, editorContent } = get(codeEditorAtom); + + return ( + waitingForEditorClose === undefined && + diffEditorContent !== undefined && + !!editorContent && + !!diffEditorContent.editorContent && + diffEditorContent.editorContent !== editorContent + ); + }, +}); export const codeEditorUnsavedChangesAtom = selector({ key: 'codeEditorUnsavedChangesAtom', get: ({ get }) => { - const unsavedChanges = get(codeEditorAtom).editorContent !== get(codeEditorAtom).codeString; - pixiAppSettings.unsavedEditorChanges = unsavedChanges ? get(codeEditorAtom).editorContent : undefined; + const { editorContent, codeString } = get(codeEditorAtom); + const unsavedChanges = editorContent !== codeString; + + if (unsavedChanges) { + pixiAppSettings.unsavedEditorChanges = editorContent; + } else { + pixiAppSettings.unsavedEditorChanges = undefined; + } + return unsavedChanges; }, }); -const createAIAssistantSelector = (key: T) => - selector({ - key: `codeEditorAIAssistant${key.charAt(0).toUpperCase() + key.slice(1)}Atom`, - get: ({ get }) => get(codeEditorAtom).aiAssistant[key], - set: ({ set }, newValue) => - set(codeEditorAtom, (prev) => ({ - ...prev, - aiAssistant: { ...prev.aiAssistant, [key]: newValue }, - })), - }); -export const codeEditorAIAssistantAbortControllerAtom = createAIAssistantSelector('abortController'); -export const codeEditorAIAssistantLoadingAtom = createAIAssistantSelector('loading'); -export const codeEditorAIAssistantMessagesAtom = createAIAssistantSelector('messages'); -export const codeEditorAIAssistantPromptAtom = createAIAssistantSelector('prompt'); +export const showAIAssistantAtom = selector({ + key: 'showAIAssistantAtom', + get: ({ get }) => { + const codeEditorState = get(codeEditorAtom); + return codeEditorState.showCodeEditor && codeEditorState.panelBottomActiveTab === 'ai-assistant'; + }, + set: ({ set }, newValue) => { + if (newValue instanceof DefaultValue) { + return; + } + + set(codeEditorAtom, (prev) => ({ + ...prev, + showCodeEditor: newValue, + panelBottomActiveTab: newValue ? 'ai-assistant' : prev.panelBottomActiveTab, + })); + }, +}); diff --git a/quadratic-client/src/app/atoms/codeHintAtom.ts b/quadratic-client/src/app/atoms/codeHintAtom.ts index f137abc7fe..ac9f694949 100644 --- a/quadratic-client/src/app/atoms/codeHintAtom.ts +++ b/quadratic-client/src/app/atoms/codeHintAtom.ts @@ -12,7 +12,7 @@ interface CodeHintState { } const defaultCodeHintState: CodeHintState = { - sheetEmpty: sheets.sheet.bounds.type === 'empty', + sheetEmpty: sheets.initialized ? sheets.sheet.bounds.type === 'empty' : false, multipleSelection: false, }; @@ -22,8 +22,7 @@ export const codeHintAtom = atom({ effects: [ ({ setSelf }) => { const updateMultipleSelection = () => { - const multipleSelection = - sheets.sheet.cursor.multiCursor !== undefined || sheets.sheet.cursor.columnRow !== undefined; + const multipleSelection = sheets.sheet.cursor.isMultiCursor(); setSelf((prev) => { if (prev instanceof DefaultValue) return prev; return { ...prev, multipleSelection, sheetEmpty: sheets.sheet.bounds.type === 'empty' }; diff --git a/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts b/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts index 2aed2450f5..c210751699 100644 --- a/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts +++ b/quadratic-client/src/app/atoms/editorInteractionStateAtom.ts @@ -1,5 +1,6 @@ import { focusGrid } from '@/app/helpers/focusGrid.js'; import { SearchOptions } from '@/app/quadratic-core-types'; +import { User } from '@/auth/auth'; import { FilePermission } from 'quadratic-shared/typesAndSchemas'; import { atom, DefaultValue, selector } from 'recoil'; @@ -10,7 +11,6 @@ export interface EditorInteractionState { showConnectionsMenu: boolean; showGoToMenu: boolean; showFeedbackMenu: boolean; - showNewFileMenu: boolean; showRenameFileMenu: boolean; showShareFileMenu: boolean; showSearch: boolean | SearchOptions; @@ -18,20 +18,20 @@ export interface EditorInteractionState { showValidation: boolean | string; annotationState?: 'dropdown' | 'date-format' | 'calendar' | 'calendar-time'; permissions: FilePermission[]; + user?: User; uuid: string; follow?: string; undo: boolean; redo: boolean; } -export const editorInteractionStateDefault: EditorInteractionState = { +export const defaultEditorInteractionState: EditorInteractionState = { isRunningAsyncAction: false, showCellTypeMenu: false, showCommandPalette: false, showConnectionsMenu: false, showGoToMenu: false, showFeedbackMenu: false, - showNewFileMenu: false, showRenameFileMenu: false, showShareFileMenu: false, showSearch: false, @@ -39,6 +39,7 @@ export const editorInteractionStateDefault: EditorInteractionState = { showValidation: false, annotationState: undefined, permissions: ['FILE_VIEW'], // FYI: when we call we initialize this with the value from the server + user: undefined, uuid: '', // when we call we initialize this with the value from the server follow: undefined, undo: false, @@ -47,7 +48,7 @@ export const editorInteractionStateDefault: EditorInteractionState = { export const editorInteractionStateAtom = atom({ key: 'editorInteractionState', // unique ID (with respect to other atoms/selectors) - default: editorInteractionStateDefault, + default: defaultEditorInteractionState, effects: [ // this effect is used to focus the grid when the modal is closed ({ onSet }) => { @@ -59,7 +60,6 @@ export const editorInteractionStateAtom = atom({ oldValue.showConnectionsMenu || oldValue.showGoToMenu || oldValue.showFeedbackMenu || - oldValue.showNewFileMenu || oldValue.showRenameFileMenu || oldValue.showShareFileMenu || oldValue.showSearch || @@ -70,7 +70,6 @@ export const editorInteractionStateAtom = atom({ newValue.showConnectionsMenu || newValue.showGoToMenu || newValue.showFeedbackMenu || - newValue.showNewFileMenu || newValue.showRenameFileMenu || newValue.showShareFileMenu || newValue.showSearch || @@ -100,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'); @@ -109,6 +107,7 @@ export const editorInteractionStateShowValidationAtom = createSelector('showVali export const editorInteractionStateAnnotationStateAtom = createSelector('annotationState'); export const editorInteractionStatePermissionsAtom = createSelector('permissions'); +export const editorInteractionStateUserAtom = createSelector('user'); export const editorInteractionStateUuidAtom = createSelector('uuid'); export const editorInteractionStateFollowAtom = createSelector('follow'); export const editorInteractionStateUndoAtom = createSelector('undo'); diff --git a/quadratic-client/src/app/atoms/gridSettingsAtom.ts b/quadratic-client/src/app/atoms/gridSettingsAtom.ts index 560a61e926..1763966275 100644 --- a/quadratic-client/src/app/atoms/gridSettingsAtom.ts +++ b/quadratic-client/src/app/atoms/gridSettingsAtom.ts @@ -5,18 +5,16 @@ import { AtomEffect, DefaultValue, atom, selector } from 'recoil'; const SETTINGS_KEY = 'viewSettings'; -export interface GridSettings { - showGridAxes: boolean; +export type GridSettings = { showHeadings: boolean; showGridLines: boolean; showCellTypeOutlines: boolean; showA1Notation: boolean; showCodePeek: boolean; presentationMode: boolean; -} +}; export const defaultGridSettings: GridSettings = { - showGridAxes: true, showHeadings: true, showGridLines: true, showCellTypeOutlines: true, @@ -72,7 +70,6 @@ const createSelector = (key: T) => }, }); -export const showGridAxesAtom = createSelector('showGridAxes'); export const showHeadingsAtom = createSelector('showHeadings'); export const showGridLinesAtom = createSelector('showGridLines'); export const showCellTypeOutlinesAtom = createSelector('showCellTypeOutlines'); diff --git a/quadratic-client/src/app/atoms/inlineEditorAtom.ts b/quadratic-client/src/app/atoms/inlineEditorAtom.ts index eb17a32cef..526371575c 100644 --- a/quadratic-client/src/app/atoms/inlineEditorAtom.ts +++ b/quadratic-client/src/app/atoms/inlineEditorAtom.ts @@ -1,4 +1,7 @@ +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'; import { atom } from 'recoil'; export interface InlineEditorState { @@ -6,7 +9,8 @@ export interface InlineEditorState { formula: boolean; left: number; top: number; - lineHeight: number; + height: number; + editMode: boolean; } export const defaultInlineEditor: InlineEditorState = { @@ -14,7 +18,8 @@ export const defaultInlineEditor: InlineEditorState = { formula: false, left: 0, top: 0, - lineHeight: 19, + height: LINE_HEIGHT, + editMode: false, }; export const inlineEditorAtom = atom({ @@ -26,6 +31,8 @@ export const inlineEditorAtom = atom({ if (newValue.visible) { inlineEditorMonaco.focus(); } + inlineEditorKeyboard.cursorMode = newValue.editMode ? CursorMode.Edit : CursorMode.Enter; + pixiApp.cursor.dirty = true; }); }, ], diff --git a/quadratic-client/src/app/bigint.ts b/quadratic-client/src/app/bigint.ts new file mode 100644 index 0000000000..ac813ec3a9 --- /dev/null +++ b/quadratic-client/src/app/bigint.ts @@ -0,0 +1,10 @@ +// Used to coerce bigints to numbers for JSON.stringify; see +// https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-2064279949. +export const bigIntReplacer = (_key: string, value: any): any => { + return typeof value === 'bigint' ? Number(value) : value; +}; + +// export const parseWithBigInt = (jsonString, bigNumChecker) => +// JSON.parse(enquoteBigNumber(jsonString, bigNumChecker), (key, value) => +// !isNaN(value) && bigNumChecker(value) ? BigInt(value) : value +// ); diff --git a/quadratic-client/src/app/debugFlags.ts b/quadratic-client/src/app/debugFlags.ts index 933010b08d..2399a29d32 100644 --- a/quadratic-client/src/app/debugFlags.ts +++ b/quadratic-client/src/app/debugFlags.ts @@ -80,3 +80,9 @@ export const debugShowUILogs = debug && false; export const debugWebWorkers = debug && false; export const debugWebWorkersMessages = debug && false; + +// ----------- +// AI +// ----------- + +export const debugShowAIInternalContext = debug && false; diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 5e758277b1..21b5e23cef 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -1,6 +1,8 @@ import { ContextMenuOptions } from '@/app/atoms/contextMenuAtom'; import { ErrorValidation } from '@/app/gridGL/cells/CellsSheet'; import { EditingCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell'; +import { CursorMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { CodeCell } from '@/app/gridGL/types/codeCell'; import { SheetPosTS } from '@/app/gridGL/types/size'; import { JsBordersSheet, @@ -12,7 +14,6 @@ import { JsRenderFill, JsSheetFill, JsValidationWarning, - Selection, SheetBounds, SheetInfo, Validation, @@ -49,16 +50,16 @@ interface EventTypes { changeSheet: (sheetId: string) => void; sheetBounds: (sheetBounds: SheetBounds) => void; - setCursor: (cursor?: string, selection?: Selection) => void; + setCursor: (selection?: string) => void; cursorPosition: () => void; generateThumbnail: () => void; - changeInput: (input: boolean, initialValue?: string) => void; + changeInput: (input: boolean, initialValue?: string, cursorMode?: CursorMode) => void; headingSize: (width: number, height: number) => void; gridSettings: () => void; sheetOffsets: (sheetId: string, offsets: JsOffset[]) => void; sheetFills: (sheetId: string, fills: JsRenderFill[]) => void; - sheetMetaFills: (sheetId: string, fills: JsSheetFill) => void; + sheetMetaFills: (sheetId: string, fills: JsSheetFill[]) => void; htmlOutput: (html: JsHtmlOutput[]) => void; htmlUpdate: (html: JsHtmlOutput) => void; bordersSheet: (sheetId: string, borders?: JsBordersSheet) => void; @@ -136,6 +137,12 @@ interface EventTypes { // use this only if you need to immediately get the viewport's value (ie, from React) viewportChangedReady: () => void; +<<<<<<< HEAD +======= + hashContentChanged: (sheetId: string, hashX: number, hashY: number) => void; + + codeEditorCodeCell: (codeCell?: CodeCell) => void; +>>>>>>> origin/qa } export const events = new EventEmitter(); diff --git a/quadratic-client/src/app/grid/actions/clipboard/clipboard.ts b/quadratic-client/src/app/grid/actions/clipboard/clipboard.ts index 1ec94d9e52..2e7e433fea 100644 --- a/quadratic-client/src/app/grid/actions/clipboard/clipboard.ts +++ b/quadratic-client/src/app/grid/actions/clipboard/clipboard.ts @@ -29,8 +29,8 @@ export const copyToClipboardEvent = async (e: ClipboardEvent) => { if (!canvasIsTarget()) return; e.preventDefault(); debugTimeReset(); - const { plainText, html } = await quadraticCore.copyToClipboard(sheets.getRustSelection()); - toClipboard(plainText, html); + const jsClipboard = await quadraticCore.copyToClipboard(sheets.getRustSelection()); + await toClipboard(jsClipboard.plainText, jsClipboard.html); debugTimeCheck('copy to clipboard'); }; @@ -39,8 +39,8 @@ export const cutToClipboardEvent = async (e: ClipboardEvent) => { if (!hasPermissionToEditFile(pixiAppSettings.permissions)) return; e.preventDefault(); debugTimeReset(); - const { plainText, html } = await quadraticCore.cutToClipboard(sheets.getRustSelection(), sheets.getCursorPosition()); - toClipboard(plainText, html); + const jsClipboard = await quadraticCore.cutToClipboard(sheets.getRustSelection(), sheets.getCursorPosition()); + await toClipboard(jsClipboard.plainText, jsClipboard.html); debugTimeCheck('[Clipboard] cut to clipboard'); }; @@ -64,8 +64,7 @@ export const pasteFromClipboardEvent = (e: ClipboardEvent) => { } if (plainText || html) { quadraticCore.pasteFromClipboard({ - sheetId: sheets.sheet.id, - selection: sheets.sheet.cursor.getRustSelection(), + selection: sheets.sheet.cursor.save(), plainText, html, special: 'None', @@ -82,11 +81,11 @@ export const pasteFromClipboardEvent = (e: ClipboardEvent) => { //#region triggered via menu (limited support on Firefox) // copies plainText and html to the clipboard -const toClipboard = (plainText: string, html: string) => { +const toClipboard = async (plainText: string, html: string) => { // https://github.com/tldraw/tldraw/blob/a85e80961dd6f99ccc717749993e10fa5066bc4d/packages/tldraw/src/state/TldrawApp.ts#L2189 // browser support clipboard api navigator.clipboard if (fullClipboardSupport()) { - navigator.clipboard.write([ + await navigator.clipboard.write([ new ClipboardItem({ 'text/html': new Blob([html], { type: 'text/html' }), 'text/plain': new Blob([plainText], { type: 'text/plain' }), @@ -96,23 +95,22 @@ const toClipboard = (plainText: string, html: string) => { // fallback support for firefox else { - navigator.clipboard.writeText(plainText); - localforage.setItem(clipboardLocalStorageKey, html); + await Promise.all([navigator.clipboard.writeText(plainText), localforage.setItem(clipboardLocalStorageKey, html)]); } }; export const cutToClipboard = async () => { if (!hasPermissionToEditFile(pixiAppSettings.permissions)) return; debugTimeReset(); - const { plainText, html } = await quadraticCore.cutToClipboard(sheets.getRustSelection(), sheets.getCursorPosition()); - toClipboard(plainText, html); + const jsClipboard = await quadraticCore.cutToClipboard(sheets.getRustSelection(), sheets.getCursorPosition()); + await toClipboard(jsClipboard.plainText, jsClipboard.html); debugTimeCheck('cut to clipboard (fallback)'); }; export const copyToClipboard = async () => { debugTimeReset(); - const { plainText, html } = await quadraticCore.copyToClipboard(sheets.getRustSelection()); - toClipboard(plainText, html); + const jsClipboard = await quadraticCore.copyToClipboard(sheets.getRustSelection()); + await toClipboard(jsClipboard.plainText, jsClipboard.html); debugTimeCheck('copy to clipboard'); }; @@ -165,8 +163,7 @@ export const pasteFromClipboard = async (special: PasteSpecial = 'None') => { html = await item.text(); } quadraticCore.pasteFromClipboard({ - sheetId: sheets.sheet.id, - selection: sheets.sheet.cursor.getRustSelection(), + selection: sheets.sheet.cursor.save(), plainText, html, special, @@ -179,8 +176,7 @@ export const pasteFromClipboard = async (special: PasteSpecial = 'None') => { const html = (await localforage.getItem(clipboardLocalStorageKey)) as string; if (html) { quadraticCore.pasteFromClipboard({ - sheetId: sheets.sheet.id, - selection: sheets.sheet.cursor.getRustSelection(), + selection: sheets.sheet.cursor.save(), plainText: undefined, html, special, diff --git a/quadratic-client/src/app/grid/actions/openCodeEditor.ts b/quadratic-client/src/app/grid/actions/openCodeEditor.ts index 9782558ec7..7defdcf97d 100644 --- a/quadratic-client/src/app/grid/actions/openCodeEditor.ts +++ b/quadratic-client/src/app/grid/actions/openCodeEditor.ts @@ -12,7 +12,7 @@ export const openCodeEditor = async () => { throw new Error('Expected setEditorInteractionState to be defined in openCodeEditor'); } - const { x, y } = sheets.sheet.cursor.cursorPosition; + const { x, y } = sheets.sheet.cursor.position; const codeCell = await quadraticCore.getCodeCell(sheets.sheet.id, x, y); if (codeCell) { const { @@ -43,6 +43,7 @@ export const openCodeEditor = async () => { // this will also open the save changes modal if there are unsaved changes setCodeEditorState({ ...codeEditorState, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -58,6 +59,7 @@ export const openCodeEditor = async () => { // code editor is already open, so check it for save before closing setCodeEditorState({ ...codeEditorState, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -76,6 +78,7 @@ export const openCodeEditor = async () => { })); setCodeEditorState({ ...codeEditorState, + diffEditorContent: undefined, initialCode: '', codeCell: { sheetId: sheets.current, diff --git a/quadratic-client/src/app/grid/computations/formulas/runFormula.ts b/quadratic-client/src/app/grid/computations/formulas/runFormula.ts deleted file mode 100644 index 42bcb81ce2..0000000000 --- a/quadratic-client/src/app/grid/computations/formulas/runFormula.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Coordinate } from '../../../gridGL/types/size'; - -// TODO: Delete this file - -export interface runFormulaReturnType { - cells_accessed: [number, number][]; - success: boolean; - error_span: [number, number] | null; - error_msg: string | null; - output_value: string | null; - array_output: string[][] | null; -} - -export async function runFormula(formula_code: string, pos: Coordinate): Promise { - // const output = await eval_formula(formula_code, pos.x, pos.y, GetCellsDB); - return {} as runFormulaReturnType; -} diff --git a/quadratic-client/src/app/grid/computations/types.ts b/quadratic-client/src/app/grid/computations/types.ts deleted file mode 100644 index 29cffecf94..0000000000 --- a/quadratic-client/src/app/grid/computations/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import z from 'zod'; -import { ArrayOutputSchema } from '../../schemas'; - -export const CellEvaluationResultSchema = z.object({ - success: z.boolean(), - std_out: z.string().optional(), - std_err: z.string().optional(), - output_type: z.string().or(z.null()).or(z.undefined()), - output_value: z.string().or(z.null()).or(z.undefined()), - cells_accessed: z.tuple([z.number(), z.number()]).array(), - array_output: ArrayOutputSchema, - formatted_code: z.string(), - error_span: z.tuple([z.number(), z.number()]).or(z.null()), -}); - -export type CellEvaluationResult = z.infer; diff --git a/quadratic-client/src/app/grid/controller/Sheets.ts b/quadratic-client/src/app/grid/controller/Sheets.ts index d989184d6d..3a3e73fb41 100644 --- a/quadratic-client/src/app/grid/controller/Sheets.ts +++ b/quadratic-client/src/app/grid/controller/Sheets.ts @@ -1,12 +1,16 @@ import { events } from '@/app/events/events'; +import { getRectSelection } from '@/app/grid/sheet/selection'; import { Sheet } from '@/app/grid/sheet/Sheet'; -import { SheetCursorSave } from '@/app/grid/sheet/SheetCursor'; +import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { JsOffset, Selection, SheetInfo } from '@/app/quadratic-core-types'; +import { A1Selection, JsOffset, Rect, SheetInfo } from '@/app/quadratic-core-types'; +import { JsSelection } from '@/app/quadratic-rust-client/quadratic_rust_client'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { rectToRectangle } from '@/app/web-workers/quadraticCore/worker/rustConversions'; import { Rectangle } from 'pixi.js'; class Sheets { + initialized: boolean; sheets: Sheet[]; // current sheet id @@ -24,6 +28,7 @@ class Sheets { events.on('sheetInfoUpdate', this.updateSheet); events.on('setCursor', this.setCursor); events.on('sheetOffsets', this.updateOffsets); + this.initialized = false; } private create = (sheetInfo: SheetInfo[]) => { @@ -35,6 +40,7 @@ class Sheets { this.sort(); this._current = this.sheets[0].id; pixiApp.cellsSheets.create(); + this.initialized = true; }; private addSheet = (sheetInfo: SheetInfo, user: boolean) => { @@ -98,34 +104,19 @@ class Sheets { pixiApp.multiplayerCursor.dirty = true; }; - private setCursor = (cursorStringified?: string, selection?: Selection) => { + private setCursor = (selection?: string) => { if (selection !== undefined) { - this.sheet.cursor.loadFromSelection(selection); - } else if (cursorStringified !== undefined) { - const cursor: SheetCursorSave = JSON.parse(cursorStringified); - if (cursor.sheetId !== this.current) { - this.current = cursor.sheetId; + try { + const a1Selection = JSON.parse(selection) as A1Selection; + const sheetId = a1Selection.sheet_id.id; + const sheet = this.getById(sheetId); + if (sheet) { + this.current = sheetId; + sheet.cursor.load(selection); + } + } catch (e) { + console.error('Error loading cursor', e); } - // need to convert from old style multiCursor from Rust to new style - if ((cursor.multiCursor as any)?.originPosition) { - const multiCursor = cursor.multiCursor as any; - const convertedCursor = { - ...cursor, - multiCursor: [ - new Rectangle( - multiCursor.originPosition.x, - multiCursor.originPosition.y, - multiCursor.terminalPosition.x - multiCursor.originPosition.x + 1, - multiCursor.terminalPosition.y - multiCursor.originPosition.y + 1 - ), - ], - }; - this.sheet.cursor.load(convertedCursor); - } else { - this.sheet.cursor.load(cursor); - } - } else { - throw new Error('Expected setCursor in Sheets.ts to have cursorStringified or selection defined'); } }; @@ -174,7 +165,6 @@ class Sheets { this._current = value; pixiApp.viewport.dirty = true; pixiApp.gridLines.dirty = true; - pixiApp.axesLines.dirty = true; pixiApp.headings.dirty = true; pixiApp.cursor.dirty = true; pixiApp.multiplayerCursor.dirty = true; @@ -268,11 +258,11 @@ class Sheets { } userAddSheet() { - quadraticCore.addSheet(sheets.getCursorPosition()); + quadraticCore.addSheet(this.getCursorPosition()); } duplicate() { - quadraticCore.duplicateSheet(this.current, sheets.getCursorPosition()); + quadraticCore.duplicateSheet(this.current, this.getCursorPosition()); } userDeleteSheet(id: string) { @@ -311,16 +301,80 @@ class Sheets { } getCursorPosition(): string { - return JSON.stringify(this.sheet.cursor.save()); + return this.sheet.cursor.save(); } getMultiplayerSelection(): string { - return this.sheet.cursor.getMultiplayerSelection(); + return this.sheet.cursor.save(); } - getRustSelection(): Selection { - return this.sheet.cursor.getRustSelection(); - } + getA1String = (sheetId = this.current): string => { + return this.sheet.cursor.jsSelection.toA1String(sheetId, this.getSheetIdNameMap()); + }; + + /// Gets a stringified SheetIdNameMap for Rust's A1 functions + getSheetIdNameMap = (): string => { + const sheetMap: Record = {}; + this.sheets.forEach((sheet) => (sheetMap[sheet.name] = { id: sheet.id })); + return JSON.stringify(sheetMap); + }; + + // Changes the cursor to the incoming selection + changeSelection = (jsSelection: JsSelection, ensureVisible = true) => { + // change the sheet id if needed + const sheetId = jsSelection.getSheetId(); + if (sheetId !== this.current) { + if (this.getById(sheetId)) { + this.current = sheetId; + } + } + + const cursor = this.sheet.cursor; + cursor.loadFromSelection(jsSelection); + cursor.updatePosition(true); + }; + + getRustSelection = (): string => { + return this.sheet.cursor.save(); + }; + + getVisibleRect = (): Rect => { + const { left, top, right, bottom } = pixiApp.viewport.getVisibleBounds(); + const scale = pixiApp.viewport.scale.x; + let { width: leftHeadingWidth, height: topHeadingHeight } = pixiApp.headings.headingSize; + leftHeadingWidth /= scale; + topHeadingHeight /= scale; + const top_left_cell = this.sheet.getColumnRow(left + 1 + leftHeadingWidth, top + 1 + topHeadingHeight); + const bottom_right_cell = this.sheet.getColumnRow(right, bottom); + return { + min: { x: BigInt(top_left_cell.x), y: BigInt(top_left_cell.y) }, + max: { x: BigInt(bottom_right_cell.x), y: BigInt(bottom_right_cell.y) }, + }; + }; + + getVisibleRectangle = (): Rectangle => { + const visibleRect = this.getVisibleRect(); + return rectToRectangle(visibleRect); + }; + + getVisibleSelection = (): string | undefined => { + const sheetBounds = this.sheet.boundsWithoutFormatting; + if (sheetBounds.type === 'empty') { + return undefined; + } + + const sheetBoundsRect: Rect = { + min: sheetBounds.min, + max: sheetBounds.max, + }; + const visibleRect = this.getVisibleRect(); + if (!intersects.rectRect(sheetBoundsRect, visibleRect)) { + return undefined; + } + + const visibleRectSelection = getRectSelection(this.current, visibleRect); + return visibleRectSelection; + }; } export const sheets = new Sheets(); diff --git a/quadratic-client/src/app/grid/sheet/Bounds.ts b/quadratic-client/src/app/grid/sheet/Bounds.ts index fdc1b61e65..8a002a80bd 100644 --- a/quadratic-client/src/app/grid/sheet/Bounds.ts +++ b/quadratic-client/src/app/grid/sheet/Bounds.ts @@ -1,5 +1,5 @@ +import { JsCoordinate } from '@/app/quadratic-core-types'; import { Rectangle } from 'pixi.js'; -import { Coordinate } from '../../gridGL/types/size'; export class Bounds { empty = true; @@ -56,7 +56,7 @@ export class Bounds { this.empty = false; } - addCoordinate(coordinate: Coordinate): void { + addCoordinate(coordinate: JsCoordinate): void { this.add(coordinate.x, coordinate.y); } @@ -99,7 +99,7 @@ export class Bounds { ); } - containsCoordinate(coordinate: Coordinate): boolean { + containsCoordinate(coordinate: JsCoordinate): boolean { return this.contains(coordinate.x, coordinate.y); } diff --git a/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts b/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts index 4f9552ccc3..34602b1ded 100644 --- a/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts +++ b/quadratic-client/src/app/grid/sheet/GridOverflowLines.ts @@ -1,10 +1,14 @@ //! Keeps track of which grid lines should not be drawn within the sheet because //! of overflow of text, images, and html tables.. +<<<<<<< HEAD import { Sheet } from '@/app/grid/sheet/Sheet'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Coordinate } from '@/app/gridGL/types/size'; import { Rectangle } from 'pixi.js'; +======= +import { JsCoordinate } from '@/app/quadratic-core-types'; +>>>>>>> origin/qa export class GridOverflowLines { private sheet: Sheet; @@ -22,7 +26,7 @@ export class GridOverflowLines { } // Updates a hash with a list of overflow lines - updateHash(hashKey: string, coordinates: Coordinate[]) { + updateHash(hashKey: string, coordinates: JsCoordinate[]) { // first remove all overflowLines from this hash this.overflowLines.forEach((value, key) => { if (value === hashKey) { diff --git a/quadratic-client/src/app/grid/sheet/Sheet.ts b/quadratic-client/src/app/grid/sheet/Sheet.ts index 99e35a5921..8e6d1e771c 100644 --- a/quadratic-client/src/app/grid/sheet/Sheet.ts +++ b/quadratic-client/src/app/grid/sheet/Sheet.ts @@ -1,13 +1,11 @@ import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; import { GridOverflowLines } from '@/app/grid/sheet/GridOverflowLines'; -import { intersects } from '@/app/gridGL/helpers/intersects'; -import { ColumnRow, GridBounds, SheetBounds, SheetInfo, Validation } from '@/app/quadratic-core-types'; -import { SheetOffsets, SheetOffsetsWasm } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { SheetCursor } from '@/app/grid/sheet/SheetCursor'; +import { ColumnRow, GridBounds, JsCoordinate, SheetBounds, SheetInfo, Validation } from '@/app/quadratic-core-types'; +import { SheetOffsets, SheetOffsetsWasm, stringToSelection } from '@/app/quadratic-rust-client/quadratic_rust_client'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Rectangle } from 'pixi.js'; -import { Coordinate } from '../../gridGL/types/size'; -import { sheets } from '../controller/Sheets'; -import { RectangleLike, SheetCursor } from './SheetCursor'; export class Sheet { id: string; @@ -26,6 +24,9 @@ export class Sheet { validations: Validation[] = []; + // clamp is the area that the cursor can move around in + clamp: Rectangle; + constructor(info: SheetInfo, testSkipOffsetsLoad = false) { this.id = info.sheet_id; this.name = info.name; @@ -35,7 +36,15 @@ export class Sheet { this.cursor = new SheetCursor(this); this.bounds = info.bounds; this.boundsWithoutFormatting = info.bounds_without_formatting; +<<<<<<< HEAD this.gridOverflowLines = new GridOverflowLines(this); +======= + this.gridOverflowLines = new GridOverflowLines(); + + // this will be imported via SheetInfo in the future + this.clamp = new Rectangle(1, 1, Infinity, Infinity); + +>>>>>>> origin/qa events.on('sheetBounds', this.updateBounds); events.on('sheetValidations', this.sheetValidations); } @@ -49,13 +58,8 @@ export class Sheet { // Returns all validations that intersect with the given point. getValidation(x: number, y: number): Validation[] | undefined { return this.validations.filter((v) => { - const selection = v.selection; - return ( - selection.all || - selection.columns?.find((c) => Number(c) === x) || - selection.rows?.find((r) => Number(r) === y) || - selection.rects?.find((r) => intersects.rectPoint(r, { x, y })) - ); + const selection = stringToSelection(v.selection.toString(), this.id, sheets.getSheetIdNameMap()); + return selection.contains(x, y); }); } @@ -101,7 +105,7 @@ export class Sheet { //#endregion //#region get grid information - getMinMax(onlyData: boolean): Coordinate[] | undefined { + getMinMax(onlyData: boolean): JsCoordinate[] | undefined { const bounds = onlyData ? this.boundsWithoutFormatting : this.bounds; if (bounds.type === 'empty') return; return [ @@ -116,8 +120,8 @@ export class Sheet { return new Rectangle( Number(bounds.min.x), Number(bounds.min.y), - Number(bounds.max.x) - Number(bounds.min.x), - Number(bounds.max.y) - Number(bounds.min.y) + Number(bounds.max.x) - Number(bounds.min.x) + 1, + Number(bounds.max.y) - Number(bounds.min.y) + 1 ); } @@ -142,24 +146,22 @@ export class Sheet { return this.offsets.getRowPlacement(y).position; } - // todo: change this to a JsValue instead of a Rust struct - getColumnRow(x: number, y: number): Coordinate { + getColumnRow(x: number, y: number): JsCoordinate { const columnRowStringified = this.offsets.getColumnRowFromScreen(x, y); const columnRow: ColumnRow = JSON.parse(columnRowStringified); return { x: columnRow.column, y: columnRow.row }; } // @returns screen rectangle for a column/row rectangle - getScreenRectangle(column: number | RectangleLike, row?: number, width?: number, height?: number): Rectangle { - if (typeof column === 'object') { - row = column.y; - width = column.width; - height = column.height; - column = column.x; - } - const topLeft = this.getCellOffsets(column, row!); - const bottomRight = this.getCellOffsets(column + width!, row! + height!); - return new Rectangle(topLeft.left, topLeft.top, bottomRight.right - topLeft.left, bottomRight.bottom - topLeft.top); + getScreenRectangle(column: number, row: number, width: number, height: number): Rectangle { + const topLeft = this.getCellOffsets(column, row); + const bottomRight = this.getCellOffsets(column + width, row + height); + return new Rectangle(topLeft.left, topLeft.top, bottomRight.left - topLeft.left, bottomRight.top - topLeft.top); + } + + // @returns screen rectangle from a selection rectangle + getScreenRectangleFromRect(rect: Rectangle): Rectangle { + return this.getScreenRectangle(rect.x, rect.y, rect.width, rect.height); } updateSheetOffsets(column: number | null, row: number | null, size: number) { @@ -170,6 +172,14 @@ export class Sheet { } } + getColumnFromScreen(x: number): number { + return this.offsets.getColumnFromScreen(x); + } + + getRowFromScreen(y: number): number { + return this.offsets.getRowFromScreen(y); + } + getColumnRowFromScreen(x: number, y: number): ColumnRow { const columnRowStringified = this.offsets.getColumnRowFromScreen(x, y); return JSON.parse(columnRowStringified); diff --git a/quadratic-client/src/app/grid/sheet/SheetCursor.ts b/quadratic-client/src/app/grid/sheet/SheetCursor.ts index 39485f0c84..dfc00a04c9 100644 --- a/quadratic-client/src/app/grid/sheet/SheetCursor.ts +++ b/quadratic-client/src/app/grid/sheet/SheetCursor.ts @@ -2,15 +2,16 @@ //! that state as you switch between sheets, a multiplayer user follows your //! cursor, or you save the cursor state in the URL at ?state=. +import { sheets } from '@/app/grid/controller/Sheets'; +import { Sheet } from '@/app/grid/sheet/Sheet'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; -import { Rect, Selection } from '@/app/quadratic-core-types'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { A1Selection, CellRefRange, JsCoordinate } from '@/app/quadratic-core-types'; +import { JsSelection } from '@/app/quadratic-rust-client/quadratic_rust_client'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; +import { rectToRectangle } from '@/app/web-workers/quadraticCore/worker/rustConversions'; import { IViewportTransformState } from 'pixi-viewport'; import { Rectangle } from 'pixi.js'; -import { pixiApp } from '../../gridGL/pixiApp/PixiApp'; -import { Coordinate } from '../../gridGL/types/size'; -import { Sheet } from './Sheet'; -import { selectionOverlapsSelection } from './sheetCursorUtils'; // Select column and/or row for the entire sheet. export interface ColumnRowCursor { @@ -29,8 +30,8 @@ export interface RectangleLike { // Save object for the cursor state. export interface SheetCursorSave { sheetId: string; - keyboardMovePosition: Coordinate; - cursorPosition: Coordinate; + keyboardMovePosition: JsCoordinate; + cursorPosition: JsCoordinate; multiCursor?: RectangleLike[]; columnRow?: ColumnRowCursor; } @@ -38,136 +39,55 @@ export interface SheetCursorSave { export class SheetCursor { private _viewport?: IViewportTransformState; - sheetId: string; + jsSelection: JsSelection; // used to determine if the boxCells (ie, autocomplete) is active boxCells: boolean; - keyboardMovePosition: Coordinate; - cursorPosition: Coordinate; - multiCursor?: Rectangle[]; - columnRow?: ColumnRowCursor; - constructor(sheet: Sheet) { - this.sheetId = sheet.id; + this.jsSelection = new JsSelection(sheet.id); this.boxCells = false; - this.keyboardMovePosition = { x: 0, y: 0 }; - this.cursorPosition = { x: 0, y: 0 }; } set viewport(save: IViewportTransformState) { this._viewport = save; } + get viewport(): IViewportTransformState { if (!this._viewport) { - const { x, y } = pixiApp.getStartingViewport(); - return { x, y, scaleX: 1, scaleY: 1 }; + const heading = pixiApp.headings.headingSize; + return { x: heading.width, y: heading.height, scaleX: 1, scaleY: 1 }; } return this._viewport; } - save(): SheetCursorSave { - return { - sheetId: this.sheetId, - keyboardMovePosition: this.keyboardMovePosition, - cursorPosition: this.cursorPosition, - multiCursor: this.multiCursor?.map((rect) => ({ x: rect.x, y: rect.y, width: rect.width, height: rect.height })), - columnRow: this.columnRow, - }; + selection(): A1Selection { + return this.jsSelection.selection(); } - load(value: SheetCursorSave): void { - this.keyboardMovePosition = value.keyboardMovePosition; - this.cursorPosition = value.cursorPosition; - this.multiCursor = value.multiCursor?.map((rect) => new Rectangle(rect.x, rect.y, rect.width, rect.height)); - this.columnRow = value.columnRow; - multiplayer.sendSelection(this.getMultiplayerSelection()); - pixiApp.cursor.dirty = true; + save(): string { + return this.jsSelection.save(); } - loadFromSelection(selection: Selection, skipMultiplayer = false) { - this.cursorPosition = { x: Number(selection.x), y: Number(selection.y) }; - - if ( - selection.rects?.length === 1 && - selection.rects[0].min.x === selection.rects[0].max.x && - selection.rects[0].min.y === selection.rects[0].max.y - ) { - // This is a single cell selection - this.multiCursor = undefined; - } else { - this.multiCursor = selection.rects?.map( - (rect) => - new Rectangle( - Number(rect.min.x), - Number(rect.min.y), - Number(rect.max.x) - Number(rect.min.x) + 1, - Number(rect.max.y) - Number(rect.min.y) + 1 - ) - ); - } - - if (selection.columns === null && selection.rows === null && selection.all === false) { - this.columnRow = undefined; - } else { - this.columnRow = { - columns: selection.columns?.map((x) => Number(x)), - rows: selection.rows?.map((y) => Number(y)), - all: selection.all === true ? true : undefined, - }; - } + load(selectionString: string): void { + this.jsSelection = JsSelection.load(selectionString); + multiplayer.sendSelection(this.save()); + pixiApp.cursor.dirty = true; + } + loadFromSelection(jsSelection: JsSelection, skipMultiplayer = false) { + this.jsSelection = jsSelection; if (!skipMultiplayer) { - multiplayer.sendSelection(this.getMultiplayerSelection()); + multiplayer.sendSelection(this.save()); } pixiApp.cursor.dirty = true; } - // Changes the cursor position. If multiCursor or columnRow is set to null, - // then it will be cleared. - changePosition( - options: { - multiCursor?: Rectangle[] | null; - columnRow?: ColumnRowCursor | null; - cursorPosition?: Coordinate; - keyboardMovePosition?: Coordinate; - ensureVisible?: boolean | Coordinate; - }, - test?: boolean - ) { - if (options.columnRow) { - this.columnRow = options.columnRow; - } else if (options.columnRow === null) { - this.columnRow = undefined; + updatePosition(ensureVisible = true) { + pixiApp.updateCursorPosition(ensureVisible); + if (!inlineEditorHandler.cursorIsMoving) { + multiplayer.sendSelection(this.save()); } - - if (options.multiCursor) { - this.multiCursor = options.multiCursor; - } else if (options.multiCursor === null) { - this.multiCursor = undefined; - } - - if (options.cursorPosition) { - this.cursorPosition = options.cursorPosition; - this.keyboardMovePosition = options.keyboardMovePosition ?? this.cursorPosition; - } else if (options.keyboardMovePosition) { - this.keyboardMovePosition = options.keyboardMovePosition; - } - if (!test) { - pixiApp.updateCursorPosition(options.ensureVisible ?? true); - if (!inlineEditorHandler.cursorIsMoving) { - multiplayer.sendSelection(this.getMultiplayerSelection()); - } - } - } - - // gets a stringified selection string for multiplayer - getMultiplayerSelection(): string { - return JSON.stringify({ - cursorPosition: this.cursorPosition, - multiCursor: this.multiCursor, - columnRow: this.columnRow, - }); } changeBoxCells(boxCells: boolean) { @@ -176,127 +96,197 @@ export class SheetCursor { } } - getCursor(): Coordinate { - return this.cursorPosition; + // Returns the cursor position. + get position(): JsCoordinate { + return this.jsSelection.getCursor(); } - // Gets all cursor Rectangles (either multiCursor or single cursor) - getRectangles(): Rectangle[] { - if (this.multiCursor) { - return this.multiCursor; - } else { - return [new Rectangle(this.cursorPosition.x, this.cursorPosition.y, 1, 1)]; - } + // Returns the last selection's end cell. + get selectionEnd(): JsCoordinate { + return this.jsSelection.getSelectionEnd(); } - getRustSelection(): Selection { - const sheet_id = { id: this.sheetId }; - const columns = this.columnRow?.columns ? this.columnRow.columns.map((x) => BigInt(x)) : null; - const rows = this.columnRow?.rows ? this.columnRow.rows.map((y) => BigInt(y)) : null; - const all = this.columnRow?.all ?? false; - let rects: Rect[] | null = null; - if (this.multiCursor) { - rects = this.multiCursor.map((rect) => ({ - min: { x: BigInt(rect.x), y: BigInt(rect.y) }, - max: { x: BigInt(rect.x + rect.width - 1), y: BigInt(rect.y + rect.height - 1) }, - })); - } else if (!this.columnRow) { - rects = [ - { - min: { x: BigInt(this.cursorPosition.x), y: BigInt(this.cursorPosition.y) }, - max: { x: BigInt(this.cursorPosition.x), y: BigInt(this.cursorPosition.y) }, - }, - ]; - } - return { - sheet_id, - x: BigInt(this.cursorPosition.x), - y: BigInt(this.cursorPosition.y), - rects, - columns, - rows, - all, - }; + // Returns the columns that are selected via ranges [c1_start, c1_end, c2_start, c2_end, ...]. + getSelectedColumnRanges(from: number, to: number): number[] { + return Array.from(this.jsSelection.getSelectedColumnRanges(from, to)); + } + + // Returns the rows that are selected via ranges [r1_start, r1_end, r2_start, r2_end, ...]. + getSelectedRowRanges(from: number, to: number): number[] { + return Array.from(this.jsSelection.getSelectedRowRanges(from, to)); + } + + // Returns the bottom-right cell for the selection. + get bottomRight(): JsCoordinate { + return this.jsSelection.getBottomRightCell(); } // Returns the largest rectangle that contains all the multiCursor rectangles - getLargestMultiCursorRectangle(): Rectangle { - if (!this.multiCursor) { - return new Rectangle(this.cursorPosition.x, this.cursorPosition.y, 1, 1); - } - let left = Infinity; - let top = Infinity; - let right = -Infinity; - let bottom = -Infinity; - this.multiCursor.forEach((rect) => { - left = Math.min(left, rect.x); - top = Math.min(top, rect.y); - right = Math.max(right, rect.x + rect.width); - bottom = Math.max(bottom, rect.y + rect.height); - }); - return new Rectangle(left, top, right - left, bottom - top); + getLargestRectangle(): Rectangle { + const rect = this.jsSelection.getLargestRectangle(); + return rectToRectangle(rect); + } + + // Returns rectangle in case of single finite range selection having more than one cell + // Returns undefined if there are multiple ranges or infinite range selection + getSingleRectangle(): Rectangle | undefined { + const rect = this.jsSelection.getSingleRectangle(); + return rect ? rectToRectangle(rect) : undefined; + } + + // Returns rectangle in case of single finite range selection, otherwise returns a rectangle that represents the cursor + // Returns undefined if there are multiple ranges or infinite range selection + getSingleRectangleOrCursor(): Rectangle | undefined { + const rect = this.jsSelection.getSingleRectangleOrCursor(); + return rect ? rectToRectangle(rect) : undefined; } - overlapsSelection(selection: Selection): boolean { - return selectionOverlapsSelection(this.getRustSelection(), selection); + overlapsSelection(a1Selection: string): boolean { + return this.jsSelection.overlapsA1Selection(a1Selection); } + // Returns true if the selection is a single cell or a single column or single row. hasOneColumnRowSelection(oneCell?: boolean): boolean { - return ( - !this.columnRow?.all && - !!( - (this.columnRow?.columns && this.columnRow.columns.length === 1) || - (this.columnRow?.rows && this.columnRow.rows.length === 1) || - (oneCell && !this.multiCursor) - ) - ); - } - - includesCell(column: number, row: number): boolean { - if (this.multiCursor) { - return this.multiCursor.some((rect) => rect.contains(column, row)); - } else { - return this.cursorPosition.x === column && this.cursorPosition.y === row; - } + return this.jsSelection.hasOneColumnRowSelection(oneCell ?? false); + } + + isSelectedColumnsFinite(): boolean { + return this.jsSelection.isSelectedColumnsFinite(); + } + + isSelectedRowsFinite(): boolean { + return this.jsSelection.isSelectedRowsFinite(); } // Returns the columns that are selected. - getColumnsSelection(): number[] { - const columns = new Set(); - if (this.columnRow?.columns) { - this.columnRow.columns.forEach((column) => columns.add(column)); - } - if (this.multiCursor) { - for (const rect of this.multiCursor) { - for (let x = rect.x; x < rect.x + rect.width; x++) { - columns.add(x); - } - } - } - columns.add(this.cursorPosition.x); - return Array.from(columns); + getSelectedColumns(): number[] { + return Array.from(this.jsSelection.getSelectedColumns()); } // Returns the rows that are selected. - getRowsSelection(): number[] { - const rows = new Set(); - if (this.columnRow?.rows) { - this.columnRow.rows.forEach((row) => rows.add(row)); - } - if (this.multiCursor) { - for (const rect of this.multiCursor) { - for (let y = rect.y; y < rect.y + rect.height; y++) { - rows.add(y); - } + getSelectedRows(): number[] { + return Array.from(this.jsSelection.getSelectedRows()); + } + + // Returns true if the cursor is only selecting a single cell + isSingleSelection(): boolean { + return this.jsSelection.isSingleSelection(); + } + + selectAll(append?: boolean) { + if (this.jsSelection.isAllSelected()) { + const bounds = sheets.sheet.boundsWithoutFormatting; + if (bounds.type === 'nonEmpty') { + this.jsSelection.selectRect( + Number(bounds.min.x), + Number(bounds.min.y), + Number(bounds.max.x), + Number(bounds.max.y), + false + ); + } else { + this.jsSelection.selectRect(1, 1, 1, 1, false); } + } else { + this.jsSelection.selectAll(append ?? false); } - rows.add(this.cursorPosition.y); - return Array.from(rows); + + this.updatePosition(true); } - // Returns true if the cursor is only selecting a single cell - onlySingleSelection(): boolean { - return !this.multiCursor?.length && !this.columnRow; + // Moves the cursor to the given position. This replaces any selection. + moveTo(x: number, y: number, append = false, ensureVisible = true) { + this.jsSelection.moveTo(x, y, append); + this.updatePosition(ensureVisible); + } + + selectTo(x: number, y: number, append: boolean, ensureVisible = true) { + this.jsSelection.selectTo(x, y, append); + this.updatePosition(ensureVisible); + } + + // Selects columns that have a current selection (used by cmd+space) + setColumnsSelected() { + this.jsSelection.setColumnsSelected(); + this.updatePosition(true); + } + + // Selects rows that have a current selection (used by shift+cmd+space) + setRowsSelected() { + this.jsSelection.setRowsSelected(); + this.updatePosition(true); + } + + selectColumn(column: number, ctrlKey: boolean, shiftKey: boolean, isRightClick: boolean, top: number) { + this.jsSelection.selectColumn(column, ctrlKey || shiftKey, shiftKey, isRightClick, top); + this.updatePosition(true); + } + + selectRow(row: number, ctrlKey: boolean, shiftKey: boolean, isRightClick: boolean, left: number) { + this.jsSelection.selectRow(row, ctrlKey || shiftKey, shiftKey, isRightClick, left); + this.updatePosition(true); + } + + isMultiCursor(): boolean { + return this.jsSelection.isMultiCursor(); + } + + isMultiRange(): boolean { + return this.rangeCount() > 1; + } + + isColumnRow(): boolean { + return this.jsSelection.isColumnRow(); + } + + toA1String(sheetId = sheets.current): string { + return this.jsSelection.toA1String(sheetId, sheets.getSheetIdNameMap()); + } + + toCursorA1(): string { + return this.jsSelection.toCursorA1(); + } + + contains(x: number, y: number): boolean { + return this.jsSelection.contains(x, y); + } + + selectRect(left: number, top: number, right: number, bottom: number, append = false, ensureVisible = true) { + this.jsSelection.selectRect(left, top, right, bottom, append); + this.updatePosition(ensureVisible); + } + + a1String(): string { + return this.jsSelection.toA1String(sheets.sheet.id, sheets.getSheetIdNameMap()); + } + + excludeCells(x0: number, y0: number, x1: number, y1: number, ensureVisible = true) { + this.jsSelection.excludeCells(x0, y0, x1, y1); + this.updatePosition(ensureVisible); + } + + rangeCount(): number { + return this.getFiniteRanges().length + this.getInfiniteRanges().length; + } + + getFiniteRanges(): CellRefRange[] { + const ranges = this.jsSelection.getFiniteRanges(); + try { + return JSON.parse(ranges); + } catch (e) { + console.error(e); + return []; + } + } + + getInfiniteRanges(): CellRefRange[] { + const ranges = this.jsSelection.getInfiniteRanges(); + try { + return JSON.parse(ranges); + } catch (e) { + console.error(e); + return []; + } } // Returns true if there is one multiselect of > 1 size diff --git a/quadratic-client/src/app/grid/sheet/SheetCursorUtils.test.ts b/quadratic-client/src/app/grid/sheet/SheetCursorUtils.test.ts deleted file mode 100644 index 20cc242cfd..0000000000 --- a/quadratic-client/src/app/grid/sheet/SheetCursorUtils.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { createSelection } from './selection'; -import { Rectangle } from 'pixi.js'; -import { selectionOverlapsSelection } from './sheetCursorUtils'; - -describe('selectionOverlapsSelection', () => { - it('rects-rects', () => { - const s1 = createSelection({ rects: [new Rectangle(0, 0, 1, 1)] }); - const s2 = createSelection({ rects: [new Rectangle(0, 0, 1, 1)] }); - expect(selectionOverlapsSelection(s1, s2)).toBe(true); - expect(selectionOverlapsSelection(s2, s1)).toBe(true); - const s3 = createSelection({ rects: [new Rectangle(2, 2, 1, 1)] }); - expect(selectionOverlapsSelection(s1, s3)).toBe(false); - expect(selectionOverlapsSelection(s3, s1)).toBe(false); - }); - - it('rects-columns', () => { - const s1 = createSelection({ rects: [new Rectangle(0, 0, 1, 1)] }); - const s2 = createSelection({ columns: [0] }); - expect(selectionOverlapsSelection(s1, s2)).toBe(true); - expect(selectionOverlapsSelection(s2, s1)).toBe(true); - const s3 = createSelection({ columns: [2] }); - expect(selectionOverlapsSelection(s1, s3)).toBe(false); - expect(selectionOverlapsSelection(s3, s1)).toBe(false); - }); - - it('rects-rows', () => { - const s1 = createSelection({ rects: [new Rectangle(0, 0, 1, 1)] }); - const s2 = createSelection({ rows: [0] }); - expect(selectionOverlapsSelection(s1, s2)).toBe(true); - expect(selectionOverlapsSelection(s2, s1)).toBe(true); - const s3 = createSelection({ rows: [2] }); - expect(selectionOverlapsSelection(s1, s3)).toBe(false); - expect(selectionOverlapsSelection(s3, s1)).toBe(false); - }); - - it('columns-rows', () => { - const s1 = createSelection({ columns: [0] }); - const s2 = createSelection({ rows: [0] }); - expect(selectionOverlapsSelection(s1, s2)).toBe(true); - expect(selectionOverlapsSelection(s2, s1)).toBe(true); - }); -}); - -/* - -There's a problem with importing monaco-editor for the test suite. I couldn't -figure out how to work around this issue. I'll have to skip this test for now. - -import { Sheet } from '@/app/grid/sheet/Sheet'; import { SheetCursor } from -'@/app/grid/sheet/SheetCursor'; import { Rectangle } from 'pixi.js'; import { -beforeEach, describe, expect, it } from 'vitest'; import { getSingleSelection } -from '../selection'; - -let sheetCursor: SheetCursor; let sheet: Sheet; - -beforeEach(() => { sheet = Sheet.testSheet(); sheetCursor = new - SheetCursor(sheet); -}); - -describe('SheetCursor.getRustSelection', () => { it('origin', () => { const - selection = sheetCursor.getRustSelection(); - expect(selection).toEqual(getSingleSelection(sheet.id, 0, 0)); - }); - - it('single position', () => { sheetCursor.changePosition({ cursorPosition: { - x: 1, y: 2 } }, true); const selection = sheetCursor.getRustSelection(); - expect(selection).toEqual({ sheet_id: { id: sheet.id }, all: false, columns: - null, rects: [{ min: { x: 1, y: 2 }, max: { x: 1, y: 2 } }], rows: null, - }); - }); - - it('multi cursor', () => { sheetCursor.changePosition({ multiCursor: [new - Rectangle(1, 2, 3, 3)] }); const selection = sheetCursor.getRustSelection(); - expect(selection).toEqual({ sheet_id: { id: sheet.id }, all: false, columns: - null, rects: [{ min: { x: 1, y: 2 }, max: { x: 3, y: 4 } }], rows: null, - }); - }); - - it('a row', () => { sheetCursor.changePosition({ columnRow: { rows: [1] } }, - true); const selection = sheetCursor.getRustSelection(); - expect(selection).toEqual({ sheet_id: { id: sheet.id }, all: false, columns: - null, rects: [{ min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }], rows: [1], - }); - }); - - it('rows', () => { sheetCursor.changePosition({ columnRow: { rows: [1, 2, 3] } - }, true); const selection = sheetCursor.getRustSelection(); - expect(selection).toEqual({ sheet_id: { id: sheet.id }, all: false, columns: - null, rects: [{ min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }], rows: [1, 2, - 3], - }); - }); - - it('a column', () => { sheetCursor.changePosition({ columnRow: { columns: [1] - } }, true); const selection = sheetCursor.getRustSelection(); - expect(selection).toEqual({ sheet_id: { id: sheet.id }, all: false, columns: - [1], rects: [{ min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }], rows: null, - }); - }); - - it('columns', () => { sheetCursor.changePosition({ columnRow: { columns: [1, - 2, 3] } }, true); const selection = sheetCursor.getRustSelection(); - expect(selection).toEqual({ sheet_id: { id: sheet.id }, all: false, columns: - [1, 2, 3], rects: [{ min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }], rows: - null, - }); - }); - - it('all', () => { sheetCursor.changePosition({ columnRow: { all: true } }, - true); const selection = sheetCursor.getRustSelection(); - expect(selection).toEqual({ sheet_id: { id: sheet.id }, all: true, columns: - null, rects: [{ min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }], rows: null, - }); - }); -}); -*/ diff --git a/quadratic-client/src/app/grid/sheet/selection.test.ts b/quadratic-client/src/app/grid/sheet/selection.test.ts deleted file mode 100644 index 9ad4ef9d07..0000000000 --- a/quadratic-client/src/app/grid/sheet/selection.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Selection } from '@/app/quadratic-core-types'; -import { describe, expect, it } from 'vitest'; -import type { RectangleLike } from './SheetCursor'; -import { - defaultSelection, - getSelectionString, - parseCoordinate, - parseNumberList, - parseRange, - parseSelectionString, -} from './selection'; - -const mockSelection = (options: { - rects?: RectangleLike[]; - columns?: number[]; - rows?: number[]; - all?: boolean; -}): Selection => { - return { - sheet_id: { id: '' }, - x: BigInt(0), - y: BigInt(0), - rects: - options.rects?.map((rect) => ({ - min: { x: BigInt(rect.x), y: BigInt(rect.y) }, - max: { x: BigInt(rect.x + rect.width - 1), y: BigInt(rect.y + rect.height - 1) }, - })) ?? null, - columns: options.columns?.map((x) => BigInt(x)) ?? null, - rows: options.rows?.map((y) => BigInt(y)) ?? null, - all: options.all || false, - }; -}; - -describe('getSelectionString', () => { - it('all', () => { - const selection = mockSelection({ all: true }); - expect(getSelectionString(selection)).toBe('all'); - }); - - it('columns', () => { - const selection = mockSelection({ columns: [1, 2] }); - expect(getSelectionString(selection)).toBe('(col=1, 2)'); - }); - - it('rows', () => { - const selection = mockSelection({ rows: [1, 2] }); - expect(getSelectionString(selection)).toBe('(row=1, 2)'); - }); - - it('columns and rows', () => { - const selection = mockSelection({ columns: [1, 2], rows: [3, 4] }); - expect(getSelectionString(selection)).toBe('(col=1, 2); (row=3, 4)'); - }); - - it('single cursor', () => { - const selection = mockSelection({ rects: [{ x: 1, y: 2, width: 1, height: 1 }] }); - expect(getSelectionString(selection)).toBe('(1,2)'); - }); - - it('multi cursor', () => { - const cursorSingle = mockSelection({ rects: [{ x: 1, y: 2, width: 1, height: 1 }] }); - expect(getSelectionString(cursorSingle)).toBe('(1,2)'); - - const cursorRect = mockSelection({ rects: [{ x: 1, y: 2, width: 2, height: 2 }] }); - expect(getSelectionString(cursorRect)).toBe('(1,2)-(2,3)'); - - const cursorMulti = mockSelection({ - rects: [ - { x: 1, y: 2, width: 1, height: 1 }, - { x: 3, y: 4, width: 2, height: 3 }, - ], - }); - expect(getSelectionString(cursorMulti)).toBe('(1,2); (3,4)-(4,6)'); - }); -}); - -it('parseCoordinate', () => { - expect(parseCoordinate('1, 2')).toEqual({ x: BigInt(1), y: BigInt(2) }); - expect(parseCoordinate('-1,-5')).toEqual({ x: BigInt(-1), y: BigInt(-5) }); - expect(parseCoordinate('test')).toBe(undefined); -}); - -it('parseRange', () => { - expect(parseRange('(1, 2)')).toEqual({ min: { x: BigInt(1), y: BigInt(2) }, max: { x: BigInt(1), y: BigInt(2) } }); - expect(parseRange('(-1,-5)')).toEqual({ - min: { x: BigInt(-1), y: BigInt(-5) }, - max: { x: BigInt(-1), y: BigInt(-5) }, - }); - expect(parseRange('test')).toBe(undefined); -}); - -it('parseNumberList', () => { - expect(parseNumberList('1, 2')).toEqual([BigInt(1), BigInt(2)]); - expect(parseNumberList('-1,-5')).toEqual([BigInt(-1), BigInt(-5)]); - expect(parseNumberList('test')).toEqual(undefined); -}); - -it('parseSelectionString', () => { - const sheetId = 'sheetId'; - expect(parseSelectionString('all', sheetId).selection).toEqual({ ...defaultSelection(sheetId), all: true }); - expect(parseSelectionString('(col=1, 2)', sheetId).selection).toEqual({ - ...defaultSelection(sheetId), - columns: [1n, 2n], - }); - expect(parseSelectionString('(row = 1,2)', sheetId).selection).toEqual({ - ...defaultSelection(sheetId), - rows: [1n, 2n], - }); - expect(parseSelectionString('(col=1, 2); (row=3, 4)', sheetId).selection).toEqual({ - ...defaultSelection(sheetId), - columns: [1n, 2n], - rows: [3n, 4n], - }); - expect(parseSelectionString('(1,2)', sheetId).selection).toEqual({ - ...defaultSelection(sheetId), - rects: [{ min: { x: 1n, y: 2n }, max: { x: 1n, y: 2n } }], - }); - expect(parseSelectionString('(1,2)-(2,3)', sheetId).selection).toEqual({ - ...defaultSelection(sheetId), - rects: [{ min: { x: 1n, y: 2n }, max: { x: 2n, y: 3n } }], - }); - expect(parseSelectionString('(1,2); (3,4)-(4,6)', sheetId).selection).toEqual({ - ...defaultSelection(sheetId), - rects: [ - { min: { x: 1n, y: 2n }, max: { x: 1n, y: 2n } }, - { min: { x: 3n, y: 4n }, max: { x: 4n, y: 6n } }, - ], - }); - expect(parseSelectionString(' test', sheetId).error).toEqual({ error: 'Unknown range reference', column: 0 }); - expect(parseSelectionString('(col=1, 2); (row=3, 4) test', sheetId).error).toEqual({ - error: 'Unknown range reference', - column: 12, - }); - expect(parseSelectionString('', sheetId).error).toEqual(undefined); -}); diff --git a/quadratic-client/src/app/grid/sheet/selection.ts b/quadratic-client/src/app/grid/sheet/selection.ts index 0555467c85..a5a932f034 100644 --- a/quadratic-client/src/app/grid/sheet/selection.ts +++ b/quadratic-client/src/app/grid/sheet/selection.ts @@ -1,201 +1,36 @@ -import { Pos, Rect, Selection } from '@/app/quadratic-core-types'; -import { rectangleToRect } from '@/app/web-workers/quadraticCore/worker/rustConversions'; -import { Rectangle } from 'pixi.js'; - -const RANGE_SEPARATOR = '; '; - -// Gets a Selection based on a SheetCursor -export const getSelectionString = (selection: Selection): string => { - if (selection.all) { - return 'all'; - } - - let range = ''; - if (selection.columns) { - if (range) { - range += RANGE_SEPARATOR; - } - range += `(col=${selection.columns.join(', ')})`; - } - - if (selection.rows) { - if (range) { - range += RANGE_SEPARATOR; - } - range += `(row=${selection.rows.join(', ')})`; - } - - if (selection.rects) { - if (range) { - range += RANGE_SEPARATOR; - } - range += selection.rects - .map((rect) => { - if (Number(rect.max.x - rect.min.x) === 0 && Number(rect.max.y - rect.min.y) === 0) { - return `(${rect.min.x},${rect.min.y})`; - } - return `(${rect.min.x},${rect.min.y})-(${rect.max.x},${rect.max.y})`; - }) - .join(RANGE_SEPARATOR); +import { Rect } from '@/app/quadratic-core-types'; +import { + newAllSelection, + newRectSelection, + newSingleSelection, +} from '@/app/quadratic-rust-client/quadratic_rust_client'; + +export const getSingleSelection = (sheetId: string, x: number, y: number): string => { + try { + const selection = newSingleSelection(sheetId, x, y); + return selection.save(); + } catch (e) { + console.error('Failed to get single selection', e); + throw new Error(`Failed to get single selection`); } - - return range; }; -// parses a string expecting to find x,y -export const parseCoordinate = (s: string): Pos | undefined => { - const parts = s.split(','); - if (parts.length !== 2) { - return; +export const getRectSelection = (sheetId: string, rect: Rect): string => { + try { + const selection = newRectSelection(sheetId, rect.min.x, rect.min.y, rect.max.x, rect.max.y); + return selection.save(); + } catch (e) { + console.error('Failed to get rect selection', e); + throw new Error(`Failed to get rect selection`); } - const x = parseInt(parts[0]); - const y = parseInt(parts[1]); - if (isNaN(x) || isNaN(y)) return; - return { x: BigInt(x), y: BigInt(y) }; }; -// parses a string expecting to find (x,y)-(x,y) -export const parseRange = (s: string): Rect | undefined => { - const parts = s.split('-'); - if (parts.length !== 2) { - const c = parseCoordinate(s.substring(1, s.length - 1)); - if (c) { - return { min: c, max: c }; - } else { - return; - } +export const getAllSelection = (sheetId: string): string => { + try { + const selection = newAllSelection(sheetId); + return selection.save(); + } catch (e) { + console.error('Failed to get all selections', e); + throw new Error(`Failed to get all selections`); } - const min = parseCoordinate(parts[0].substring(1, parts[0].length - 1)); - const max = parseCoordinate(parts[1].substring(1, parts[1].length - 1)); - if (!min || !max) { - return; - } - return { min, max }; -}; - -// parses a string expecting to find a list of numbers -export const parseNumberList = (s: string): bigint[] | undefined => { - const numbers = s.split(','); - const result: bigint[] = []; - for (let number of numbers) { - const n = parseInt(number); - if (isNaN(n)) { - return; - } - result.push(BigInt(n)); - } - return result; -}; - -// Parses a string to find a Selection -// @returns Selection | [error message, index of error] -export const parseSelectionString = ( - range: string, - sheetId: string -): { selection?: Selection; error?: { error: string; column: number } } => { - range = range.trim(); - const selection: Selection = { - sheet_id: { id: sheetId }, - x: BigInt(0), - y: BigInt(0), - columns: null, - rows: null, - rects: null, - all: false, - }; - - if (range === 'all') { - selection.all = true; - return { selection }; - } - - if (range === '') { - return { selection }; - } - - // this can be replaced by a regex--but this is more readable - const parts = range.split(RANGE_SEPARATOR); - for (let part of parts) { - // remove all spaces - const trimmedPart = part.replace(/ /g, ''); - - if (trimmedPart.length === 0) { - return { - error: { error: 'Empty range', column: 0 }, - }; - } - if (trimmedPart.startsWith('(col=') && trimmedPart.endsWith(')')) { - const columns = parseNumberList(trimmedPart.substring(5, trimmedPart.length - 1)); - if (columns) { - selection.columns = columns; - } else { - return { - error: { error: 'Unknown column reference', column: range.indexOf(part) }, - }; - } - } else if (trimmedPart.startsWith('(row=') && trimmedPart.endsWith(')')) { - const rows = parseNumberList(trimmedPart.substring(5, trimmedPart.length - 1)); - if (rows) { - selection.rows = rows; - } else { - return { - error: { error: 'Unknown row reference', column: range.indexOf(part) }, - }; - } - } else { - const rect = parseRange(trimmedPart); - if (rect) { - if (!selection.rects) { - selection.rects = []; - } - selection.rects.push(rect); - } else { - return { - error: { error: 'Unknown range reference', column: range.indexOf(part) }, - }; - } - } - } - return { selection }; -}; - -// Returns a Selection given a single x,y value -export const getSingleSelection = (sheetId: string, x: number, y: number): Selection => { - return { - sheet_id: { id: sheetId }, - x: BigInt(x), - y: BigInt(y), - columns: null, - rows: null, - rects: [{ min: { x: BigInt(x), y: BigInt(y) }, max: { x: BigInt(x), y: BigInt(y) } }], - all: false, - }; -}; - -export const defaultSelection = (sheetId: string): Selection => ({ - x: 0n, - y: 0n, - sheet_id: { id: sheetId }, - all: false, - columns: null, - rows: null, - rects: null, -}); - -export const createSelection = (options: { - rects?: Rectangle[]; - columns?: number[]; - rows?: number[]; - all?: boolean; - sheetId?: string; -}): Selection => { - return { - sheet_id: { id: options.sheetId ?? '' }, - x: 0n, - y: 0n, - all: options.all ?? false, - columns: options.columns?.map((x) => BigInt(x)) || null, - rows: options.rows?.map((y) => BigInt(y)) || null, - rects: options.rects?.map((rect) => rectangleToRect(rect)) || null, - }; }; diff --git a/quadratic-client/src/app/grid/sheet/sheetCursorUtils.ts b/quadratic-client/src/app/grid/sheet/sheetCursorUtils.ts deleted file mode 100644 index b751d030ff..0000000000 --- a/quadratic-client/src/app/grid/sheet/sheetCursorUtils.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { intersects } from '@/app/gridGL/helpers/intersects'; -import { Selection } from '@/app/quadratic-core-types'; - -// Returns whether a Selection overlaps another Selection -export const selectionOverlapsSelection = (s1: Selection, s2: Selection): boolean => { - if (s1.all || s2.all) return true; - - if ((s1.columns && s2.rows) || (s2.columns && s1.rows)) { - return true; - } - - if (s1.columns && s2.columns) { - if (s1.columns.some((c) => s2.columns?.includes(c))) { - return true; - } - } - - if (s1.rows && s2.rows) { - if (s1.rows.some((r) => s2.rows?.includes(r))) { - return true; - } - } - - if (s1.rects && s2.rects) { - for (const rect1 of s1.rects) { - for (const rect2 of s2.rects) { - if (intersects.rectRect(rect1, rect2)) { - return true; - } - } - } - } - - if (s1.rects) { - for (const rect1 of s1.rects) { - if (s2.columns) { - for (const c of s2.columns) { - if (c >= rect1.min.x && c <= rect1.max.x) { - return true; - } - } - } - if (s2.rows) { - for (const r of s2.rows) { - if (r >= rect1.min.y && r <= rect1.max.y) { - return true; - } - } - } - } - } - - if (s2.rects) { - for (const rect2 of s2.rects) { - if (s1.columns) { - for (const c of s1.columns) { - if (c >= rect2.min.x && c <= rect2.max.x) { - return true; - } - } - } - if (s1.rows) { - for (const r of s1.rows) { - if (r >= rect2.min.y && r <= rect2.max.y) { - return true; - } - } - } - } - } - - return false; -}; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/CodeHint.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/CodeHint.tsx index e806831539..0433ea8c8b 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/CodeHint.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/CodeHint.tsx @@ -10,7 +10,7 @@ export const CodeHint = () => { if (isMobile || sheetNotEmpty || !showCodeHint) return null; - const offset = sheets.sheet.getCellOffsets(0, 0); + const offset = sheets.sheet.getCellOffsets(1, 1); return (
{ + const [show, setShow] = useRecoilState(gridHeadingAtom); + + useEffect(() => { + const handleViewportChanged = () => { + setShow({ world: undefined, column: null, row: null }); + }; + + events.on('viewportChanged', handleViewportChanged); + return () => { + events.off('viewportChanged', handleViewportChanged); + }; + }, [setShow]); + + const ref = useRef(null); + + const isColumnRowAvailable = sheets.sheet.cursor.hasOneColumnRowSelection(true); + const isColumnFinite = sheets.sheet.cursor.isSelectedColumnsFinite(); + const isRowFinite = sheets.sheet.cursor.isSelectedRowsFinite(); + + return ( +
+ { + setShow({ world: undefined, column: null, row: null }); + focusGrid(); + }} + anchorRef={ref} + menuStyle={{ padding: '0', color: 'inherit' }} + menuClassName="bg-background" + > + + + + + + + + + {show.column === null ? null : ( + <> + {isColumnFinite && } + {isColumnRowAvailable && isColumnFinite && } + {isColumnRowAvailable && isColumnFinite && } + {isColumnFinite && } + + )} + + {show.row === null ? null : ( + <> + {isRowFinite && } + {isColumnRowAvailable && isRowFinite && } + {isColumnRowAvailable && isRowFinite && } + {isRowFinite && } + + )} + +
+ ); +}; + +function MenuItemAction({ action }: { action: Action }) { + const { label, Icon, run, isAvailable } = defaultActionSpec[action]; + const isAvailableArgs = useIsAvailableArgs(); + const keyboardShortcut = keyboardShortcutEnumToDisplay(action); + + if (isAvailable && !isAvailable(isAvailableArgs)) { + return null; + } + + return ( + + {label} + + ); +} + +function MenuItemShadStyle({ + children, + Icon, + onClick, + keyboardShortcut, +}: { + children: string; + Icon?: IconComponent; + onClick: any; + keyboardShortcut?: string; +}) { + const menuItemClassName = + 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'; + return ( + + + {Icon && } {children} + + {keyboardShortcut && ( + {keyboardShortcut} + )} + + ); +} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index d5b95854df..7d10fa3351 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -1,5 +1,6 @@ import { events } from '@/app/events/events'; import { Annotations } from '@/app/gridGL/HTMLGrid/annotations/Annotations'; +import { AskAISelection } from '@/app/gridGL/HTMLGrid/askAISelection/AskAISelection'; import { CodeHint } from '@/app/gridGL/HTMLGrid/CodeHint'; import { CodeRunning } from '@/app/gridGL/HTMLGrid/codeRunning/CodeRunning'; import { GridContextMenu } from '@/app/gridGL/HTMLGrid/contextMenus/GridContextMenu'; @@ -66,14 +67,10 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { normalContainer.style.transform = transform; }; updateTransform(); - viewport.on('moved', updateTransform); - viewport.on('moved-end', updateTransform); - viewport.on('zoomed', updateTransform); + events.on('viewportChangedReady', updateTransform); window.addEventListener('resize', updateTransform); return () => { - viewport.off('moved', updateTransform); - viewport.off('moved-end', updateTransform); - viewport.off('zoomed', updateTransform); + events.off('viewportChangedReady', updateTransform); window.removeEventListener('resize', updateTransform); }; }, [normalContainer, parent, zoomContainer]); @@ -82,6 +79,11 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { if (!parent) return null; + if (isNaN(leftHeading) || isNaN(topHeading)) { + console.log('todo in HTMLGridContainer', { leftHeading, topHeading }); + return null; + } + return ( <> {/* This is positioned on the grid inside the headings and zoomed */} @@ -115,6 +117,7 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { + diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/SuggestionDropdown.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/SuggestionDropdown.tsx index 565d979271..4ff4c60823 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/SuggestionDropdown.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/SuggestionDropdown.tsx @@ -1,15 +1,15 @@ -import { cn } from '@/shared/shadcn/utils'; -import { useInlineEditorStatus } from './inlineEditor/useInlineEditorStatus'; -import { useCallback, useEffect, useState } from 'react'; -import { inlineEditorEvents } from './inlineEditor/inlineEditorEvents'; -import { inlineEditorMonaco } from './inlineEditor/inlineEditorMonaco'; -import { inlineEditorHandler } from './inlineEditor/inlineEditorHandler'; -import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -import { sheets } from '@/app/grid/controller/Sheets'; -import { Rectangle } from 'pixi.js'; -import { pixiApp } from '../pixiApp/PixiApp'; import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { inlineEditorEvents } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorEvents'; +import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; +import { inlineEditorMonaco } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco'; +import { useInlineEditorStatus } from '@/app/gridGL/HTMLGrid/inlineEditor/useInlineEditorStatus'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { validationRuleSimple } from '@/app/ui/menus/Validations/Validation/validationType'; +import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { cn } from '@/shared/shadcn/utils'; +import { Rectangle } from 'pixi.js'; +import { useCallback, useEffect, useState } from 'react'; export const SuggestionDropDown = () => { const inlineEditorStatus = useInlineEditorStatus(); @@ -20,7 +20,14 @@ export const SuggestionDropDown = () => { useEffect(() => { const populateList = async () => { const sheet = sheets.sheet; - const pos = sheet.cursor.cursorPosition; + const cursor = sheet.cursor; + if (cursor.isMultiCursor()) { + setList(undefined); + inlineEditorMonaco.autocompleteShowingList = false; + return; + } + + const pos = sheet.cursor.position; // if there are validations, don't autocomplete // todo: we can make this better by showing only validated values @@ -33,26 +40,25 @@ export const SuggestionDropDown = () => { inlineEditorMonaco.autocompleteList = values; } else if (validationRuleSimple(validation) === 'list') { if (validation && validationRuleSimple(validation) === 'list') { - quadraticCore.getValidationList(sheets.sheet.id, pos.x, pos.y).then((values) => { - if (values) { - // we set the list to undefined so the dropdown doesn't show (it will show only if validation is set to show list) - // we still want the autocomplete to work so we send the values to the monaco editor - setList(undefined); - inlineEditorMonaco.autocompleteList = values; - } - }); + const values = await quadraticCore.getValidationList(sheets.sheet.id, pos.x, pos.y); + if (values) { + // we set the list to undefined so the dropdown doesn't show (it will show only if validation is set to show list) + // we still want the autocomplete to work so we send the values to the monaco editor + setList(undefined); + inlineEditorMonaco.autocompleteList = values; + } } } else { setList(undefined); inlineEditorMonaco.autocompleteShowingList = false; } - return; - } - const values = await quadraticCore.neighborText(sheet.id, pos.x, pos.y); - setList(values); - inlineEditorMonaco.autocompleteList = values; - if (values) { - setOffsets(sheet.getCellOffsets(pos.x, pos.y)); + } else { + const values = await quadraticCore.neighborText(sheets.current, pos.x, pos.y); + setList(values); + inlineEditorMonaco.autocompleteList = values; + if (values) { + setOffsets(sheets.sheet.getCellOffsets(pos.x, pos.y)); + } } }; @@ -83,7 +89,7 @@ export const SuggestionDropDown = () => { inlineEditorEvents.on('valueChanged', valueChanged); return () => { - inlineEditorEvents.off('status', populateList); + events.off('cursorPosition', populateList); inlineEditorEvents.off('valueChanged', valueChanged); }; }, [filteredList, list]); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/annotations/Annotations.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/annotations/Annotations.tsx index 1744f56949..4aa6c19087 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/annotations/Annotations.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/annotations/Annotations.tsx @@ -10,13 +10,12 @@ export const Annotations = () => { const [offsets, setOffsets] = useState(); useEffect(() => { const updateOffsets = () => { - const p = sheets.sheet.cursor.cursorPosition; + const p = sheets.sheet.cursor.position; setOffsets(sheets.sheet.getCellOffsets(p.x, p.y)); }; updateOffsets(); events.on('cursorPosition', updateOffsets); - return () => { events.off('cursorPosition', updateOffsets); }; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/annotations/CalendarPicker.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/annotations/CalendarPicker.tsx index d8f18c4908..c56fc4df0a 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/annotations/CalendarPicker.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/annotations/CalendarPicker.tsx @@ -43,7 +43,7 @@ export const CalendarPicker = () => { const [dateFormat, setDateFormat] = useState(''); useEffect(() => { const fetchValue = async () => { - const position = sheets.sheet.cursor.cursorPosition; + const position = sheets.sheet.cursor.position; const value = await quadraticCore.getDisplayCell(sheets.sheet.id, position.x, position.y); if (value) { const d = new Date(value as string); @@ -56,7 +56,7 @@ export const CalendarPicker = () => { } setValue(value); } - const summary = await quadraticCore.getCellFormatSummary(sheets.sheet.id, position.x, position.y, true); + const summary = await quadraticCore.getCellFormatSummary(sheets.sheet.id, position.x, position.y); if (summary.dateTime) { setDateFormat(summary.dateTime); } else { diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/askAISelection/AskAISelection.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/askAISelection/AskAISelection.tsx new file mode 100644 index 0000000000..c363fd0f60 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/askAISelection/AskAISelection.tsx @@ -0,0 +1,177 @@ +import { inlineEditorAtom } from '@/app/atoms/inlineEditorAtom'; +import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { focusGrid } from '@/app/helpers/focusGrid'; +import { JsCoordinate } from '@/app/quadratic-core-types'; +import { useSubmitAIAnalystPrompt } from '@/app/ui/menus/AIAnalyst/hooks/useSubmitAIAnalystPrompt'; +import { AIIcon } from '@/shared/components/Icons'; +import { Button } from '@/shared/shadcn/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/shared/shadcn/ui/dropdown-menu'; +import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; +import { Context } from 'quadratic-shared/typesAndSchemasAI'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useRecoilValue } from 'recoil'; + +const SELECTION_PROMPTS: { label: string; prompt: string }[] = [ + { label: 'Create a chart', prompt: 'Create a chart from my selected data using Plotly in Python' }, + { label: 'Summarize data', prompt: 'Generate insights on my selected data using Python code' }, + { label: 'Tell me about this data', prompt: 'What kind of data is this, do not use code' }, + { label: 'Clean data', prompt: 'Clean my selected data using Python' }, +]; + +const ASK_AI_SELECTION_DELAY = 500; + +export function AskAISelection() { + const inlineEditorState = useRecoilValue(inlineEditorAtom); + const [currentSheet, setCurrentSheet] = useState(sheets.current); + const [selectionSheetId, setSelectionSheetId] = useState(); + const [selection, setSelection] = useState(); + const [displayPos, setDisplayPos] = useState(); + const [loading, setLoading] = useState(false); + const timeoutRef = useRef(); + + const { submitPrompt } = useSubmitAIAnalystPrompt(); + + const showAskAISelection = useCallback(() => { + const singleRect = sheets.sheet.cursor.getSingleRectangle(); + if (singleRect) { + const hasContent = pixiApp.cellsSheets.getById(sheets.current)?.cellsLabels.hasCellInRect(singleRect); + if (hasContent && !inlineEditorState.visible) { + const selection = sheets.sheet.cursor.save(); + const screenRect = sheets.sheet.getScreenRectangleFromRect(singleRect); + setSelection(selection); + setSelectionSheetId(sheets.current); + setDisplayPos({ + x: screenRect.x + screenRect.width, + y: screenRect.y, + }); + } else { + setSelection(undefined); + setSelectionSheetId(undefined); + setDisplayPos(undefined); + } + } else { + setSelection(undefined); + setSelectionSheetId(undefined); + setDisplayPos(undefined); + } + }, [inlineEditorState.visible]); + + const updateSelection = useCallback(() => { + clearTimeout(timeoutRef.current); + setSelection(undefined); + setSelectionSheetId(undefined); + setDisplayPos(undefined); + + timeoutRef.current = setTimeout(() => { + showAskAISelection(); + }, ASK_AI_SELECTION_DELAY); + }, [showAskAISelection]); + + const handleSubmitPrompt = useCallback( + (prompt: string) => { + setLoading(true); + submitPrompt({ + userPrompt: prompt, + context: { + sheets: [], + currentSheet: sheets.sheet.name, + selection, + }, + clearMessages: true, + }) + .catch(console.error) + .finally(() => { + setLoading(false); + setSelection(undefined); + setSelectionSheetId(undefined); + setDisplayPos(undefined); + }); + }, + [selection, submitPrompt] + ); + + useEffect(() => { + const handleCursorPosition = () => { + updateSelection(); + }; + + events.on('cursorPosition', handleCursorPosition); + return () => { + events.off('cursorPosition', handleCursorPosition); + }; + }, [updateSelection]); + + useEffect(() => { + const updateSheet = (sheetId: string) => { + setCurrentSheet(sheetId); + updateSelection(); + }; + + events.on('changeSheet', updateSheet); + return () => { + events.off('changeSheet', updateSheet); + }; + }, [updateSelection]); + + useEffect(() => { + const handleHashContentChanged = (sheetId: string) => { + if (currentSheet === sheetId) updateSelection(); + }; + + events.on('hashContentChanged', handleHashContentChanged); + return () => { + events.off('hashContentChanged', handleHashContentChanged); + }; + }, [currentSheet, updateSelection]); + + if (selectionSheetId !== currentSheet || displayPos === undefined) return null; + + return ( +
+ + + + + + + + { + e.preventDefault(); + focusGrid(); + }} + > + {SELECTION_PROMPTS.map(({ label, prompt }) => ( + { + e.stopPropagation(); + handleSubmitPrompt(prompt); + }} + > + {label} + + ))} + + +
+ ); +} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/codeRunning/CodeRunning.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/codeRunning/CodeRunning.tsx index bc51e802a4..be45d612a3 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/codeRunning/CodeRunning.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/codeRunning/CodeRunning.tsx @@ -51,6 +51,12 @@ export const CodeRunning = () => { events.on('pythonState', updateRunningState); events.on('javascriptState', updateRunningState); events.on('connectionState', updateRunningState); + + return () => { + events.off('pythonState', updateRunningState); + events.off('javascriptState', updateRunningState); + events.off('connectionState', updateRunningState); + }; }, []); // update multiplayer's code runs @@ -107,7 +113,7 @@ export const CodeRunning = () => { events.on('multiplayerCodeRunning', updateMultiplayerCodeRunning); return () => { - events.on('multiplayerUpdate', updateMultiplayerUsers); + events.off('multiplayerUpdate', updateMultiplayerUsers); events.off('multiplayerCodeRunning', updateMultiplayerCodeRunning); }; }, [playerCode?.length]); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/hoverCell/HoverCell.css b/quadratic-client/src/app/gridGL/HTMLGrid/hoverCell/HoverCell.css deleted file mode 100644 index 5fafc4bc1e..0000000000 --- a/quadratic-client/src/app/gridGL/HTMLGrid/hoverCell/HoverCell.css +++ /dev/null @@ -1,43 +0,0 @@ -.hover-cell-container { - position: 'relative'; - opacity: 0; -} - -.hover-cell-header { - margin-bottom: 0.5rem; - display: flex; - justify-content: space-between; -} - -.hover-cell-header-buttons { - display: flex; - gap: 4px; -} - -.hover-cell-header-space { - margin: 0.5rem 0; -} - -.hover-cell-body { - @apply text-xs; -} - -.hover-cell-code { - @apply text-nowrap rounded-md bg-gray-100 p-0.5; -} - -.hover-cell-error-msg { - @apply font-mono text-destructive; - font-size: 0.65rem; -} - -.hover-cell-body > div:first-child { - margin-bottom: 0.5rem; -} - -.code-body { - @apply max-h-48 rounded-md bg-gray-50 p-1 font-mono; - white-space: pre; - overflow: hidden; - font-size: 0.65rem; -} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/hoverCell/HoverCell.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/hoverCell/HoverCell.tsx index 2fd7077d0f..77f96ab47c 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/hoverCell/HoverCell.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/hoverCell/HoverCell.tsx @@ -1,3 +1,4 @@ +import { aiAssistantLoadingAtom } from '@/app/atoms/codeEditorAtom'; import { showCodePeekAtom } from '@/app/atoms/gridSettingsAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; @@ -5,15 +6,19 @@ import { ErrorValidation } from '@/app/gridGL/cells/CellsSheet'; import { usePositionCellMessage } from '@/app/gridGL/HTMLGrid/usePositionCellMessage'; import { HtmlValidationMessage } from '@/app/gridGL/HTMLGrid/validations/HtmlValidationMessage'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { getLanguage } from '@/app/helpers/codeCellLanguage'; +import { CodeCell } from '@/app/gridGL/types/codeCell'; +import { getCodeCell, getLanguage } from '@/app/helpers/codeCellLanguage'; import { pluralize } from '@/app/helpers/pluralize'; import { JsCodeCell, JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { xyToA1 } from '@/app/quadratic-rust-client/quadratic_rust_client'; +import { FixSpillError } from '@/app/ui/components/FixSpillError'; +import { useSubmitAIAssistantPrompt } from '@/app/ui/menus/CodeEditor/hooks/useSubmitAIAssistantPrompt'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { Button } from '@/shared/shadcn/ui/button'; import { cn } from '@/shared/shadcn/utils'; import { Rectangle } from 'pixi.js'; -import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; +import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import './HoverCell.css'; export const HOVER_CELL_FADE_IN_OUT_DELAY = 500; @@ -72,87 +77,67 @@ export function HoverCell() { const [text, setText] = useState(); const [onlyCode, setOnlyCode] = useState(false); - const updateText = useCallback(async (cell: JsRenderCodeCell | EditingCell | ErrorValidation) => { - const errorValidationCell = 'validationId' in cell ? cell : undefined; - const renderCodeCell = 'language' in cell && 'state' in cell && 'spill_error' in cell ? cell : undefined; - const editingCell = 'user' in cell && 'codeEditor' in cell ? cell : undefined; - - if (errorValidationCell) { - const offsets = sheets.sheet.getCellOffsets(errorValidationCell.x, errorValidationCell.y); - const validation = sheets.sheet.getValidationById(errorValidationCell.validationId); - setOnlyCode(false); - if (validation) { - setText( -
- -
- ); - } - } else if (renderCodeCell) { - const spillError = renderCodeCell.spill_error; + const updateText = useCallback( + async (cell: JsRenderCodeCell | EditingCell | ErrorValidation) => { + const errorValidationCell = 'validationId' in cell ? cell : undefined; + const renderCodeCell = 'language' in cell && 'state' in cell && 'spill_error' in cell ? cell : undefined; + const editingCell = 'user' in cell && 'codeEditor' in cell ? cell : undefined; - if (spillError) { + if (errorValidationCell) { + const offsets = sheets.sheet.getCellOffsets(errorValidationCell.x, errorValidationCell.y); + const validation = sheets.sheet.getValidationById(errorValidationCell.validationId); setOnlyCode(false); - setText( - <> -
Spill Error
-
-
Array output could not expand because it would overwrite existing values.
-
- To fix this, remove content in {pluralize('cell', spillError.length)}{' '} - {spillError.map((pos, index) => ( -
- - ({String(pos.x)}, {String(pos.y)}) - - {index !== spillError.length - 1 ? (index === spillError.length - 2 ? ', and ' : ', ') : '.'} -
- ))} -
+ if (validation) { + setText( +
+
- - ); - } else { - const language = getLanguage(renderCodeCell.language); + ); + } + } else if (renderCodeCell) { const sheetId = sheets.sheet.id; const { x, y } = renderCodeCell; const codeCell = await quadraticCore.getCodeCell(sheetId, x, y); - - if (renderCodeCell.state === 'RunError') { + if (renderCodeCell.state === 'SpillError') { setOnlyCode(false); if (codeCell) { - setText(); + setText(); } } else { - setOnlyCode(true); - setText( - <> -
{language} Code
-
{codeCell?.code_string}
- - ); + const language = getLanguage(renderCodeCell.language); + if (renderCodeCell.state === 'RunError') { + setOnlyCode(false); + if (codeCell) { + setText(); + } + } else { + setOnlyCode(true); + setText( + + {codeCell?.code_string} + + ); + } } - } - } else if (editingCell) { - setOnlyCode(false); - setText( - <> -
Multiplayer Edit
-
+ } else if (editingCell) { + setOnlyCode(false); + setText( + {editingCell.codeEditor ? 'The code in this cell' : 'This cell'} is being edited by {editingCell.user}. -
- - ); - } + + ); + } - setLoading(false); - }, []); + setLoading(false); + }, + [hideHoverCell] + ); useEffect(() => { const addCell = (cell?: JsRenderCodeCell | EditingCell | ErrorValidation) => { @@ -204,7 +189,7 @@ export function HoverCell() {
-
{text}
+ {text}
); } -function HoverCellRunError({ codeCell }: { codeCell: JsCodeCell }) { - const language = getLanguage(codeCell.language); +function HoverCellRunError({ codeCell: codeCellCore, onClick }: { codeCell: JsCodeCell; onClick: () => void }) { + const cell = useMemo(() => getCodeCell(codeCellCore.language), [codeCellCore.language]); + const language = cell?.label; + const x = Number(codeCellCore.x); + const y = Number(codeCellCore.y); + + const codeCell: CodeCell = useMemo( + () => ({ + sheetId: sheets.current, + pos: { x, y }, + language: codeCellCore.language, + }), + [codeCellCore.language, x, y] + ); + + const loading = useRecoilValue(aiAssistantLoadingAtom); + + const { submitPrompt } = useSubmitAIAssistantPrompt(); return ( - <> -
- Run Error -
+ { + submitPrompt({ userPrompt: 'Fix the error in the code cell', clearMessages: true, codeCell }).catch( + console.error + ); + onClick(); + }} + disabled={loading} + > + Fix with AI + + } + > + {codeCellCore.std_err} + {codeCellCore.code_string} + + ); +} + +function HoverCellSpillError({ codeCell: codeCellCore, onClick }: { codeCell: JsCodeCell; onClick: () => void }) { + const x = Number(codeCellCore.x); + const y = Number(codeCellCore.y); + const codeCell: CodeCell = useMemo( + () => ({ + sheetId: sheets.current, + pos: { x, y }, + language: codeCellCore.language, + }), + [codeCellCore.language, x, y] + ); -
-
There was an error running the code in this cell.
-
{codeCell.std_err}
+ const evaluationResult = useMemo( + () => (codeCellCore.evaluation_result ? JSON.parse(codeCellCore.evaluation_result) : {}), + [codeCellCore.evaluation_result] + ); + + const spillError = codeCellCore.spill_error; + if (!spillError) { + return null; + } + + return ( + } + isError + > +

Array output could not expand because it would overwrite existing values.

+ +

+ To fix: remove content in {pluralize('cell', spillError.length)}{' '} + {spillError.map((pos, index) => ( + + {xyToA1(Number(pos.x), Number(pos.y))} + + {index !== spillError.length - 1 ? (index === spillError.length - 2 ? ', and ' : ', ') : '.'} + + ))}{' '} + Or move this cell. +

+
+ ); +} + +function HoverCellDisplay({ + title, + children, + actions, + isError, +}: { + title: string; + children: ReactNode; + actions?: ReactNode; + isError?: boolean; +}) { + return ( +
+
+ {title} + {actions &&
{actions}
}
+
{children}
+
+ ); +} + +function HoverCellDisplayCode({ language, children }: { language?: string; children: string | undefined }) { + if (!children) { + return null; + } -
{language} Code
-
{codeCell.code_string}
- + const lines = children?.split('\n').length; + return ( +
11 && + "-mb-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-16 after:bg-gradient-to-t after:from-background after:to-transparent after:content-['']" + )} + > +
    + {Array.from({ length: lines }).map((_, index) => ( +
  1. {index + 1}
  2. + ))} +
+
{children}
+
); } + +function HoverCellDisplayError({ children }: { children: string | null }) { + if (!children) { + return null; + } + + return

{children}

; +} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/hoverTooltip/HoverTooltip.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/hoverTooltip/HoverTooltip.tsx index 42d2596e16..704a7819cd 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/hoverTooltip/HoverTooltip.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/hoverTooltip/HoverTooltip.tsx @@ -34,13 +34,11 @@ export const HoverTooltip = () => { setSubtext(undefined); }; - pixiApp.viewport.on('moved', remove); - pixiApp.viewport.on('zoomed', remove); events.on('cursorPosition', remove); + events.on('viewportChanged', remove); return () => { - pixiApp.viewport.off('moved', remove); - pixiApp.viewport.off('zoomed', remove); events.off('cursorPosition', remove); + events.off('viewportChanged', remove); }; }, []); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts index 7ab8a7c76e..a806891a0a 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/htmlCells/HtmlCell.ts @@ -5,7 +5,7 @@ import { JsHtmlOutput } from '@/app/quadratic-core-types'; import { CELL_HEIGHT, CELL_WIDTH } from '@/shared/constants/gridConstants'; import { InteractionEvent, Point, Rectangle } from 'pixi.js'; import { pixiApp } from '../../pixiApp/PixiApp'; -import { Wheel } from '../../pixiOverride/Wheel'; +import { Wheel } from '../../pixiApp/viewport/Wheel'; import { HtmlCellResizing } from './HtmlCellResizing'; // number of screen pixels to trigger the resize cursor diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/InlineEditor.tsx index 01217d8593..daa64107bd 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 { DockToLeftIcon } 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 } = 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/inlineEditorFormula.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorFormula.ts index a7d5eef4f9..fc54303c6e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorFormula.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorFormula.ts @@ -7,9 +7,8 @@ import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEd import { inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; import { inlineEditorMonaco } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { SheetPosTS } from '@/app/gridGL/types/size'; -import { getA1Notation } from '@/app/gridGL/UI/gridHeadings/getA1Notation'; -import { ParseFormulaReturnType } from '@/app/helpers/formulaNotation'; +import type { SheetPosTS } from '@/app/gridGL/types/size'; +import { parseFormulaReturnToCellsAccessed, type ParseFormulaReturnType } from '@/app/helpers/formulaNotation'; import { checkFormula, parseFormula } from '@/app/quadratic-rust-client/quadratic_rust_client'; import { colors } from '@/app/theme/colors'; import { extractCellsFromParseFormula } from '@/app/ui/menus/CodeEditor/hooks/useEditorCellHighlights'; @@ -24,12 +23,17 @@ class InlineEditorFormula { events.on('cursorPosition', this.cursorMoved); } - async cellHighlights(location: SheetPosTS, formula: string) { - const parsed = (await parseFormula(formula, location.x, location.y)) as ParseFormulaReturnType; + cellHighlights(location: SheetPosTS, formula: string) { + const parsed = JSON.parse(parseFormula(formula, location.x, location.y)) as ParseFormulaReturnType; if (parsed) { - pixiApp.cellHighlights.fromFormula(parsed, { x: location.x, y: location.y }, location.sheetId); - - const extractedCells = extractCellsFromParseFormula(parsed, { x: location.x, y: location.y }, location.sheetId); + const cellsAccessed = parseFormulaReturnToCellsAccessed( + parsed, + { x: location.x, y: location.y }, + location.sheetId + ); + pixiApp.cellHighlights.fromCellsAccessed(cellsAccessed); + + const extractedCells = extractCellsFromParseFormula(parsed, { x: location.x, y: location.y }); const newDecorations: monaco.editor.IModelDeltaDecoration[] = []; const cellColorReferences = new Map(); @@ -61,7 +65,7 @@ class InlineEditorFormula { const editorCursorPosition = inlineEditorMonaco.getPosition(); if (editorCursorPosition && range.containsPosition(editorCursorPosition)) { - pixiApp.cellHighlights.setHighlightedCell(index); + pixiApp.cellHighlights.setSelectedCell(index); } }); @@ -119,23 +123,7 @@ class InlineEditorFormula { inlineEditorHandler.cursorIsMoving = true; inlineEditorMonaco.removeSelection(); - let sheet = ''; - if (location.sheetId !== sheets.sheet.id) { - sheet = `'${sheets.sheet.name}'!`; - } - if (cursor.multiCursor) { - let coords = ''; - cursor.multiCursor.forEach((c, i) => { - const start = getA1Notation(c.left, c.top); - const end = getA1Notation(c.right - 1, c.bottom - 1); - coords += `${start}:${end}${i !== cursor.multiCursor!.length - 1 ? ',' : ''}`; - }); - this.insertInsertingCells(`${sheet}${coords}`); - } else { - const location = cursor.getCursor(); - const a1Notation = getA1Notation(location.x, location.y); - this.insertInsertingCells(`${sheet}${a1Notation}`); - } + this.insertInsertingCells(cursor.toA1String()); inlineEditorHandler.sendMultiplayerUpdate(); @@ -151,7 +139,10 @@ 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 diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts index 80985faae4..bca0833437 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler.ts @@ -4,10 +4,9 @@ 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 { inlineEditorMonaco } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco'; +import { CursorMode, inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { inlineEditorMonaco, PADDING_FOR_INLINE_EDITOR } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { SheetPosTS } from '@/app/gridGL/types/size'; @@ -24,9 +23,6 @@ import mixpanel from 'mixpanel-browser'; import { Rectangle } from 'pixi.js'; import { inlineEditorEvents } from './inlineEditorEvents'; -// Minimum amount to scroll viewport when cursor is near the edge. -const MINIMUM_MOVE_VIEWPORT = 50; - class InlineEditorHandler { private div?: HTMLDivElement; @@ -95,42 +91,62 @@ class InlineEditorHandler { keepCursorVisible = () => { if (sheets.sheet.id !== this.location?.sheetId || !this.showing) return; - const { position, bounds } = inlineEditorMonaco.getEditorSizing(); + const sheetRectangle = pixiApp.getViewportRectangle(); + const scale = pixiApp.viewport.scale.x; const canvas = pixiApp.canvas.getBoundingClientRect(); - const cursor = position.left + bounds.left; - const worldCursorTop = pixiApp.viewport.toWorld(cursor, bounds.top - canvas.top); - const worldCursorBottom = pixiApp.viewport.toWorld(cursor, bounds.bottom - canvas.top); - const viewportBounds = pixiApp.viewport.getVisibleBounds(); - - if ( - intersects.rectangleRectangle( - viewportBounds, - new Rectangle( - worldCursorTop.x, - worldCursorTop.y, - worldCursorBottom.x - worldCursorTop.x, - worldCursorBottom.y - worldCursorTop.y - ) - ) - ) { - return; - } - - let x = 0, - y = 0; - if (worldCursorTop.x > viewportBounds.right) { - x = viewportBounds.right - (worldCursorTop.x + MINIMUM_MOVE_VIEWPORT); - } else if (worldCursorTop.x < viewportBounds.left) { - x = viewportBounds.left + MINIMUM_MOVE_VIEWPORT - worldCursorTop.x; - } - if (worldCursorBottom.y > viewportBounds.bottom) { - y = viewportBounds.bottom - (worldCursorBottom.y + MINIMUM_MOVE_VIEWPORT); - } else if (worldCursorTop.y < viewportBounds.top) { - y = viewportBounds.top + MINIMUM_MOVE_VIEWPORT - worldCursorTop.y; + + // calculate inline editor rectangle, factoring in scale + const { bounds, position } = inlineEditorMonaco.getEditorSizing(); + const editorWidth = this.width * scale; + const editorTopLeft = pixiApp.viewport.toWorld(bounds.left - canvas.left, bounds.top - canvas.top); + const editorBottomRight = pixiApp.viewport.toWorld( + bounds.left + editorWidth - canvas.left, + bounds.bottom - canvas.top + ); + const editorRectangle = new Rectangle( + editorTopLeft.x, + editorTopLeft.y, + editorBottomRight.x - editorTopLeft.x, + editorBottomRight.y - editorTopLeft.y + ); + + let x = 0; + if (editorRectangle.left - PADDING_FOR_INLINE_EDITOR <= sheetRectangle.left) { + x = sheetRectangle.left + -(editorRectangle.left - PADDING_FOR_INLINE_EDITOR); + } else if (editorRectangle.right + PADDING_FOR_INLINE_EDITOR >= sheetRectangle.right) { + x = sheetRectangle.right - (editorRectangle.right + PADDING_FOR_INLINE_EDITOR); + } + + let y = 0; + // check if the editor is too tall to fit in the viewport + if (editorRectangle.height + PADDING_FOR_INLINE_EDITOR <= sheetRectangle.height) { + // keep the editor in view + if (editorRectangle.top - PADDING_FOR_INLINE_EDITOR <= sheetRectangle.top) { + y = sheetRectangle.top - (editorRectangle.top - PADDING_FOR_INLINE_EDITOR); + } else if (editorRectangle.bottom + PADDING_FOR_INLINE_EDITOR >= sheetRectangle.bottom) { + y = sheetRectangle.bottom - (editorRectangle.bottom + PADDING_FOR_INLINE_EDITOR); + } + } else { + // just keep the cursor in view + const cursorTop = pixiApp.viewport.toWorld( + position.left * scale + bounds.left - canvas.left, + position.top * scale + bounds.top - canvas.top + ); + const cursorBottom = pixiApp.viewport.toWorld( + position.left * scale + bounds.left - canvas.left, + position.top * scale + position.height * scale + bounds.top - canvas.top + ); + if (cursorTop.y < sheetRectangle.top) { + y = sheetRectangle.top - cursorTop.y; + } else if (cursorBottom.y > sheetRectangle.bottom) { + y = sheetRectangle.bottom - cursorBottom.y; + } } + if (x || y) { - pixiApp.viewport.x += x; - pixiApp.viewport.y += y; + const { width, height } = pixiApp.headings.headingSize; + pixiApp.viewport.x = Math.min(pixiApp.viewport.x + x * scale, width); + pixiApp.viewport.y = Math.min(pixiApp.viewport.y + y * scale, height); pixiApp.setViewportDirty(); } }; @@ -155,7 +171,7 @@ class InlineEditorHandler { }; // Handler for the changeInput event. - private changeInput = async (input: boolean, initialValue?: string) => { + private changeInput = async (input: boolean, initialValue?: string, cursorMode?: CursorMode) => { if (!input && !this.open) return; if (initialValue) { @@ -171,7 +187,7 @@ class InlineEditorHandler { if (input) { this.open = true; const sheet = sheets.sheet; - const cursor = sheet.cursor.getCursor(); + const cursor = sheet.cursor.position; this.location = { sheetId: sheet.id, x: cursor.x, @@ -181,7 +197,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') { @@ -189,13 +205,26 @@ class InlineEditorHandler { changeToFormula = true; } else { value = (await quadraticCore.getEditCell(this.location.sheetId, this.location.x, this.location.y)) || ''; + changeToFormula = false; + } + } + + if (cursorMode === undefined) { + if (changeToFormula) { + cursorMode = value.length > 1 ? CursorMode.Edit : CursorMode.Enter; + } else { + cursorMode = value ? CursorMode.Edit : CursorMode.Enter; } } + pixiAppSettings.setInlineEditorState?.((prev) => ({ + ...prev, + editMode: cursorMode === CursorMode.Edit, + })); + this.formatSummary = await quadraticCore.getCellFormatSummary( this.location.sheetId, this.location.x, - this.location.y, - true + this.location.y ); this.temporaryBold = this.formatSummary?.bold || undefined; this.temporaryItalic = this.formatSummary?.italic || undefined; @@ -341,8 +370,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; @@ -467,16 +496,8 @@ class InlineEditorHandler { // Update Grid Interaction state, reset input value state if (deltaX || deltaY) { - const position = sheets.sheet.cursor.cursorPosition; - sheets.sheet.cursor.changePosition({ - multiCursor: null, - columnRow: null, - cursorPosition: { - x: position.x + deltaX, - y: position.y + deltaY, - }, - ensureVisible: true, - }); + const position = sheets.sheet.cursor.position; + sheets.sheet.cursor.moveTo(position.x + deltaX, position.y + deltaY); } // Set focus back to Grid @@ -485,17 +506,20 @@ 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'); } + if (!pixiAppSettings.setCodeEditorState) { + throw new Error('Expected setEditorInteractionState to be defined in openCodeEditor'); + } if (!this.location) { throw new Error('Expected location to be defined in openCodeEditor'); } const { sheetId, x, y } = this.location; pixiAppSettings.setCodeEditorState({ ...pixiAppSettings.codeEditorState, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard.ts index 9d5e1876d5..734b4b702c 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'; @@ -15,17 +16,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 CursorMode { + Enter, + Edit, +} + class InlineEditorKeyboard { escapeBackspacePressed = false; + cursorMode: CursorMode = CursorMode.Enter; 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.cursorMode === CursorMode.Enter) { const column = inlineEditorMonaco.getCursorColumn(); e.stopPropagation(); e.preventDefault(); @@ -39,9 +46,10 @@ class InlineEditorKeyboard { inlineEditorHandler.close(isRight ? 1 : -1, 0, false); } } - } else { - const column = inlineEditorMonaco.getCursorColumn(); - if (column === target) { + } + // text + else { + if (this.cursorMode === CursorMode.Enter) { e.stopPropagation(); e.preventDefault(); if (!(await this.handleValidationError())) { @@ -60,12 +68,13 @@ class InlineEditorKeyboard { return; } + // formula if (inlineEditorHandler.isEditingFormula()) { e.stopPropagation(); e.preventDefault(); if (inlineEditorHandler.cursorIsMoving) { keyboardPosition(e); - } else { + } 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 @@ -87,11 +96,15 @@ class InlineEditorKeyboard { return; } } - } else { - e.stopPropagation(); - e.preventDefault(); - if (!(await this.handleValidationError())) { - inlineEditorHandler.close(0, isDown ? 1 : -1, false); + } + // text + else { + if (this.cursorMode === CursorMode.Enter) { + e.stopPropagation(); + e.preventDefault(); + if (!(await this.handleValidationError())) { + inlineEditorHandler.close(0, isDown ? 1 : -1, false); + } } } }; @@ -113,6 +126,13 @@ class InlineEditorKeyboard { return false; } + toggleArrowMode = () => { + pixiAppSettings.setInlineEditorState?.((prev) => ({ + ...prev, + editMode: !prev.editMode, + })); + }; + // 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) => { @@ -133,6 +153,14 @@ class InlineEditorKeyboard { this.escapeBackspacePressed = false; } + const position = inlineEditorMonaco.getPosition(); + if (e.code === 'Equal' && position.lineNumber === 1 && position.column === 1) { + pixiAppSettings.setInlineEditorState?.((prev) => ({ + ...prev, + editMode: false, + })); + } + // Escape key if (matchShortcut(Action.CloseInlineEditor, e)) { e.stopPropagation(); @@ -167,6 +195,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) { @@ -258,7 +292,7 @@ class InlineEditorKeyboard { inlineEditorHandler.location.x, inlineEditorHandler.location.y ); - quadraticCore.setCellItalic(selection, !!inlineEditorHandler.temporaryItalic); + quadraticCore.setItalic(selection, !!inlineEditorHandler.temporaryItalic); } } @@ -273,7 +307,7 @@ class InlineEditorKeyboard { inlineEditorHandler.location.x, inlineEditorHandler.location.y ); - quadraticCore.setCellBold(selection, !!inlineEditorHandler.temporaryBold); + quadraticCore.setBold(selection, !!inlineEditorHandler.temporaryBold); } } @@ -288,7 +322,7 @@ class InlineEditorKeyboard { inlineEditorHandler.location.x, inlineEditorHandler.location.y ); - quadraticCore.setCellUnderline(selection, !!inlineEditorHandler.temporaryUnderline); + quadraticCore.setUnderline(selection, !!inlineEditorHandler.temporaryUnderline); } } @@ -303,16 +337,43 @@ class InlineEditorKeyboard { inlineEditorHandler.location.x, inlineEditorHandler.location.y ); - quadraticCore.setCellStrikeThrough(selection, !!inlineEditorHandler.temporaryStrikeThrough); + quadraticCore.setStrikeThrough(selection, !!inlineEditorHandler.temporaryStrikeThrough); } } + // 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(); + }); + } + + // show command palette + else if (matchShortcut(Action.ShowCommandPalette, e)) { + e.stopPropagation(); + e.preventDefault(); + inlineEditorHandler.close(0, 0, false).then(() => { + defaultActionSpec[Action.ShowCommandPalette].run(); + }); + } + // trigger cell type menu else if (matchShortcut(Action.ShowCellTypeMenu, e) && inlineEditorMonaco.get().length === 0) { e.preventDefault(); e.stopPropagation(); pixiAppSettings.changeInput(false); - const cursor = sheets.sheet.cursor.getCursor(); + const cursor = sheets.sheet.cursor.position; pixiAppSettings.setEditorInteractionState?.({ ...pixiAppSettings.editorInteractionState, showCellTypeMenu: true, @@ -358,19 +419,12 @@ class InlineEditorKeyboard { if (!location) return; inlineEditorHandler.cursorIsMoving = false; - pixiApp.cellHighlights.clearHighlightedCell(); + pixiApp.cellHighlights.clearSelectedCell(); const editingSheet = sheets.getById(location.sheetId); if (!editingSheet) { throw new Error('Expected editingSheet to be defined in resetKeyboardPosition'); } - const position = { x: location.x, y: location.y }; - editingSheet.cursor.changePosition({ - cursorPosition: position, - multiCursor: null, - columnRow: null, - keyboardMovePosition: position, - ensureVisible: true, - }); + editingSheet.cursor.moveTo(location.x, location.y); if (sheets.sheet.id !== location.sheetId) { sheets.current = location.sheetId; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts index 857f411bda..d38322105d 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/inlineEditor/inlineEditorMonaco.ts @@ -2,6 +2,8 @@ import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; import { inlineEditorKeyboard } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +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'; @@ -10,6 +12,7 @@ import { FONT_SIZE, LINE_HEIGHT } from '@/app/web-workers/renderWebWorker/worker import * as monaco from 'monaco-editor'; import { editor } from 'monaco-editor'; import DefaultEditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; +import JsonEditorWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import TsEditorWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; import { inlineEditorEvents } from './inlineEditorEvents'; @@ -21,6 +24,8 @@ window.MonacoEnvironment = { case 'typescript': case 'javascript': return new TsEditorWorker({ name: label }); + case 'json': + return new JsonEditorWorker({ name: label }); default: return new DefaultEditorWorker({ name: label }); } @@ -31,6 +36,9 @@ window.MonacoEnvironment = { // (determined by experimentation). const PADDING_FOR_GROWING_HORIZONTALLY = 20; +// Padding for the inline editor when calling keepCursorVisible, to keep the editor/cursor in view. +export const PADDING_FOR_INLINE_EDITOR = 5; + class InlineEditorMonaco { editor?: editor.IStandaloneCodeEditor; private suggestionWidgetShowing: boolean = false; @@ -54,12 +62,12 @@ class InlineEditorMonaco { } // Gets the value of the inline editor. - get(): string { + get = (): string => { if (!this.editor) { throw new Error('Expected editor to be defined in getValue'); } return this.editor.getValue(); - } + }; // Sets the value of the inline editor and moves the cursor to the end. set(s: string, select?: boolean | number) { @@ -147,10 +155,22 @@ class InlineEditorMonaco { padding: { top: paddingTop, bottom: 0 }, }); - // set final width and height const scrollWidth = textarea.scrollWidth; width = textWrap === 'wrap' ? width : Math.max(width, scrollWidth + PADDING_FOR_GROWING_HORIZONTALLY); height = Math.max(contentHeight, height); + + const viewportRectangle = pixiApp.getViewportRectangle(); + const maxWidthDueToViewport = viewportRectangle.width - 2 * PADDING_FOR_INLINE_EDITOR; + if (width > maxWidthDueToViewport) { + textWrap = 'wrap'; + width = maxWidthDueToViewport; + this.editor.updateOptions({ + wordWrap: textWrap === 'wrap' ? 'on' : 'off', + padding: { top: paddingTop, bottom: 0 }, + }); + } + + // set final width and height this.editor.layout({ width, height }); return { width, height }; @@ -265,7 +285,7 @@ class InlineEditorMonaco { return this.editor.getValue().length + 1; } - getPosition(): monaco.Position { + getPosition = (): monaco.Position => { if (!this.editor) { throw new Error('Expected editor to be defined in getPosition'); } @@ -274,7 +294,7 @@ class InlineEditorMonaco { throw new Error('Expected position to be defined in getPosition'); } return position; - } + }; getCursorColumn(): number { if (!this.editor) { @@ -313,13 +333,13 @@ class InlineEditorMonaco { return { bounds, position }; } - getNonWhitespaceCharBeforeCursor(): string { - const formula = inlineEditorMonaco.get(); + getNonWhitespaceCharBeforeCursor = (): string => { + const formula = this.get(); // If there is a selection then use the start of the selection; otherwise // use the cursor position. - const selection = inlineEditorMonaco.editor?.getSelection()?.getStartPosition(); - const position = selection ?? inlineEditorMonaco.getPosition(); + const selection = this.editor?.getSelection()?.getStartPosition(); + const position = selection ?? this.getPosition(); const line = formula.split('\n')[position.lineNumber - 1]; const lastCharacter = @@ -328,7 +348,7 @@ class InlineEditorMonaco { .trimEnd() .at(-1) ?? ''; return lastCharacter; - } + }; createDecorationsCollection(newDecorations: editor.IModelDeltaDecoration[]) { if (!this.editor) { @@ -408,6 +428,8 @@ class InlineEditorMonaco { language: inlineEditorHandler.formula ? 'formula' : 'inline-editor', }); + this.disableKeybindings(); + interface SuggestController { widget: { value: { onDidShow: (fn: () => void) => void; onDidHide: (fn: () => void) => void } }; } @@ -427,7 +449,7 @@ class InlineEditorMonaco { monaco.languages.registerCompletionItemProvider('inline-editor', { provideCompletionItems: (model, position) => { const lowerCase = this.get().toLowerCase(); - if (!this.autocompleteList?.find((t) => t.toLowerCase().startsWith(lowerCase))) { + if (!this.autocompleteList?.find((t) => t.toLowerCase().startsWith(lowerCase) && t.length > lowerCase.length)) { this.autocompleteSuggestionShowing = false; return; } @@ -450,7 +472,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, editMode: true })); + }); this.editor.onDidChangeModelContent(() => inlineEditorEvents.emit('valueChanged', this.get())); } @@ -478,6 +503,38 @@ class InlineEditorMonaco { } this.editor.trigger(null, 'editor.action.inlineSuggest.trigger', null); } + + disableKeybindings() { + editor.addKeybindingRules([ + { + keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyF, + }, + { + keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG, + }, + { + keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL, + }, + { + keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyL, + }, + { + keybinding: monaco.KeyCode.F1, + }, + { + keybinding: monaco.KeyCode.F3, + }, + { + keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.F3, + }, + { + keybinding: monaco.KeyMod.Shift | monaco.KeyCode.F3, + }, + { + keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.F3, + }, + ]); + } } export const inlineEditorMonaco = new InlineEditorMonaco(); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerCursor/MultiplayerCursors.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerCursor/MultiplayerCursors.tsx index f94b541101..cb8d29526d 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerCursor/MultiplayerCursors.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerCursor/MultiplayerCursors.tsx @@ -23,15 +23,13 @@ export const MultiplayerCursors = (props: Props) => { events.on('multiplayerCursor', updatePlayersTrigger); events.on('changeSheet', updatePlayersTrigger); window.addEventListener('resize', updatePlayersTrigger); - pixiApp.viewport.on('moved', updatePlayersTrigger); - pixiApp.viewport.on('zoomed', updatePlayersTrigger); + events.on('viewportChangedReady', updatePlayersTrigger); return () => { events.off('multiplayerChangeSheet', updatePlayersTrigger); events.off('multiplayerCursor', updatePlayersTrigger); events.off('changeSheet', updatePlayersTrigger); window.removeEventListener('resize', updatePlayersTrigger); - pixiApp.viewport.off('moved', updatePlayersTrigger); - pixiApp.viewport.off('zoomed', updatePlayersTrigger); + events.off('viewportChangedReady', updatePlayersTrigger); }; }, []); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdit.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdit.tsx index 0668deb6e9..9ece2f1641 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdit.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdit.tsx @@ -19,7 +19,7 @@ export const MultiplayerCellEdit = (props: Props) => { const [formatting, setFormatting] = useState(); useEffect(() => { (async () => { - const format = await quadraticCore.getCellFormatSummary(sheet.id, input.location.x, input.location.y, true); + const format = await quadraticCore.getCellFormatSummary(sheet.id, input.location.x, input.location.y); setFormatting(format); })(); }, [input.location, sheet.id]); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdits.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdits.tsx index c0f9046644..57d9e0e2dc 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdits.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/multiplayerInput/MultiplayerCellEdits.tsx @@ -19,13 +19,14 @@ export const MultiplayerCellEdits = () => { const updateMultiplayerCellEdit = (cellEdit: CellEdit, player: MultiplayerUser) => { setMultiplayerCellInput((prev) => { if (player.x === undefined || player.y === undefined || !player.parsedSelection) return prev; + const cursor = player.parsedSelection.getCursor(); const updatedCellEdit: MultiplayerCell = { sessionId: player.session_id, sheetId: player.sheet_id, cellEdit, location: { - x: player.parsedSelection.cursorPosition.x, - y: player.parsedSelection.cursorPosition.y, + x: cursor.x, + y: cursor.y, sheetId: player.sheet_id, }, playerColor: player.colorString, diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/usePositionCellMessage.ts b/quadratic-client/src/app/gridGL/HTMLGrid/usePositionCellMessage.ts index 59fdc4aa89..f3a3f7bbb4 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/usePositionCellMessage.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/usePositionCellMessage.ts @@ -48,18 +48,27 @@ export const usePositionCellMessage = (props: Props): PositionCellMessage => { const leftHeadingScaled = leftHeading / scale; if (side === 'vertical') { + // align left to cell left let left = offsets.left - (centerHorizontal ? offsetWidth / 2 : 0); + // make sure left does not go to the right of the viewport left = Math.min(left, bounds.right - offsetWidth); + // make sure left does not go to the left of the viewport left = Math.max(left, bounds.left + leftHeadingScaled); setLeft(left); + // align top to cell bottom let top = offsets.bottom; + // if the model is to be displayed above the cell if (forceTop) { + // align bottom to cell top top = offsets.top - offsetHeight; + // if it goes above the heading, switch to displaying below the cell if (top < bounds.top + topHeadingScaled) { top = offsets.bottom; } } + // make sure top does not go above the heading + top = Math.max(top, bounds.top + topHeadingScaled); setTop(top); } else { // checks whether the inline editor or dropdown is open; if so, always @@ -77,27 +86,28 @@ export const usePositionCellMessage = (props: Props): PositionCellMessage => { setLeft(offsets.right); } - // only box going up if it doesn't fit. - if (offsets.top + offsetHeight < bounds.bottom) { - // box going down - setTop(offsets.top); - } else { - // box going up - setTop(offsets.bottom - offsetHeight); + // align top to cell top + let top = offsets.top; + // if the modal is too tall, align bottom to cell bottom + if (top + offsetHeight > bounds.bottom) { + top = offsets.bottom - offsetHeight; } + // make sure top does not go above the heading + top = Math.max(top, bounds.top + topHeadingScaled); + setTop(top); } }; updatePosition(); inlineEditorEvents.on('status', updatePosition); events.on('cursorPosition', updatePosition); - pixiApp.viewport.on('moved', updatePosition); + events.on('viewportChangedReady', updatePosition); window.addEventListener('resize', updatePosition); return () => { inlineEditorEvents.off('status', updatePosition); events.off('cursorPosition', updatePosition); - pixiApp.viewport.off('moved', updatePosition); + events.off('viewportChangedReady', updatePosition); window.removeEventListener('resize', updatePosition); }; }, [centerHorizontal, div, annotationState, forceLeft, forceTop, leftHeading, offsets, side, topHeading]); diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationCheckbox.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationCheckbox.tsx index d0fe5e2e67..e9d8c68d11 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationCheckbox.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationCheckbox.tsx @@ -1,8 +1,8 @@ -import { useEffect } from 'react'; -import { HtmlValidationsData } from './useHtmlValidations'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +import { HtmlValidationsData } from '@/app/gridGL/HTMLGrid/validations/useHtmlValidations'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; +import { useEffect } from 'react'; interface Props { htmlValidationsData: HtmlValidationsData; diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationList.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationList.tsx index fa872ff25b..4d8c32dc66 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationList.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/validations/HtmlValidationList.tsx @@ -6,7 +6,7 @@ import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEd import { useInlineEditorStatus } from '@/app/gridGL/HTMLGrid/inlineEditor/useInlineEditorStatus'; import { HtmlValidationsData } from '@/app/gridGL/HTMLGrid/validations/useHtmlValidations'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { Coordinate } from '@/app/gridGL/types/size'; +import { JsCoordinate } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { cn } from '@/shared/shadcn/utils'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -26,7 +26,7 @@ export const HtmlValidationList = (props: Props) => { const [list, setList] = useState(); - const listCoordinate = useRef(); + const listCoordinate = useRef(); const inlineEditorStatus = useInlineEditorStatus(); useEffect(() => { @@ -43,13 +43,17 @@ export const HtmlValidationList = (props: Props) => { const [filter, setFilter] = useState(); useEffect(() => { - inlineEditorEvents.on('valueChanged', (value) => { + const updateFilter = (value: string) => { if (value.trim()) { setFilter(value); } else { setFilter(undefined); } - }); + }; + inlineEditorEvents.on('valueChanged', updateFilter); + return () => { + inlineEditorEvents.off('valueChanged', updateFilter); + }; }, []); useEffect(() => { @@ -77,7 +81,7 @@ export const HtmlValidationList = (props: Props) => { const changeStatus = (opened: boolean) => { if (opened) { - updateShowDropdown(sheets.sheet.cursor.cursorPosition.x, sheets.sheet.cursor.cursorPosition.y, true); + updateShowDropdown(sheets.sheet.cursor.position.x, sheets.sheet.cursor.position.y, true); } }; inlineEditorEvents.on('status', changeStatus); @@ -92,8 +96,8 @@ export const HtmlValidationList = (props: Props) => { (value: string) => { quadraticCore.setCellValue( sheets.sheet.id, - sheets.sheet.cursor.cursorPosition.x, - sheets.sheet.cursor.cursorPosition.y, + sheets.sheet.cursor.position.x, + sheets.sheet.cursor.position.y, value, sheets.getCursorPosition() ); @@ -122,12 +126,10 @@ export const HtmlValidationList = (props: Props) => { } } else if (key === 'ArrowLeft' || key === 'ArrowRight') { changeValue(list[index]); - sheets.sheet.cursor.changePosition({ - cursorPosition: { - x: sheets.sheet.cursor.cursorPosition.x + (key === 'ArrowLeft' ? -1 : 1), - y: sheets.sheet.cursor.cursorPosition.y, - }, - }); + sheets.sheet.cursor.moveTo( + sheets.sheet.cursor.position.x + (key === 'ArrowLeft' ? -1 : 1), + sheets.sheet.cursor.position.y + ); } else if (key === 'Escape') { setAnnotationState(undefined); } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/validations/translateValidationError.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/validations/translateValidationError.tsx index 97c5cd1251..b79da99a04 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/validations/translateValidationError.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/validations/translateValidationError.tsx @@ -1,4 +1,4 @@ -import { getSelectionString } from '@/app/grid/sheet/selection'; +import { sheets } from '@/app/grid/controller/Sheets'; import { Validation } from '@/app/quadratic-core-types'; import { numberToDate, numberToTime } from '@/app/quadratic-rust-client/quadratic_rust_client'; import { joinWithOr } from '@/shared/utils/text'; @@ -157,7 +157,7 @@ export const translateValidationError = (validation: Validation): JSX.Element | return (
Value {verb} be one of the values in the selected range{' '} - {getSelectionString(validation.rule.List.source.Selection)}. + {sheets.sheet.cursor.toA1String()}.
); } diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/validations/useHtmlValidations.ts b/quadratic-client/src/app/gridGL/HTMLGrid/validations/useHtmlValidations.ts index 65b5f2ae19..8b8ace8c7a 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/validations/useHtmlValidations.ts +++ b/quadratic-client/src/app/gridGL/HTMLGrid/validations/useHtmlValidations.ts @@ -4,8 +4,7 @@ import { hasPermissionToEditFile } from '@/app/actions'; import { editorInteractionStatePermissionsAtom } from '@/app/atoms/editorInteractionStateAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; -import { Coordinate } from '@/app/gridGL/types/size'; -import { Validation } from '@/app/quadratic-core-types'; +import { JsCoordinate, Validation } from '@/app/quadratic-core-types'; import { validationRuleSimple, ValidationRuleSimple } from '@/app/ui/menus/Validations/Validation/validationType'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Rectangle } from 'pixi.js'; @@ -16,7 +15,7 @@ export interface HtmlValidationsData { offsets?: Rectangle; validation?: Validation; validationRuleSimple: ValidationRuleSimple; - location?: Coordinate; + location?: JsCoordinate; readOnly: boolean; } @@ -27,17 +26,17 @@ export const useHtmlValidations = (): HtmlValidationsData => { const [offsets, setOffsets] = useState(); const [validation, setValidation] = useState(); const [validationType, setValidationType] = useState(''); - const [location, setLocation] = useState(); + const [location, setLocation] = useState(); // Change in cursor position triggers update of validation useEffect(() => { const updateCursor = async () => { - if (sheets.sheet.cursor.multiCursor) { + if (sheets.sheet.cursor.isMultiCursor()) { setValidation(undefined); setValidationType(''); return; } - const { x, y } = sheets.sheet.cursor.cursorPosition; + const { x, y } = sheets.sheet.cursor.position; setLocation({ x, y }); const validation = await quadraticCore.getValidationFromPos(sheets.sheet.id, x, y); @@ -51,7 +50,7 @@ export const useHtmlValidations = (): HtmlValidationsData => { setValidation(validation); setValidationType(validationRuleSimple(validation)); - + setLocation({ x, y }); const offsets = sheets.sheet.getCellOffsets(x, y); setOffsets(offsets); }; diff --git a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx index d9dbe54c79..676de0f055 100644 --- a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx +++ b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx @@ -1,3 +1,4 @@ +import { aiAnalystAtom } from '@/app/atoms/aiAnalystAtom'; import { codeEditorAtom, codeEditorShowCodeEditorAtom } from '@/app/atoms/codeEditorAtom'; import { contextMenuAtom } from '@/app/atoms/contextMenuAtom'; import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAtom'; @@ -59,10 +60,17 @@ export const PixiAppEffects = () => { pixiAppSettings.updateGridPanMode(gridPanMode, setGridPanMode); }, [gridPanMode, setGridPanMode]); +<<<<<<< HEAD const [contextMenu, setContextMenu] = useRecoilState(contextMenuAtom); useEffect(() => { pixiAppSettings.updateContextMenu(contextMenu, setContextMenu); }, [contextMenu, setContextMenu]); +======= + const [aiAnalystState, setAIAnalystState] = useRecoilState(aiAnalystAtom); + useEffect(() => { + pixiAppSettings.updateAIAnalystState(aiAnalystState, setAIAnalystState); + }, [aiAnalystState, setAIAnalystState]); +>>>>>>> origin/qa useEffect(() => { const handleMouseUp = () => { diff --git a/quadratic-client/src/app/gridGL/QuadraticGrid.tsx b/quadratic-client/src/app/gridGL/QuadraticGrid.tsx index e23ad40a8c..42d4a44aa6 100644 --- a/quadratic-client/src/app/gridGL/QuadraticGrid.tsx +++ b/quadratic-client/src/app/gridGL/QuadraticGrid.tsx @@ -36,6 +36,7 @@ export default function QuadraticGrid() { return (
= viewport.left && 0 <= viewport.right) { - this.moveTo(0, viewport.top); - this.lineTo(0, viewport.bottom); - } - if (0 >= viewport.top && 0 <= viewport.bottom) { - this.moveTo(viewport.left, 0); - this.lineTo(viewport.right, 0); - } - } - } -} diff --git a/quadratic-client/src/app/gridGL/UI/Background.ts b/quadratic-client/src/app/gridGL/UI/Background.ts new file mode 100644 index 0000000000..94942cfc0c --- /dev/null +++ b/quadratic-client/src/app/gridGL/UI/Background.ts @@ -0,0 +1,37 @@ +import { Graphics } from 'pixi.js'; +import { pixiApp } from '../pixiApp/PixiApp'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { colors } from '@/app/theme/colors'; + +export class Background extends Graphics { + update(dirty: boolean) { + if (dirty) { + this.clear(); + const clamp = sheets.sheet.clamp; + const bounds = pixiApp.viewport.getVisibleBounds(); + const left = Math.max(bounds.left, clamp.left); + const top = Math.max(bounds.top, clamp.top); + const right = Math.min(bounds.right, clamp.right); + const bottom = Math.min(bounds.bottom, clamp.bottom); + + // draw normal background + this.beginFill(colors.gridBackground); + this.drawRect(left, top, right - left, bottom - top); + this.endFill(); + + // draw out of bounds to the left + if (left > bounds.left) { + this.beginFill(colors.gridBackgroundOutOfBounds); + this.drawRect(bounds.left, top, left - bounds.left, bottom - top); + this.endFill(); + } + + // draw out of bounds to the top + if (top > bounds.top) { + this.beginFill(colors.gridBackgroundOutOfBounds); + this.drawRect(bounds.left, bounds.top, right - bounds.left, top - bounds.top); + this.endFill(); + } + } + } +} diff --git a/quadratic-client/src/app/gridGL/UI/Cursor.ts b/quadratic-client/src/app/gridGL/UI/Cursor.ts index 89e91ddcb7..8f79efafff 100644 --- a/quadratic-client/src/app/gridGL/UI/Cursor.ts +++ b/quadratic-client/src/app/gridGL/UI/Cursor.ts @@ -5,8 +5,9 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; -import { Coordinate } from '@/app/gridGL/types/size'; -import { drawColumnRowCursor, drawMultiCursor } from '@/app/gridGL/UI/drawCursor'; +import { drawFiniteSelection, drawInfiniteSelection } from '@/app/gridGL/UI/drawCursor'; +import { getCSSVariableTint } from '@/app/helpers/convertColor'; +import { CellRefRange, JsCoordinate } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; import { Container, Graphics, Rectangle, Sprite } from 'pixi.js'; @@ -16,9 +17,9 @@ 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 }; +const CURSOR_CELL_DEFAULT_VALUE = new Rectangle(0, 0, 0, 0); // outside border when editing the cell const CURSOR_INPUT_ALPHA = 0.333; @@ -27,8 +28,8 @@ export class Cursor extends Container { indicator: Rectangle; dirty = true; - startCell: CursorCell; - endCell: CursorCell; + startCell: Rectangle; + endCell: Rectangle; cursorRectangle?: Rectangle; @@ -45,7 +46,7 @@ export class Cursor extends Container { } // redraws corners if there is an error - private drawError(cell: Coordinate, x: number, y: number, width: number, height: number) { + private drawError(cell: JsCoordinate, x: number, y: number, width: number, height: number) { const error = pixiApp.cellsSheets.current?.getErrorMarker(cell.x, cell.y); if (error) { if (error.triangle) { @@ -73,7 +74,7 @@ export class Cursor extends Container { const cursor = sheet.cursor; const { viewport } = pixiApp; const { codeEditorState } = pixiAppSettings; - const cell = cursor.cursorPosition; + const cell = cursor.position; const showInput = pixiAppSettings.input.show; if (cursor.onlySingleSelection() && pixiApp.cellsSheet().tables.isHtmlOrImage(cell)) { @@ -87,8 +88,8 @@ export class Cursor extends Container { const indicatorSize = hasPermissionToEditFile(pixiAppSettings.editorInteractionState.permissions) && (!pixiAppSettings.codeEditorState.showCodeEditor || - cursor.cursorPosition.x !== codeCell.pos.x || - cursor.cursorPosition.y !== codeCell.pos.y) + cursor.position.x !== codeCell.pos.x || + cursor.position.y !== codeCell.pos.y) ? Math.max(INDICATOR_SIZE / viewport.scale.x, 4) : 0; this.indicator.width = this.indicator.height = indicatorSize; @@ -98,16 +99,14 @@ export class Cursor extends Container { const inlineShowing = inlineEditorHandler.getShowing(); if (showInput) { 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); } } else { - if (!cursor.multiCursor) { + if (!cursor.isMultiCursor()) { indicatorOffset = indicatorSize / 2 + indicatorPadding; } } @@ -146,34 +145,12 @@ export class Cursor extends Container { } } - private drawMultiCursor() { + private drawFiniteCursor(ranges: CellRefRange[]) { const sheet = sheets.sheet; const { cursor } = sheet; - this.startCell = sheet.getCellOffsets(cursor.cursorPosition.x, cursor.cursorPosition.y); - if (cursor.multiCursor) { - drawMultiCursor(this.graphics, pixiApp.accentColor, FILL_ALPHA, cursor.multiCursor); - - // endCell is only interesting for one multiCursor since we use it to draw - // the indicator, which is only active for one multiCursor - const multiCursor = cursor.multiCursor[0]; - const startCell = sheet.getCellOffsets(multiCursor.left, multiCursor.top); - this.endCell = sheet.getCellOffsets(multiCursor.right - 1, multiCursor.bottom - 1); - this.cursorRectangle = new Rectangle( - startCell.x, - startCell.y, - this.endCell.x + this.endCell.width - startCell.x, - this.endCell.y + this.endCell.height - startCell.y - ); - } else { - this.endCell = sheet.getCellOffsets(cursor.cursorPosition.x, cursor.cursorPosition.y); - this.cursorRectangle = new Rectangle( - this.startCell.x, - this.startCell.y, - this.endCell.x + this.endCell.width - this.startCell.x, - this.endCell.y + this.endCell.height - this.startCell.y - ); - } + this.startCell = sheet.getCellOffsets(cursor.position.x, cursor.position.y); + drawFiniteSelection(this.graphics, pixiApp.accentColor, FILL_ALPHA, ranges); } private drawCursorIndicator() { @@ -183,7 +160,10 @@ export class Cursor extends Container { if (viewport.scale.x > HIDE_INDICATORS_BELOW_SCALE) { const { codeEditorState } = pixiAppSettings; const codeCell = codeEditorState.codeCell; - const cell = cursor.cursorPosition; + const cell = cursor.position; + + const endCell = cursor.bottomRight; + this.endCell = sheets.sheet.getCellOffsets(endCell.x, endCell.y); // draw cursor indicator const indicatorSize = Math.max(INDICATOR_SIZE / viewport.scale.x, 4); @@ -192,13 +172,15 @@ export class Cursor extends Container { this.indicator.x = x - indicatorSize / 2; this.indicator.y = y - indicatorSize / 2; this.graphics.lineStyle(0); + // have cursor color match code editor mode let color = pixiApp.accentColor; if ( inlineEditorHandler.getShowing(cell.x, cell.y) || (codeEditorState.showCodeEditor && codeCell.pos.x === cell.x && codeCell.pos.y === cell.y) - ) + ) { color = pixiApp.accentColor; + } this.graphics.beginFill(color).drawShape(this.indicator).endFill(); } } @@ -208,11 +190,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; @@ -229,32 +227,85 @@ 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 drawInlineCursorModeIndicator() { + const inlineShowing = inlineEditorHandler.getShowing(); + if (!inlineShowing) 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); + 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 + 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); + corners.forEach((corner) => { + this.graphics.drawRect(corner.x, corner.y, indicatorSize, indicatorSize); + }); + this.graphics.endFill(); + } + + private drawUnselectDown() { + const { unselectDown } = pixiApp.pointer.pointerDown; + if (!unselectDown) return; + const foreground = pixiApp.accentColor; + this.graphics.lineStyle({ color: foreground, width: 1 }); + const background = getCSSVariableTint('background'); + this.graphics.beginFill(background, 0.5); + const rectangle = sheets.sheet.getScreenRectangle( + unselectDown.x, + unselectDown.y, + unselectDown.width + 1, + unselectDown.height + 1 + ); + this.graphics.drawShape(rectangle); + 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. update(viewportDirty: boolean) { +<<<<<<< HEAD const columnRow = !!sheets.sheet.cursor.columnRow; +======= + const cursor = sheets.sheet.cursor; + const columnRow = cursor.isColumnRow(); +>>>>>>> origin/qa if (this.dirty || (viewportDirty && columnRow)) { this.dirty = false; this.graphics.clear(); while (this.children.length > 1) { this.removeChildAt(1); } - if (!columnRow && !inlineEditorHandler.isEditingFormula()) { + if (!inlineEditorHandler.isEditingFormula()) { this.drawCursor(); } this.drawCodeCursor(); + this.drawInlineCursorModeIndicator(); + if (!pixiAppSettings.input.show) { +<<<<<<< HEAD const cursorPosition = sheets.sheet.cursor.cursorPosition; this.drawMultiCursor(); const columnRow = sheets.sheet.cursor.columnRow; @@ -268,10 +319,26 @@ export class Cursor extends Container { }); } if (sheets.sheet.cursor.onlySingleSelection() && !pixiApp.cellsSheet().tables.isHtmlOrImage(cursorPosition)) { +======= + const finiteRanges: CellRefRange[] = cursor.getFiniteRanges(); + this.drawFiniteCursor(finiteRanges); + const infiniteRanges: CellRefRange[] = cursor.getInfiniteRanges(); + drawInfiniteSelection({ + g: this.graphics, + color: pixiApp.accentColor, + alpha: FILL_ALPHA, + ranges: infiniteRanges, + }); + if (!columnRow && cursor.rangeCount() === 1 && !cursor.getInfiniteRanges().length) { +>>>>>>> origin/qa this.drawCursorIndicator(); } } + if (pixiApp.pointer.pointerDown.unselectDown) { + this.drawUnselectDown(); + } + pixiApp.setViewportDirty(); } } diff --git a/quadratic-client/src/app/gridGL/UI/GridLines.ts b/quadratic-client/src/app/gridGL/UI/GridLines.ts index b42ceccabe..e9df9c8c6f 100644 --- a/quadratic-client/src/app/gridGL/UI/GridLines.ts +++ b/quadratic-client/src/app/gridGL/UI/GridLines.ts @@ -1,6 +1,7 @@ //! Draws grid lines on the canvas. The grid lines fade as the user zooms out, //! and disappears at higher zoom levels. We remove lines between cells that -//! overflow (and in the future, merged cells). +//! overflow (and in the future, merged cells). Grid lines also respect the +//! sheet.clamp value. import { Graphics, ILineStyleOptions, Rectangle } from 'pixi.js'; import { sheets } from '../../grid/controller/Sheets'; @@ -68,12 +69,21 @@ export class GridLines extends Graphics { } private drawVerticalLines(bounds: Rectangle, range: [number, number]) { - const offsets = sheets.sheet.offsets; + const sheet = sheets.sheet; + const offsets = sheet.offsets; const columnPlacement = offsets.getXPlacement(bounds.left); const index = columnPlacement.index; const position = columnPlacement.position; const gridOverflowLines = sheets.sheet.gridOverflowLines; + const top = bounds.top <= sheet.clamp.top ? sheet.clamp.top : bounds.top; + + // draw 0-line if it's visible (since it's not part of the sheet anymore) + if (bounds.left <= 0) { + this.moveTo(0, top); + this.lineTo(0, bounds.bottom); + } + let column = index; const offset = bounds.left - position; let size = 0; @@ -89,10 +99,10 @@ export class GridLines extends Graphics { this.lineTo(x - offset, end); } } else { - this.moveTo(x - offset, bounds.top); + this.moveTo(x - offset, top); this.lineTo(x - offset, bounds.bottom); } - this.gridLinesX.push({ column, x: x - offset, y: bounds.top, w: 1, h: bounds.bottom - bounds.top }); + this.gridLinesX.push({ column, x: x - offset, y: top, w: 1, h: bounds.bottom - top }); } size = sheets.sheet.offsets.getColumnWidth(column); column++; @@ -100,19 +110,34 @@ export class GridLines extends Graphics { } // @returns the vertical range of [rowStart, rowEnd] +<<<<<<< HEAD private drawHorizontalLines(bounds: Rectangle, columns: [number, number]): [number, number] { const offsets = sheets.sheet.offsets; +======= + private drawHorizontalLines(bounds: Rectangle): [number, number] { + const sheet = sheets.sheet; + const offsets = sheet.offsets; +>>>>>>> origin/qa const rowPlacement = offsets.getYPlacement(bounds.top); const index = rowPlacement.index; const position = rowPlacement.position; const gridOverflowLines = sheets.sheet.gridOverflowLines; + const left = bounds.left <= sheet.clamp.left ? sheet.clamp.left : bounds.left; + + // draw 0-line if it's visible (since it's not part of the sheet anymore) + if (bounds.top <= sheet.clamp.top) { + this.moveTo(left, 0); + this.lineTo(bounds.right, 0); + } + let row = index; const offset = bounds.top - position; let size = 0; for (let y = bounds.top; y <= bounds.bottom + size - 1; y += size) { // don't draw grid lines when hidden if (size !== 0) { +<<<<<<< HEAD const lines = gridOverflowLines.getRowHorizontalRange(row, columns); if (lines) { for (const [x0, x1] of lines) { @@ -126,6 +151,11 @@ export class GridLines extends Graphics { this.lineTo(bounds.right, y - offset); } this.gridLinesY.push({ row, x: bounds.left, y: y - offset, w: bounds.right - bounds.left, h: 1 }); +======= + this.moveTo(left, y - offset); + this.lineTo(bounds.right, y - offset); + this.gridLinesY.push({ row, x: bounds.left, y: y - offset, w: bounds.right - left, h: 1 }); +>>>>>>> origin/qa } size = offsets.getRowHeight(row); row++; diff --git a/quadratic-client/src/app/gridGL/UI/UICellImages.ts b/quadratic-client/src/app/gridGL/UI/UICellImages.ts index af9086d12d..6ecff1f29e 100644 --- a/quadratic-client/src/app/gridGL/UI/UICellImages.ts +++ b/quadratic-client/src/app/gridGL/UI/UICellImages.ts @@ -1,7 +1,12 @@ import { events } from '@/app/events/events'; +import { CellsImage } from '@/app/gridGL/cells/cellsImages/CellsImage'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { convertColorStringToTint } from '@/app/helpers/convertColor'; import { Container, Graphics } from 'pixi.js'; +<<<<<<< HEAD import { CellsImage } from '../cells/cellsImages/CellsImage'; +======= +>>>>>>> origin/qa // These should be consistent with ResizeControl.tsx export const IMAGE_BORDER_WIDTH = 5; @@ -28,6 +33,11 @@ export class UICellImages extends Container { events.on('changeSheet', this.changeSheet); } + destroy() { + events.off('changeSheet', this.changeSheet); + super.destroy(); + } + private changeSheet = () => { this.active = undefined; // this.dirtyBorders = true; diff --git a/quadratic-client/src/app/gridGL/UI/UIMultiplayerCursor.ts b/quadratic-client/src/app/gridGL/UI/UIMultiplayerCursor.ts index 60e7c29048..6a9c8447c2 100644 --- a/quadratic-client/src/app/gridGL/UI/UIMultiplayerCursor.ts +++ b/quadratic-client/src/app/gridGL/UI/UIMultiplayerCursor.ts @@ -1,12 +1,12 @@ +import { sheets } from '@/app/grid/controller/Sheets'; +import { drawFiniteSelection, drawInfiniteSelection } from '@/app/gridGL/UI/drawCursor'; +import { CellRefRange, JsCoordinate } from '@/app/quadratic-core-types'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { Graphics } from 'pixi.js'; -import { sheets } from '../../grid/controller/Sheets'; -import { Coordinate } from '../types/size'; -import { drawColumnRowCursor, drawMultiCursor } from './drawCursor'; export const CURSOR_THICKNESS = 1; const ALPHA = 0.5; -const FILL_ALPHA = 0.01 / ALPHA; +const FILL_ALPHA = 0.05; // outside border when editing the cell const CURSOR_INPUT_ALPHA = 0.333 / ALPHA; @@ -28,7 +28,7 @@ export class UIMultiPlayerCursor extends Graphics { code, }: { color: number; - cursor: Coordinate; + cursor: JsCoordinate; editing: boolean; sessionId: string; code: boolean; @@ -68,7 +68,7 @@ export class UIMultiPlayerCursor extends Graphics { // we need to update the multiplayer cursor if a player has selected a row, // column, or the sheet, and the viewport has changed const dirtySheet = viewportDirty - ? [...multiplayer.users].some(([_, player]) => player.parsedSelection?.columnRow) + ? [...multiplayer.users].some(([_, player]) => player.parsedSelection?.isColumnRow()) : false; if (dirtySheet || this.dirty) { @@ -80,24 +80,26 @@ export class UIMultiPlayerCursor extends Graphics { if (player.parsedSelection && player.sheet_id === sheetId) { this.drawCursor({ color, - cursor: player.parsedSelection.cursorPosition, editing: player.cell_edit.active, sessionId: player.session_id, code: player.cell_edit.code_editor, + cursor: player.parsedSelection?.getCursor(), }); - const columnRow = player.parsedSelection.columnRow; - if (columnRow) { - drawColumnRowCursor({ - g: this, - color, - alpha: FILL_ALPHA, - cursorPosition: player.parsedSelection.cursorPosition, - columnRow, - }); - } else if (player.parsedSelection.multiCursor) { - drawMultiCursor(this, color, FILL_ALPHA, player.parsedSelection.multiCursor); + const rangesStringified = player.parsedSelection.getRanges(); + let ranges: CellRefRange[] = []; + try { + ranges = JSON.parse(rangesStringified); + } catch (e) { + console.error(e); } + drawFiniteSelection(this, color, FILL_ALPHA, ranges); + drawInfiniteSelection({ + g: this, + color, + alpha: FILL_ALPHA, + ranges, + }); } }); } diff --git a/quadratic-client/src/app/gridGL/UI/UIValidations.ts b/quadratic-client/src/app/gridGL/UI/UIValidations.ts index e3df153ea1..e491291e5c 100644 --- a/quadratic-client/src/app/gridGL/UI/UIValidations.ts +++ b/quadratic-client/src/app/gridGL/UI/UIValidations.ts @@ -3,16 +3,19 @@ //! CellsTextHashSpecial. Since there are "infinite", we only apply them to the //! visible cells and redraw them whenever the viewport moves. +import { hasPermissionToEditFile } from '@/app/actions'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; -import { Container, Point, Rectangle } from 'pixi.js'; -import { pixiApp } from '../pixiApp/PixiApp'; +import { drawCheckbox, drawDropdown, SpecialSprite } from '@/app/gridGL/cells/cellsLabel/drawSpecial'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { getRangeRectangleFromCellRefRange } from '@/app/gridGL/helpers/selection'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; +import { CellRefRange } from '@/app/quadratic-core-types'; +import { A1SelectionValueToSelection } from '@/app/quadratic-rust-client/quadratic_rust_client'; import { ValidationUIType, validationUIType } from '@/app/ui/menus/Validations/Validation/validationType'; -import { drawCheckbox, drawDropdown, SpecialSprite } from '../cells/cellsLabel/drawSpecial'; -import { pixiAppSettings } from '../pixiApp/PixiAppSettings'; -import { hasPermissionToEditFile } from '@/app/actions'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -import { intersects } from '../helpers/intersects'; +import { Container, Point } from 'pixi.js'; const MINIMUM_SCALE_TO_SHOW_VALIDATIONS = 0.25; const FADE_SCALE = 0.1; @@ -25,31 +28,23 @@ export class UIValidations extends Container { constructor() { super(); this.occupied = new Set(); - events.on('sheetValidations', (sheetId: string) => { - if (sheetId === sheets.sheet.id) { - this.dirty = true; - } - }); - events.on('renderValidationWarnings', (sheetId: string) => { - if (sheetId === sheets.sheet.id) { - this.dirty = true; - } - }); + events.on('sheetValidations', this.setDirty); + events.on('renderValidationWarnings', this.setDirty); } - // Returns the visible range of cells within the viewport. - getVisibleRange(): Rectangle { - const offsets = sheets.sheet.offsets; - const bounds = pixiApp.viewport.getVisibleBounds(); - const xStart = offsets.getXPlacement(bounds.left).index; - const xEnd = offsets.getXPlacement(bounds.right).index; - const yStart = offsets.getYPlacement(bounds.top).index; - const yEnd = offsets.getYPlacement(bounds.bottom).index; - - return new Rectangle(xStart, yStart, xEnd - xStart + 1, yEnd - yStart + 1); + destroy() { + events.off('sheetValidations', this.setDirty); + events.off('renderValidationWarnings', this.setDirty); + super.destroy(); } - private drawValidations(range: Rectangle) { + setDirty = (sheetId: string) => { + if (sheetId === sheets.sheet.id) { + this.dirty = true; + } + }; + + private drawValidations() { // we need to take the validations in reverse order const validations = sheets.sheet.validations; for (let i = validations.length - 1; i >= 0; i--) { @@ -57,88 +52,29 @@ export class UIValidations extends Container { const type = validationUIType(v); if (v.selection.sheet_id.id !== sheets.sheet.id || !type) continue; - if (v.selection.all) { - this.drawAll(range, type); - } - if (v.selection.rows?.length) { - const rows = v.selection.rows.filter((r) => r >= range.y && r <= range.y + range.height); - rows.forEach((row) => this.drawRow(Number(row), range, type)); - } - if (v.selection.columns?.length) { - const columns = v.selection.columns.filter((c) => c >= range.x && c <= range.x + range.width); - columns.forEach((column) => this.drawColumn(Number(column), range, type)); - } + const jsSelection = A1SelectionValueToSelection(v.selection); + const infiniteRangesStringified = jsSelection.getInfiniteRanges(); + const infiniteRanges: CellRefRange[] = JSON.parse(infiniteRangesStringified); + infiniteRanges.forEach((range) => this.drawInfiniteRange(range, type)); } } - private drawColumn(column: number, range: Rectangle, type: ValidationUIType) { - const offsets = sheets.sheet.offsets; - const xPlacement = offsets.getColumnPlacement(column); - const x = xPlacement.position; - let yPlacement = offsets.getRowPlacement(range.y); - let y = yPlacement.position; - const cellsLabels = pixiApp.cellsSheets.current?.cellsLabels; - for (let row = range.y; row < range.y + range.height; row++) { - const key = `${column},${row}`; - // Check if UIValidation has added content to this cell or if - // CellsTextHash has rendered content in this cell. - if (!this.occupied.has(key) && !cellsLabels?.hasCell(column, row)) { - if (type === 'checkbox') { - this.addChild( - drawCheckbox({ x: x + xPlacement.size / 2, y: y + yPlacement.size / 2, column, row, value: false }) - ); - } else if (type === 'dropdown') { - this.addChild(drawDropdown({ x: x + xPlacement.size, y: y, column, row })); - } - this.occupied.add(key); - } - - y += yPlacement.size; - if (row !== range.y + range.height - 1) { - yPlacement = offsets.getRowPlacement(row + 1); - } - } - } - - private drawRow(row: number, range: Rectangle, type: ValidationUIType) { - const offsets = sheets.sheet.offsets; - const yPlacement = offsets.getRowPlacement(row); - const y = yPlacement.position; - let xPlacement = offsets.getColumnPlacement(range.x); - let x = xPlacement.position; - const cellsLabels = pixiApp.cellsSheets.current?.cellsLabels; - for (let column = range.x; column < range.x + range.width; column++) { - const key = `${column},${row}`; - // Check if UIValidation has added content to this cell or if - // CellsTextHash has rendered content in this cell. - if (!this.occupied.has(key) && !cellsLabels?.hasCell(column, row)) { - if (type === 'checkbox') { - this.addChild( - drawCheckbox({ x: x + xPlacement.size / 2, y: y + yPlacement.size / 2, column, row, value: false }) - ); - } else if (type === 'dropdown') { - this.addChild(drawDropdown({ x: x + xPlacement.size, y: y, column, row })); - } - this.occupied.add(key); - } - - x += xPlacement.size; - if (column !== range.x + range.width - 1) { - xPlacement = offsets.getColumnPlacement(column + 1); - } + private drawInfiniteRange(range: CellRefRange, type: ValidationUIType) { + const screenRangeRectangle = getRangeRectangleFromCellRefRange(range); + const visibleRectangle = sheets.getVisibleRectangle(); + const intersection = intersects.rectangleClip(screenRangeRectangle, visibleRectangle); + if (!intersection) { + return; } - } - private drawAll(range: Rectangle, type: ValidationUIType) { const offsets = sheets.sheet.offsets; - let xPlacement = offsets.getColumnPlacement(range.x); - let x = xPlacement.position; - const xStart = x; - let yPlacement = offsets.getRowPlacement(range.y); - let y = yPlacement.position; const cellsLabels = pixiApp.cellsSheets.current?.cellsLabels; - for (let row = range.y; row < range.y + range.height; row++) { - for (let column = range.x; column < range.x + range.width; column++) { + for (let row = intersection.top; row < intersection.bottom; row++) { + const yPlacement = offsets.getRowPlacement(row); + const y = yPlacement.position; + for (let column = intersection.left; column < intersection.right; column++) { + let xPlacement = offsets.getColumnPlacement(column); + let x = xPlacement.position; const key = `${column},${row}`; // Check if UIValidation has added content to this cell or if // CellsTextHash has rendered content in this cell. @@ -152,16 +88,6 @@ export class UIValidations extends Container { } this.occupied.add(key); } - - x += xPlacement.size; - if (column !== range.x + range.width - 1) { - xPlacement = offsets.getColumnPlacement(column + 1); - } - } - x = xStart; - y += yPlacement.size; - if (row !== range.y + range.height - 1) { - yPlacement = offsets.getRowPlacement(row + 1); } } } @@ -185,8 +111,7 @@ export class UIValidations extends Container { // Shortcut if there are no validations in this sheet. if (sheets.sheet.validations.length === 0) return; - const range = this.getVisibleRange(); - this.drawValidations(range); + this.drawValidations(); } // handle clicking on UI elements diff --git a/quadratic-client/src/app/gridGL/UI/boxCells.ts b/quadratic-client/src/app/gridGL/UI/boxCells.ts index 919a9ab31a..68c72932f9 100644 --- a/quadratic-client/src/app/gridGL/UI/boxCells.ts +++ b/quadratic-client/src/app/gridGL/UI/boxCells.ts @@ -42,12 +42,7 @@ export class BoxCells extends Graphics { private drawRectangle(): void { if (!this.gridRectangle) return; - const screenRectangle = sheets.sheet.getScreenRectangle( - this.gridRectangle.x, - this.gridRectangle.y, - this.gridRectangle.width, - this.gridRectangle.height - ); + const screenRectangle = sheets.sheet.getScreenRectangleFromRect(this.gridRectangle); this.dirty = false; this.clear(); this.lineStyle({ @@ -74,12 +69,7 @@ export class BoxCells extends Graphics { this.lineStyle(0); this.deleteRectangles?.forEach((rectangle) => { this.beginFill(colors.boxCellsDeleteColor, colors.boxCellsAlpha); - const screenRectangle = sheets.sheet.getScreenRectangle( - rectangle.x, - rectangle.y, - rectangle.width, - rectangle.height - ); + const screenRectangle = sheets.sheet.getScreenRectangleFromRect(rectangle); screenRectangle.height++; this.drawShape(screenRectangle); this.endFill(); diff --git a/quadratic-client/src/app/gridGL/UI/cellHighlights/CellHighlights.ts b/quadratic-client/src/app/gridGL/UI/cellHighlights/CellHighlights.ts index f319522d1e..e74a3afda4 100644 --- a/quadratic-client/src/app/gridGL/UI/cellHighlights/CellHighlights.ts +++ b/quadratic-client/src/app/gridGL/UI/cellHighlights/CellHighlights.ts @@ -1,40 +1,20 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +import { DASHED } from '@/app/gridGL/generateTextures'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; -import { Coordinate } from '@/app/gridGL/types/size'; +import { drawDashedRectangle, drawDashedRectangleMarching } from '@/app/gridGL/UI/cellHighlights/cellHighlightsDraw'; +import { convertColorStringToTint } from '@/app/helpers/convertColor'; +import type { JsCellsAccessed } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; import { Container, Graphics } from 'pixi.js'; -import { convertColorStringToTint } from '../../../helpers/convertColor'; -import { CellPosition, ParseFormulaReturnType, Span } from '../../../helpers/formulaNotation'; -import { DASHED } from '../../generateTextures'; -import { drawDashedRectangle, drawDashedRectangleMarching } from './cellHighlightsDraw'; - -// TODO: these files need to be cleaned up and properly typed. Lots of untyped -// data passed around within the data. - -export interface HighlightedCellRange { - column: number; - row: number; - width: number; - height: number; - span: Span; - sheet: string; - index: number; -} - -export interface HighlightedCell { - column: number; - row: number; - sheet: string; -} const NUM_OF_CELL_REF_COLORS = colors.cellHighlightColor.length; const MARCH_ANIMATE_TIME_MS = 80; export class CellHighlights extends Container { - private highlightedCells: HighlightedCellRange[] = []; - highlightedCellIndex: number | undefined; + private cellsAccessed: JsCellsAccessed[] = []; + private selectedCellIndex: number | undefined; private highlights: Graphics; private marchingHighlight: Graphics; @@ -47,12 +27,21 @@ export class CellHighlights extends Container { super(); this.highlights = this.addChild(new Graphics()); this.marchingHighlight = this.addChild(new Graphics()); - events.on('changeSheet', () => (this.dirty = true)); + events.on('changeSheet', this.setDirty); + } + + destroy() { + events.off('changeSheet', this.setDirty); + super.destroy(); } + setDirty = () => { + this.dirty = true; + }; + clear() { - this.highlightedCells = []; - this.highlightedCellIndex = undefined; + this.cellsAccessed = []; + this.selectedCellIndex = undefined; this.highlights.clear(); this.marchingHighlight.clear(); pixiApp.setViewportDirty(); @@ -61,40 +50,38 @@ export class CellHighlights extends Container { private draw() { this.highlights.clear(); - const highlightedCells = [...this.highlightedCells]; - const highlightedCellIndex = this.highlightedCellIndex; - if (!highlightedCells.length) return; - highlightedCells.forEach((cell, index) => { - if (cell.sheet !== sheets.sheet.id) return; - - const colorNumber = convertColorStringToTint(colors.cellHighlightColor[cell.index % NUM_OF_CELL_REF_COLORS]); - const cursorCell = sheets.sheet.getScreenRectangle(cell.column, cell.row, cell.width, cell.height); - - // We do not draw the dashed rectangle if the inline Formula editor's cell - // cursor is moving (it's handled by updateMarchingHighlights instead). - if (highlightedCellIndex === undefined || highlightedCellIndex !== index || !inlineEditorHandler.cursorIsMoving) { - drawDashedRectangle({ - g: this.highlights, - color: colorNumber, - isSelected: highlightedCellIndex === index, - startCell: cursorCell, - }); - } - }); - if (highlightedCells.length) { - pixiApp.setViewportDirty(); - } + + if (!this.cellsAccessed.length) return; + + const selectedCellIndex = this.selectedCellIndex; + + const cellsAccessed = [...this.cellsAccessed]; + cellsAccessed + .filter(({ sheetId }) => sheetId === sheets.current) + .flatMap(({ ranges }) => ranges) + .forEach((range, index) => { + if (selectedCellIndex === undefined || selectedCellIndex !== index || !inlineEditorHandler.cursorIsMoving) { + drawDashedRectangle({ + g: this.highlights, + color: convertColorStringToTint(colors.cellHighlightColor[index % NUM_OF_CELL_REF_COLORS]), + isSelected: selectedCellIndex === index, + range, + }); + } + }); + + pixiApp.setViewportDirty(); } // Draws the marching highlights by using an offset dashed line to create the // marching effect. private updateMarchingHighlight() { if (!inlineEditorHandler.cursorIsMoving) { - this.highlightedCellIndex = undefined; + this.selectedCellIndex = undefined; return; } // Index may not have been set yet. - if (this.highlightedCellIndex === undefined) return; + if (this.selectedCellIndex === undefined) return; if (this.marchLastTime === 0) { this.marchLastTime = Date.now(); } else if (Date.now() - this.marchLastTime < MARCH_ANIMATE_TIME_MS) { @@ -102,18 +89,16 @@ export class CellHighlights extends Container { } else { this.marchLastTime = Date.now(); } - const highlightedCell = this.highlightedCells[this.highlightedCellIndex]; - if (!highlightedCell) return; - const colorNumber = convertColorStringToTint( - colors.cellHighlightColor[highlightedCell.index % NUM_OF_CELL_REF_COLORS] - ); - const cursorCell = sheets.sheet.getScreenRectangle( - highlightedCell.column, - highlightedCell.row, - highlightedCell.width, - highlightedCell.height - ); - drawDashedRectangleMarching(this.marchingHighlight, colorNumber, cursorCell, this.march); + const selectedCellIndex = this.selectedCellIndex; + const accessedCell = this.cellsAccessed[selectedCellIndex]; + if (!accessedCell) return; + const colorNumber = convertColorStringToTint(colors.cellHighlightColor[selectedCellIndex % NUM_OF_CELL_REF_COLORS]); + drawDashedRectangleMarching({ + g: this.marchingHighlight, + color: colorNumber, + march: this.march, + range: accessedCell.ranges[0], + }); this.march = (this.march + 1) % Math.floor(DASHED); pixiApp.setViewportDirty(); } @@ -136,16 +121,6 @@ export class CellHighlights extends Container { return this.dirty || inlineEditorHandler.cursorIsMoving; } - private getSheet(cellSheet: string | undefined, sheetId: string): string { - if (!cellSheet) return sheetId; - - // It may come in as either a sheet id or a sheet name. - if (sheets.getById(cellSheet)) { - return cellSheet; - } - return sheets.getSheetByName(cellSheet)?.id ?? sheetId; - } - evalCoord(cell: { type: 'Relative' | 'Absolute'; coord: number }, origin: number) { const isRelative = cell.type === 'Relative'; const getOrigin = isRelative ? origin : 0; @@ -153,70 +128,17 @@ export class CellHighlights extends Container { return getOrigin + cell.coord; } - private fromCellRange( - cellRange: { type: 'CellRange'; start: CellPosition; end: CellPosition; sheet?: string }, - origin: Coordinate, - sheet: string, - span: Span, - index: number - ) { - const startX = this.evalCoord(cellRange.start.x, origin.x); - const startY = this.evalCoord(cellRange.start.y, origin.y); - const endX = this.evalCoord(cellRange.end.x, origin.x); - const endY = this.evalCoord(cellRange.end.y, origin.y); - this.highlightedCells.push({ - column: startX, - row: startY, - width: endX - startX, - height: endY - startY, - sheet: this.getSheet(cellRange.sheet ?? cellRange.start.sheet, sheet), - span, - index, - }); - } - - private fromCell(cell: CellPosition, origin: Coordinate, sheet: string, span: Span, index: number) { - this.highlightedCells.push({ - column: this.evalCoord(cell.x, origin.x), - row: this.evalCoord(cell.y, origin.y), - width: 0, - height: 0, - sheet: this.getSheet(cell.sheet, sheet), - span, - index, - }); - } - - fromFormula(formula: ParseFormulaReturnType, cell: Coordinate, sheet: string) { - this.highlightedCells = []; - - formula.cell_refs.forEach((cellRef, index) => { - switch (cellRef.cell_ref.type) { - case 'CellRange': - this.fromCellRange(cellRef.cell_ref, cell, sheet, cellRef.span, index); - break; - - case 'Cell': - this.fromCell(cellRef.cell_ref.pos, cell, sheet, cellRef.span, index); - break; - - default: - throw new Error('Unsupported cell-ref in fromFormula'); - } - }); + fromCellsAccessed(cellsAccessed: JsCellsAccessed[] | null) { + this.cellsAccessed = cellsAccessed ?? []; pixiApp.cellHighlights.dirty = true; } - setHighlightedCell(index: number) { - this.highlightedCellIndex = this.highlightedCells.findIndex((cell) => cell.index === index); - } - - getHighlightedCells() { - return this.highlightedCells; + setSelectedCell(index: number) { + this.selectedCellIndex = index; } - clearHighlightedCell() { - this.highlightedCellIndex = undefined; + clearSelectedCell() { + this.selectedCellIndex = undefined; this.marchingHighlight.clear(); } } diff --git a/quadratic-client/src/app/gridGL/UI/cellHighlights/cellHighlightsDraw.ts b/quadratic-client/src/app/gridGL/UI/cellHighlights/cellHighlightsDraw.ts index 87fcd9af80..6d8a665f59 100644 --- a/quadratic-client/src/app/gridGL/UI/cellHighlights/cellHighlightsDraw.ts +++ b/quadratic-client/src/app/gridGL/UI/cellHighlights/cellHighlightsDraw.ts @@ -1,56 +1,80 @@ import { DASHED, DASHED_THICKNESS, generatedTextures } from '@/app/gridGL/generateTextures'; -import { CURSOR_THICKNESS, CursorCell, FILL_ALPHA } from '@/app/gridGL/UI/Cursor'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { getRangeScreenRectangleFromCellRefRange } from '@/app/gridGL/helpers/selection'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { CURSOR_THICKNESS, FILL_ALPHA } from '@/app/gridGL/UI/Cursor'; +import { CellRefRange } from '@/app/quadratic-core-types'; import { Graphics } from 'pixi.js'; -export function drawDashedRectangle(options: { - g: Graphics; - color: number; - isSelected: boolean; - startCell: CursorCell; - endCell?: CursorCell; -}) { - const { g, color, isSelected, startCell, endCell } = options; - const minX = Math.min(startCell.x, endCell?.x ?? Infinity); - const minY = Math.min(startCell.y, endCell?.y ?? Infinity); - const maxX = Math.max(startCell.width + startCell.x, endCell ? endCell.x + endCell.width : -Infinity); - const maxY = Math.max(startCell.y + startCell.height, endCell ? endCell.y + endCell.height : -Infinity); - - const path = [ - [maxX, minY], - [maxX, maxY], - [minX, maxY], - [minX, minY], - ]; - - // have to fill a rect because setting multiple line styles makes it unable to be filled +export function drawDashedRectangle(options: { g: Graphics; color: number; isSelected: boolean; range: CellRefRange }) { + const { g, color, isSelected, range } = options; + + const selectionRect = getRangeScreenRectangleFromCellRefRange(range); + const bounds = pixiApp.viewport.getVisibleBounds(); + if (!intersects.rectangleRectangle(selectionRect, bounds)) { + return; + } + + g.lineStyle({ + width: CURSOR_THICKNESS, + color, + alignment: 0.5, + texture: generatedTextures.dashedHorizontal, + }); + g.moveTo(selectionRect.left, selectionRect.top); + g.lineTo(Math.min(selectionRect.right, bounds.right), selectionRect.top); + if (selectionRect.bottom <= bounds.bottom) { + g.moveTo(Math.min(selectionRect.right, bounds.right), selectionRect.bottom); + g.lineTo(selectionRect.left, selectionRect.bottom); + } + + g.lineStyle({ + width: CURSOR_THICKNESS, + color, + alignment: 0.5, + texture: generatedTextures.dashedVertical, + }); + g.moveTo(selectionRect.left, Math.min(selectionRect.bottom, bounds.bottom)); + g.lineTo(selectionRect.left, selectionRect.top); + if (selectionRect.right <= bounds.right) { + g.moveTo(selectionRect.right, Math.min(selectionRect.bottom, bounds.bottom)); + g.lineTo(selectionRect.right, selectionRect.top); + } + if (isSelected) { g.lineStyle({ alignment: 0, }); - g.moveTo(minX, minY); + g.moveTo(selectionRect.left, selectionRect.top); g.beginFill(color, FILL_ALPHA); - g.drawRect(minX, minY, maxX - minX, maxY - minY); + g.drawRect( + selectionRect.left, + selectionRect.top, + Math.min(selectionRect.right, bounds.right) - selectionRect.left, + Math.min(selectionRect.bottom, bounds.bottom) - selectionRect.top + ); g.endFill(); } +} - g.moveTo(minX, minY); - for (let i = 0; i < path.length; i++) { - const texture = i % 2 === 0 ? generatedTextures.dashedHorizontal : generatedTextures.dashedVertical; - g.lineStyle({ - width: CURSOR_THICKNESS, - color, - alignment: 0, - texture, - }); - g.lineTo(path[i][0], path[i][1]); +export function drawDashedRectangleMarching(options: { + g: Graphics; + color: number; + march: number; + range: CellRefRange; +}) { + const { g, color, march, range } = options; + + const selectionRect = getRangeScreenRectangleFromCellRefRange(range); + const bounds = pixiApp.viewport.getVisibleBounds(); + if (!intersects.rectangleRectangle(selectionRect, bounds)) { + return; } -} -export function drawDashedRectangleMarching(g: Graphics, color: number, startCell: CursorCell, march: number) { - const minX = startCell.x; - const minY = startCell.y; - const maxX = startCell.width + startCell.x; - const maxY = startCell.y + startCell.height; + const minX = selectionRect.left; + const minY = selectionRect.top; + const maxX = selectionRect.right; + const maxY = selectionRect.bottom; g.clear(); diff --git a/quadratic-client/src/app/gridGL/UI/drawCursor.ts b/quadratic-client/src/app/gridGL/UI/drawCursor.ts index 56f6ec9c44..66e166e347 100644 --- a/quadratic-client/src/app/gridGL/UI/drawCursor.ts +++ b/quadratic-client/src/app/gridGL/UI/drawCursor.ts @@ -1,85 +1,212 @@ +//! Generic draw cursor functions that is used by both Cursor.ts and +//! UIMultiplayerCursor.ts. + import { sheets } from '@/app/grid/controller/Sheets'; -import { ColumnRowCursor, RectangleLike } from '@/app/grid/sheet/SheetCursor'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { CURSOR_THICKNESS } from '@/app/gridGL/UI/Cursor'; +import { CellRefRange, JsCoordinate } from '@/app/quadratic-core-types'; import { Graphics } from 'pixi.js'; -import { pixiApp } from '../pixiApp/PixiApp'; -import { Coordinate } from '../types/size'; -const drawCursorOutline = (g: Graphics, color: number) => { - const sheet = sheets.sheet; - const cursor = sheet.cursor.getCursor(); - const outline = sheet.getCellOffsets(cursor.x, cursor.y); +const SECTION_OUTLINE_WIDTH = 1; +const SECTION_OUTLINE_NATIVE = true; + +export const isStart = (coord: bigint): boolean => { + // eslint-disable-next-line eqeqeq + return coord == 1n; +}; + +export const isUnbounded = (coord: bigint): boolean => { + // eslint-disable-next-line eqeqeq + return coord == -1n; +}; + +export const drawCursorOutline = (g: Graphics, color: number, cursor: JsCoordinate) => { + const outline = sheets.sheet.getCellOffsets(cursor.x, cursor.y); g.lineStyle({ width: CURSOR_THICKNESS, color, alignment: 0 }); g.drawRect(outline.x, outline.y, outline.width, outline.height); }; -// this is generic so it can be used by UIMultiplayerCursor -export const drawColumnRowCursor = (options: { +// Draws a cursor with a finite number of cells (this is drawn once for each +// selection setting). +export const drawFiniteSelection = (g: Graphics, color: number, alpha: number, ranges: CellRefRange[]) => { + if (ranges.length === 0) return; + + g.lineStyle({ width: SECTION_OUTLINE_WIDTH, color, alignment: 0, native: SECTION_OUTLINE_NATIVE }); + g.beginFill(color, alpha); + + const sheet = sheets.sheet; + ranges.forEach(({ range }) => { + const start = range.start; + const end = range.end; + + // we have all four points, just draw a rectangle + if (!isUnbounded(end.col.coord) && !isUnbounded(end.row.coord)) { + const startX = Math.min(Number(start.col.coord), Number(end.col.coord)); + const startY = Math.min(Number(start.row.coord), Number(end.row.coord)); + const width = Math.abs(Number(end.col.coord) - Number(start.col.coord)) + 1; + const height = Math.abs(Number(end.row.coord) - Number(start.row.coord)) + 1; + if (width > 1 || height > 1) { + const rect = sheet.getScreenRectangle(startX, startY, width, height); + g.drawShape(rect); + } + } + }); + g.endFill(); +}; + +// Draws a cursor with an infinite number of cells (this is drawn on each +// viewport update). +export const drawInfiniteSelection = (options: { g: Graphics; - cursorPosition: Coordinate; - columnRow: ColumnRowCursor; color: number; alpha: number; + ranges: CellRefRange[]; }) => { - const { g, cursorPosition, columnRow, color, alpha } = options; + const { g, color, alpha, ranges } = options; + if (ranges.length === 0) return; + const sheet = sheets.sheet; - g.lineStyle(); - g.beginFill(color, alpha); + // we use headingSize to avoid getting column/row 0 from the viewport in + // getScreenRectangle + const headingSize = pixiApp.headings.headingSize; + const bounds = pixiApp.viewport.getVisibleBounds(); - if (columnRow.all) { - g.drawRect(bounds.x, bounds.y, bounds.width, bounds.height); - } else { - if (columnRow.columns) { - let minX = Infinity, - maxX = -Infinity; - columnRow.columns.forEach((column) => { - const { x, width } = sheet.getCellOffsets(column, 0); - minX = Math.min(minX, x); - maxX = Math.max(maxX, x + width); - g.drawRect(x, bounds.y, width, bounds.height); - if (column === cursorPosition.x) { - } - }); - - // draw outline - g.lineStyle(1, color, 1, 0, true); - g.moveTo(minX, bounds.top); - g.lineTo(minX, bounds.bottom); - g.moveTo(maxX, bounds.top); - g.lineTo(maxX, bounds.bottom); + bounds.x = Math.max(bounds.x, 0); + bounds.y = Math.max(bounds.y, 0); + + ranges.forEach(({ range }) => { + const start = range.start; + const end = range.end; + + g.lineStyle(); + g.beginFill(color, alpha); + // the entire sheet is selected + if ( + isStart(start.col.coord) && + isStart(start.row.coord) && + isUnbounded(end.col.coord) && + isUnbounded(end.row.coord) + ) { + g.drawRect(bounds.x, bounds.y, bounds.width, bounds.height); + g.endFill(); + g.lineStyle({ width: SECTION_OUTLINE_WIDTH, color, alignment: 1, native: SECTION_OUTLINE_NATIVE }); + g.moveTo(0, 0); + g.lineTo(0, bounds.height); + g.moveTo(0, 0); + g.lineTo(bounds.width, 0); } - if (columnRow.rows) { - let minY = Infinity, - maxY = -Infinity; - columnRow.rows.forEach((row) => { - const { y, height } = sheet.getCellOffsets(0, row); - minY = Math.min(minY, y); - maxY = Math.max(maxY, y + height); - g.drawRect(bounds.x, y, bounds.width, height); - if (row === cursorPosition.y) { - } - }); - - // draw outline - g.lineStyle(1, color, 1, 0, true); - g.moveTo(bounds.left, minY); - g.lineTo(bounds.right, minY); - g.moveTo(bounds.left, maxY); - g.lineTo(bounds.right, maxY); + + // the entire sheet is selected starting from the start location + else if (isUnbounded(end.col.coord) && isUnbounded(end.row.coord)) { + const rect = sheet.getCellOffsets(start.col.coord, start.row.coord); + rect.x = Math.max(rect.x, bounds.x); + rect.y = Math.max(rect.y, bounds.y); + rect.width = bounds.right - rect.x; + rect.height = bounds.bottom - rect.y; + if (intersects.rectangleRectangle(rect, bounds)) { + g.drawShape(rect); + g.endFill(); + g.lineStyle({ width: SECTION_OUTLINE_WIDTH, color, alignment: 1, native: SECTION_OUTLINE_NATIVE }); + g.moveTo(rect.right, rect.top); + g.lineTo(rect.left, rect.top); + g.lineTo(rect.left, rect.bottom); + } } - } - g.endFill(); - drawCursorOutline(g, color); -}; -export const drawMultiCursor = (g: Graphics, color: number, alpha: number, rectangles: RectangleLike[]) => { - const sheet = sheets.sheet; - g.lineStyle(1, color, 1, 0, true); - g.beginFill(color, alpha); - rectangles.forEach((rectangle) => { - const rect = sheet.getScreenRectangle(rectangle.x, rectangle.y, rectangle.width - 1, rectangle.height - 1); - g.drawShape(rect); + // column(s) selected + else if (isStart(start.row.coord) && isUnbounded(end.row.coord)) { + const startX = Math.min(Number(start.col.coord), Number(end.col.coord)); + const width = Math.abs(Number(end.col.coord) - Number(start.col.coord)) + 1; + const rect = sheet.getScreenRectangle(startX, headingSize.height, width, 0); + rect.y = Math.max(0, bounds.y); + rect.height = bounds.height; + if (intersects.rectangleRectangle(rect, bounds)) { + g.drawShape(rect); + g.endFill(); + g.lineStyle({ width: SECTION_OUTLINE_WIDTH, color, alignment: 1, native: SECTION_OUTLINE_NATIVE }); + g.moveTo(rect.left, 0); + g.lineTo(rect.right, 0); + g.moveTo(rect.left, Math.max(0, bounds.top)); + g.lineTo(rect.left, bounds.bottom); + g.moveTo(rect.right, Math.max(0, bounds.top)); + g.lineTo(rect.right, bounds.bottom); + } + } + + // multiple columns are selected starting on a row + else if (!isUnbounded(end.col.coord) && isUnbounded(end.row.coord)) { + const startX = Math.min(Number(start.col.coord), Number(end.col.coord)); + const endX = Math.max(Number(start.col.coord), Number(end.col.coord)); + const rect = sheet.getScreenRectangle( + startX, + Number(start.row.coord), + endX - startX + 1, + Number(start.row.coord) + ); + if (rect.y > bounds.bottom) return; + + rect.y = Math.max(rect.top, bounds.top); + rect.height = bounds.bottom - rect.y; + if (intersects.rectangleRectangle(rect, bounds)) { + g.drawShape(rect); + g.endFill(); + g.lineStyle({ width: SECTION_OUTLINE_WIDTH, color, alignment: 1, native: SECTION_OUTLINE_NATIVE }); + g.moveTo(rect.left, rect.top); + g.lineTo(rect.right, rect.top); + g.moveTo(rect.left, rect.top); + g.lineTo(rect.left, bounds.bottom); + g.moveTo(rect.right, rect.top); + g.lineTo(rect.right, bounds.bottom); + } + } + + // row(s) selected + else if (isStart(start.col.coord) && isUnbounded(end.col.coord)) { + const startY = Math.min(Number(start.row.coord), Number(end.row.coord)); + const height = Math.abs(Number(end.row.coord) - Number(start.row.coord)) + 1; + const rect = sheet.getScreenRectangle(headingSize.width, startY, 0, height); + rect.x = Math.max(0, bounds.x); + rect.width = bounds.width; + if (intersects.rectangleRectangle(rect, bounds)) { + g.drawShape(rect); + g.endFill(); + g.lineStyle({ width: SECTION_OUTLINE_WIDTH, color, alignment: 1, native: SECTION_OUTLINE_NATIVE }); + g.moveTo(0, rect.top); + g.lineTo(0, rect.bottom); + g.moveTo(bounds.left, rect.top); + g.lineTo(bounds.right, rect.top); + g.moveTo(bounds.left, rect.bottom); + g.lineTo(bounds.right, rect.bottom); + } + } + + // multiple rows are selected starting on a column + else if (!isUnbounded(end.row.coord) && isUnbounded(end.col.coord)) { + const startY = Math.min(Number(start.row.coord), Number(end.row.coord)); + const endY = Math.max(Number(start.row.coord), Number(end.row.coord)); + const rect = sheet.getScreenRectangle( + Number(start.col.coord), + startY, + Number(start.col.coord), + endY - startY + 1 + ); + if (rect.x > bounds.right) return; + + rect.x = Math.max(rect.left, bounds.x); + rect.width = bounds.right - rect.x; + if (intersects.rectangleRectangle(rect, bounds)) { + g.drawShape(rect); + g.endFill(); + g.lineStyle({ width: SECTION_OUTLINE_WIDTH, color, alignment: 1, native: SECTION_OUTLINE_NATIVE }); + g.moveTo(rect.left, rect.top); + g.lineTo(rect.left, rect.bottom); + g.moveTo(rect.left, rect.top); + g.lineTo(bounds.right, rect.top); + g.moveTo(rect.left, rect.bottom); + g.lineTo(rect.right, rect.bottom); + } + } }); - g.endFill(); }; diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts index 062116045c..3c88c71105 100644 --- a/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/GridHeadings.ts @@ -17,7 +17,7 @@ type Selected = 'all' | number[] | undefined; export type IntersectsHeadings = { column: number | null; row: number | null; corner?: true }; // Constants for headers -export const LABEL_MAXIMUM_WIDTH_PERCENT = 0.7; +export const LABEL_MAXIMUM_WIDTH_PERCENT = 0.9; export const LABEL_MAXIMUM_HEIGHT_PERCENT = 0.5; export const LABEL_PADDING_ROWS = 2; export const GRID_HEADER_FONT_SIZE = 10; @@ -25,7 +25,7 @@ export const ROW_DIGIT_OFFSET = { x: 0, y: -1 }; const GRID_HEADING_RESIZE_TOLERANCE = 3; // this is the number of digits to use when calculating what horizontal headings are hidden -export const LABEL_DIGITS_TO_CALCULATE_SKIP = 4; +export const LABEL_DIGITS_TO_CALCULATE_SKIP = 3; export class GridHeadings extends Container { private characterSize?: Size; @@ -70,7 +70,7 @@ export class GridHeadings extends Container { private findIntervalX(i: number): number { if (i > 100) return 50; - if (i > 20) return 25; + if (i > 20) return 26; if (i > 5) return 10; return 5; } @@ -91,71 +91,36 @@ export class GridHeadings extends Container { const bounds = viewport.getVisibleBounds(); const scale = viewport.scaled; const cellHeight = CELL_HEIGHT / scale; - const offsets = sheets.sheet.offsets; - const cursor = sheets.sheet.cursor; + const sheet = sheets.sheet; + const offsets = sheet.offsets; + const cursor = sheet.cursor; + const clamp = sheet.clamp; this.headingsGraphics.lineStyle(0); this.headingsGraphics.beginFill(colors.headerBackgroundColor); - this.columnRect = new Rectangle(bounds.left, bounds.top, bounds.width, cellHeight); + const left = Math.max(bounds.left, clamp.left); + this.columnRect = new Rectangle(left, bounds.top, bounds.width, cellHeight); this.headingsGraphics.drawShape(this.columnRect); this.headingsGraphics.endFill(); - // fill the entire viewport if all cells are selected - if (cursor.columnRow?.all) { - this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedRowColumnBackgroundColorAlpha); - this.headingsGraphics.drawRect(viewport.left, viewport.top, viewport.screenWidthInWorldPixels, cellHeight); - this.headingsGraphics.endFill(); - return 'all'; - } - - // dark fill headings if there is a columnRow selection - if (cursor.columnRow?.columns) { - this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedRowColumnBackgroundColorAlpha); - cursor.columnRow.columns.forEach((column) => { - const offset = offsets.getColumnPlacement(column); - this.headingsGraphics.drawRect(offset.position, viewport.top, offset.size, cellHeight); - }); - this.headingsGraphics.endFill(); - return cursor.columnRow.columns; - } - - // if we're selecting rows, then show all columns as selected - if (cursor.columnRow?.rows) { - this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedBackgroundColorAlpha); - this.headingsGraphics.drawRect(viewport.left, viewport.top, viewport.screenWidthInWorldPixels, cellHeight); - this.headingsGraphics.endFill(); - return 'all'; - } - - // selected cells based on multiCursor - else if (cursor.multiCursor) { - const selectedColumns = new Set(); - this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedBackgroundColorAlpha); - cursor.multiCursor.forEach((rectangle) => { - const start = offsets.getColumnPlacement(rectangle.left); - const end = offsets.getColumnPlacement(rectangle.right - 1); - this.headingsGraphics.drawRect( - start.position, - viewport.top, - end.position + end.size - start.position, - cellHeight - ); - for (let x = rectangle.left; x < rectangle.right; x++) { - selectedColumns.add(x); - } - }); - this.headingsGraphics.endFill(); - this.selectedColumns = Array.from(selectedColumns); - } - - // otherwise selected cursor is cursorPosition - else { - const offset = offsets.getColumnPlacement(cursor.cursorPosition.x); - this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedBackgroundColorAlpha); - this.headingsGraphics.drawRect(offset.position, viewport.top, offset.size, cellHeight); - this.headingsGraphics.endFill(); - this.selectedColumns = [cursor.cursorPosition.x]; + const leftColumn = sheet.getColumnFromScreen(left); + const rightColumn = sheet.getColumnFromScreen(left + bounds.width); + this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedRowColumnBackgroundColorAlpha); + + this.selectedColumns = cursor.getSelectedColumnRanges(leftColumn, rightColumn); + for (let i = 0; i < this.selectedColumns.length; i += 2) { + const startPlacement = offsets.getColumnPlacement(this.selectedColumns[i]); + const start = startPlacement.position; + let end: number; + if (this.selectedColumns[i] === this.selectedColumns[i + 1]) { + end = start + startPlacement.size; + } else { + const endPlacement = offsets.getColumnPlacement(this.selectedColumns[i + 1]); + end = endPlacement.position + endPlacement.size; + } + this.headingsGraphics.drawRect(start, viewport.top, end - start, cellHeight); } + this.headingsGraphics.endFill(); } // Adds horizontal labels @@ -169,7 +134,6 @@ export class GridHeadings extends Container { const cellWidth = CELL_WIDTH / scale; const cellHeight = CELL_HEIGHT / scale; const gridAlpha = calculateAlphaForGridLines(scale); - const showA1Notation = pixiAppSettings.showA1Notation; const start = offsets.getXPlacement(bounds.left); const end = offsets.getXPlacement(bounds.right); @@ -219,7 +183,7 @@ export class GridHeadings extends Container { currentWidth > charactersWidth || pixiApp.gridLines.alpha < colors.headerSelectedRowColumnBackgroundColorAlpha ) { - // don't show numbers if it overlaps with the selected value (eg, hides 0 if selected 1 overlaps it) + // don't show numbers if it overlaps with the selected value (eg, hides B if selected A overlaps it) let xPosition = x + currentWidth / 2; const left = xPosition - charactersWidth / 2; const right = xPosition + charactersWidth / 2; @@ -230,7 +194,14 @@ export class GridHeadings extends Container { // leave only the first. let intersectsLast = lastLabel && intersects.lineLineOneDimension(lastLabel.left, lastLabel.right, left, right); - const selectedColumns = Array.isArray(this.selectedColumns) ? [...this.selectedColumns] : []; + const selectedColumns = []; + if (this.selectedColumns) { + for (let i = 0; i < this.selectedColumns.length; i += 2) { + for (let j = Number(this.selectedColumns[i]); j <= Number(this.selectedColumns[i + 1]); j++) { + selectedColumns.push(j); + } + } + } if ( intersectsLast && selected && @@ -243,7 +214,7 @@ export class GridHeadings extends Container { // show only when selected or not intersects one of the selected numbers if (!intersectsLast) { - const text = showA1Notation ? getColumnA1Notation(column) : column.toString(); + const text = getColumnA1Notation(column); this.labels.add({ text, x: xPosition, y }); lastLabel = { left, right, selected }; } @@ -263,8 +234,10 @@ export class GridHeadings extends Container { const viewport = pixiApp.viewport; const bounds = viewport.getVisibleBounds(); - const offsets = sheets.sheet.offsets; - const cursor = sheets.sheet.cursor; + const sheet = sheets.sheet; + const cursor = sheet.cursor; + const offsets = sheet.offsets; + const clamp = sheet.clamp; const start = offsets.getYPlacement(bounds.top); const end = offsets.getYPlacement(bounds.bottom); @@ -279,66 +252,40 @@ export class GridHeadings extends Container { this.rowWidth = Math.max(this.rowWidth, CELL_HEIGHT / viewport.scale.x); // draw background of vertical bar +<<<<<<< HEAD this.gridHeadingsRows.headingsGraphics.lineStyle(0); this.gridHeadingsRows.headingsGraphics.beginFill(colors.headerBackgroundColor); this.columnRect = new Rectangle(bounds.left, bounds.top, this.rowWidth, bounds.height); this.gridHeadingsRows.headingsGraphics.drawShape(this.columnRect); this.gridHeadingsRows.headingsGraphics.endFill(); +======= + this.headingsGraphics.lineStyle(0); + this.headingsGraphics.beginFill(colors.headerBackgroundColor); + const top = Math.max(bounds.top, clamp.top); + this.columnRect = new Rectangle(bounds.left, top, this.rowWidth, bounds.height); + this.headingsGraphics.drawShape(this.columnRect); + this.headingsGraphics.endFill(); +>>>>>>> origin/qa this.rowRect = new Rectangle(bounds.left, bounds.top, this.rowWidth, bounds.height); - // fill the entire viewport if all cells are selected - if (cursor.columnRow?.all) { - this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedRowColumnBackgroundColorAlpha); - this.headingsGraphics.drawRect(bounds.left, bounds.top, this.rowWidth, bounds.height); - this.headingsGraphics.endFill(); - } - - // dark fill headings if there is a columnRow selection - if (cursor.columnRow?.rows) { - this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedRowColumnBackgroundColorAlpha); - cursor.columnRow.rows.forEach((row) => { - const offset = offsets.getRowPlacement(row); - this.headingsGraphics.drawRect(bounds.left, offset.position, this.rowWidth, offset.size); - }); - this.headingsGraphics.endFill(); - } - - // if we're selecting columns, then show all rows as selected - if (cursor.columnRow?.columns) { - this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedBackgroundColorAlpha); - this.headingsGraphics.drawRect(bounds.left, bounds.top, this.rowWidth, bounds.height); - this.headingsGraphics.endFill(); - } - - // selected cells based on multiCursor - if (cursor.multiCursor) { - const selectedRows = new Set(); - this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedBackgroundColorAlpha); - cursor.multiCursor.forEach((rectangle) => { - const start = offsets.getRowPlacement(rectangle.top); - const end = offsets.getRowPlacement(rectangle.bottom - 1); - this.headingsGraphics.drawRect( - bounds.left, - start.position, - this.rowWidth, - end.position + end.size - start.position - ); - for (let y = rectangle.top; y < rectangle.bottom; y++) { - selectedRows.add(y); - } - }); - this.headingsGraphics.endFill(); - this.selectedRows = Array.from(selectedRows); - } - - // otherwise selected cursor is cursorPosition - if (!cursor.multiCursor && !cursor.columnRow) { - const offset = offsets.getRowPlacement(cursor.cursorPosition.y); - this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedBackgroundColorAlpha); - this.headingsGraphics.drawRect(bounds.left, offset.position, this.rowWidth, offset.size); - this.headingsGraphics.endFill(); - this.selectedRows = [cursor.cursorPosition.y]; + const topRow = sheet.getRowFromScreen(top); + const bottomRow = sheet.getRowFromScreen(top + bounds.height); + this.headingsGraphics.beginFill(pixiApp.accentColor, colors.headerSelectedRowColumnBackgroundColorAlpha); + + this.selectedRows = cursor.getSelectedRowRanges(topRow, bottomRow); + for (let i = 0; i < this.selectedRows.length; i += 2) { + const startPlacement = offsets.getRowPlacement(this.selectedRows[i]); + const start = startPlacement.position; + let end: number; + if (this.selectedRows[i] === this.selectedRows[i + 1]) { + end = start + startPlacement.size; + } else { + const endPlacement = offsets.getRowPlacement(this.selectedRows[i + 1]); + end = endPlacement.position + endPlacement.size; + } + this.headingsGraphics.drawRect(bounds.left, start, this.rowWidth, end - start); } + this.headingsGraphics.endFill(); } private verticalLabels() { @@ -374,6 +321,15 @@ export class GridHeadings extends Container { const halfCharacterHeight = this.characterSize.height / scale; + const selectedRows = []; + if (this.selectedRows) { + for (let i = 0; i < this.selectedRows.length; i += 2) { + for (let j = this.selectedRows[i]; j <= this.selectedRows[i + 1]; j++) { + selectedRows.push(j); + } + } + } + for (let y = topOffset; y <= bottomOffset; y += currentHeight) { currentHeight = offsets.getRowHeight(row); if (gridAlpha !== 0) { @@ -390,13 +346,11 @@ export class GridHeadings extends Container { } // show selected numbers - const selected = Array.isArray(this.selectedRows) ? this.selectedRows.includes(row) : false; + const selected = selectedRows.includes(row); // only show the label if selected or mod calculation if (selected || mod === 0 || row % mod === 0) { // only show labels that will fit (unless grid lines are hidden) - // if (currentHeight > halfCharacterHeight * 2 || pixiApp.gridLines.alpha < colors.headerSelectedRowColumnBackgroundColorAlpha) { - // don't show numbers if it overlaps with the selected value (eg, hides 0 if selected 1 overlaps it) let yPosition = y + currentHeight / 2; const top = yPosition - halfCharacterHeight / 2; const bottom = yPosition + halfCharacterHeight / 2; @@ -406,7 +360,6 @@ export class GridHeadings extends Container { // selections, unless there is only two selections, in which case we // leave only the first. let intersectsLast = lastLabel && intersects.lineLineOneDimension(lastLabel.top, lastLabel.bottom, top, bottom); - const selectedRows = Array.isArray(this.selectedRows) ? [...this.selectedRows] : []; if ( intersectsLast && selected && @@ -434,7 +387,7 @@ export class GridHeadings extends Container { this.verticalLabels(); } - private drawCorner(): void { + private drawCorner() { const { viewport } = pixiApp; const bounds = viewport.getVisibleBounds(); const cellHeight = CELL_HEIGHT / viewport.scale.x; @@ -443,16 +396,28 @@ export class GridHeadings extends Container { this.cornerRect = new Rectangle(bounds.left, bounds.top, this.rowWidth, cellHeight); this.corner.drawShape(this.cornerRect); this.corner.endFill(); + this.corner.lineStyle(1, colors.gridLines, colors.headerSelectedRowColumnBackgroundColorAlpha, 0, true); + this.corner.moveTo(bounds.left + this.rowWidth, bounds.top); + this.corner.lineTo(bounds.left + this.rowWidth, bounds.top + cellHeight); + this.corner.lineTo(bounds.left, bounds.top + cellHeight); } - private drawHeadingLines(): void { + // draws the lines under and to the right of the headings + private drawHeadingLines() { const { viewport } = pixiApp; const cellHeight = CELL_HEIGHT / viewport.scale.x; const bounds = viewport.getVisibleBounds(); + const clamp = sheets.sheet.clamp; this.headingsGraphics.lineStyle(1, colors.gridLines, colors.headerSelectedRowColumnBackgroundColorAlpha, 0.5, true); - this.headingsGraphics.moveTo(bounds.left + this.rowWidth, viewport.top); + + // draw the left line to the right of the headings + const top = Math.max(bounds.top, clamp.top); + this.headingsGraphics.moveTo(bounds.left + this.rowWidth, top); this.headingsGraphics.lineTo(bounds.left + this.rowWidth, viewport.bottom); - this.headingsGraphics.moveTo(bounds.left, bounds.top + cellHeight); + + // draw the top line under the headings + const left = Math.max(bounds.left, clamp.left); + this.headingsGraphics.moveTo(left, bounds.top + cellHeight); this.headingsGraphics.lineTo(bounds.right, bounds.top + cellHeight); } @@ -461,8 +426,10 @@ export class GridHeadings extends Container { // selection (which requires a redraw) if ( !this.dirty && - !viewportDirty && - !(viewportDirty && (sheets.sheet.cursor.columnRow?.columns || sheets.sheet.cursor.columnRow?.rows)) + !viewportDirty + + // todo.... + // !(viewportDirty && (sheets.sheet.cursor.columnRow?.columns || sheets.sheet.cursor.columnRow?.rows)) ) { return; } diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/getA1Notation.test.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/getA1Notation.test.ts new file mode 100644 index 0000000000..8dac336963 --- /dev/null +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/getA1Notation.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { getA1Notation, getColumnA1Notation, getRowA1Notation } from './getA1Notation'; + +describe('A1 notation translation', () => { + it('gets column (based on tests in quadratic-core)', () => { + // Test near 0 + expect(getColumnA1Notation(1)).toBe('A'); + expect(getColumnA1Notation(2)).toBe('B'); + expect(getColumnA1Notation(3)).toBe('C'); + expect(getColumnA1Notation(4)).toBe('D'); + expect(getColumnA1Notation(5)).toBe('E'); + expect(getColumnA1Notation(6)).toBe('F'); + + // Test near ±26 + expect(getColumnA1Notation(25)).toBe('Y'); + expect(getColumnA1Notation(26)).toBe('Z'); + expect(getColumnA1Notation(27)).toBe('AA'); + expect(getColumnA1Notation(28)).toBe('AB'); + + // Test near ±52 + expect(getColumnA1Notation(51)).toBe('AY'); + expect(getColumnA1Notation(52)).toBe('AZ'); + expect(getColumnA1Notation(53)).toBe('BA'); + expect(getColumnA1Notation(54)).toBe('BB'); + + // Test near ±702 + expect(getColumnA1Notation(701)).toBe('ZY'); + expect(getColumnA1Notation(702)).toBe('ZZ'); + expect(getColumnA1Notation(703)).toBe('AAA'); + expect(getColumnA1Notation(704)).toBe('AAB'); + + // tests too big to be stored as integers are skipped + + // Test 64 bit integer limits (±9,223,372,036,854,775,807) + // expect(getColumnA1Notation(9223372036854775807n)).toBe('CRPXNLSKVLJFHH'); + + // test fun stuff + expect(getColumnA1Notation(3719092809669)).toBe('QUADRATIC'); + }); + + it('gets row (positive)', () => { + expect(getRowA1Notation(1)).toBe('1'); + expect(getRowA1Notation(100)).toBe('100'); + }); + + it('gets both column and row', () => { + expect(getA1Notation(1, 1)).toBe('A1'); + expect(getA1Notation(2, 2)).toBe('B2'); + }); +}); diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/getA1Notation.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/getA1Notation.ts index 0eb18673da..239bce04ac 100644 --- a/quadratic-client/src/app/gridGL/UI/gridHeadings/getA1Notation.ts +++ b/quadratic-client/src/app/gridGL/UI/gridHeadings/getA1Notation.ts @@ -2,9 +2,12 @@ * Turns a positive column number into A1 notation * based on https://www.labnol.org/convert-column-a1-notation-210601 * @param column - * @returns + * @returns a string */ -function translateNumberToA1Notation(column: number): string { +export function getColumnA1Notation(column: number): string { + // adjust for 1-indexing (ie, A1 starts at 1 instead of 0) + column -= 1; + const a1Notation: string[] = []; const totalAlphabets = 'Z'.charCodeAt(0) - 'A'.charCodeAt(0) + 1; let block = column; @@ -15,20 +18,8 @@ function translateNumberToA1Notation(column: number): string { return a1Notation.join(''); } -/** - * Turns a quadratic numbered column into A1 notation - * @param column - * @returns - */ -export function getColumnA1Notation(column: number): string { - if (column < 0) return `n${translateNumberToA1Notation(-column - 1)}`; - return translateNumberToA1Notation(column); -} - export function getRowA1Notation(row: number): string { - if (row > 0) return row.toString(); - if (row === 0) return '0'; - return `n${-row}`; + return row.toString(); } export function getA1Notation(column: number, row: number): string { diff --git a/quadratic-client/src/app/gridGL/UI/gridHeadings/tests/getA1Notation.test.ts b/quadratic-client/src/app/gridGL/UI/gridHeadings/tests/getA1Notation.test.ts deleted file mode 100644 index 4e6b3d9b95..0000000000 --- a/quadratic-client/src/app/gridGL/UI/gridHeadings/tests/getA1Notation.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getA1Notation, getColumnA1Notation, getRowA1Notation } from '../getA1Notation'; - -describe('A1 notation translation', () => { - it('gets column (based on tests in quadratic-core)', () => { - // Test near 0 - expect(getColumnA1Notation(0)).toBe('A'); - expect(getColumnA1Notation(1)).toBe('B'); - expect(getColumnA1Notation(2)).toBe('C'); - expect(getColumnA1Notation(3)).toBe('D'); - expect(getColumnA1Notation(4)).toBe('E'); - expect(getColumnA1Notation(5)).toBe('F'); - - expect(getColumnA1Notation(-1)).toBe('nA'); - expect(getColumnA1Notation(-2)).toBe('nB'); - expect(getColumnA1Notation(-3)).toBe('nC'); - expect(getColumnA1Notation(-4)).toBe('nD'); - expect(getColumnA1Notation(-5)).toBe('nE'); - expect(getColumnA1Notation(-6)).toBe('nF'); - - // Test near ±26 - expect(getColumnA1Notation(24)).toBe('Y'); - expect(getColumnA1Notation(25)).toBe('Z'); - expect(getColumnA1Notation(26)).toBe('AA'); - expect(getColumnA1Notation(27)).toBe('AB'); - expect(getColumnA1Notation(-25)).toBe('nY'); - expect(getColumnA1Notation(-26)).toBe('nZ'); - expect(getColumnA1Notation(-27)).toBe('nAA'); - expect(getColumnA1Notation(-28)).toBe('nAB'); - - // Test near ±52 - expect(getColumnA1Notation(50)).toBe('AY'); - expect(getColumnA1Notation(51)).toBe('AZ'); - expect(getColumnA1Notation(52)).toBe('BA'); - expect(getColumnA1Notation(53)).toBe('BB'); - expect(getColumnA1Notation(-51)).toBe('nAY'); - expect(getColumnA1Notation(-52)).toBe('nAZ'); - expect(getColumnA1Notation(-53)).toBe('nBA'); - expect(getColumnA1Notation(-54)).toBe('nBB'); - - // Test near ±702 - expect(getColumnA1Notation(700)).toBe('ZY'); - expect(getColumnA1Notation(701)).toBe('ZZ'); - expect(getColumnA1Notation(702)).toBe('AAA'); - expect(getColumnA1Notation(703)).toBe('AAB'); - expect(getColumnA1Notation(-701)).toBe('nZY'); - expect(getColumnA1Notation(-702)).toBe('nZZ'); - expect(getColumnA1Notation(-703)).toBe('nAAA'); - expect(getColumnA1Notation(-704)).toBe('nAAB'); - - // tests too big to be stored as integers are skipped - - // // Test 64 bit integer limits (±9,223,372,036,854,775,807) - // expect(getColumnA1Notation(9223372036854775807n)).toBe('CRPXNLSKVLJFHH'); - // expect(getColumnA1Notation(-9223372036854775808n)).toBe('nCRPXNLSKVLJFHH'); - - // test fun stuff - expect(getColumnA1Notation(3719092809668)).toBe('QUADRATIC'); - expect(getColumnA1Notation(-3719092809669)).toBe('nQUADRATIC'); - // expect(getColumnA1Notation(1700658608758053877)).toBe('QUICKBROWNFOX'); - }); - - it('gets row (positive)', () => { - expect(getRowA1Notation(0)).toBe('0'); - expect(getRowA1Notation(1)).toBe('1'); - expect(getRowA1Notation(100)).toBe('100'); - }); - - it('gets row (negative)', () => { - expect(getRowA1Notation(-1)).toBe('n1'); - expect(getRowA1Notation(-100)).toBe('n100'); - }); - - it('gets both column and row', () => { - expect(getA1Notation(0, 0)).toBe('A0'); - expect(getA1Notation(1, 1)).toBe('B1'); - }); -}); diff --git a/quadratic-client/src/app/gridGL/cells/Borders.ts b/quadratic-client/src/app/gridGL/cells/Borders.ts new file mode 100644 index 0000000000..4c730d38f4 --- /dev/null +++ b/quadratic-client/src/app/gridGL/cells/Borders.ts @@ -0,0 +1,217 @@ +//! Draws both cell-based borders and sheet-based borders. The cell-based +//! borders are saved and culled during the update loop. The sheet-based borders +//! are always redrawn whenever the viewport changes. +//! +//! The logic in this is rather complicated as we need to remove overlapping +//! borders based on timestamps. For example, if you have a column with a left +//! border, before drawing that left border, you need to check if there are any +//! existing, visible cell-based, either left in that column, or right in the +//! previous column. You also need to check if there are any row-based borders +//! that have a left or right that would overlap the vertical line (and have a +//! later timestamp). +//! +//! Regrettably, you can't just draw over the border as they may be different +//! widths and this would remove any transparent background. + +import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { JsBorderHorizontal, JsBordersSheet, JsBorderVertical } from '@/app/quadratic-core-types'; +import { Container, Rectangle, Sprite, Texture, TilingSprite } from 'pixi.js'; +import { Sheet } from '../../grid/sheet/Sheet'; +import { pixiApp } from '../pixiApp/PixiApp'; +import { CellsSheet } from './CellsSheet'; +import { BorderCull, drawCellBorder } from './drawBorders'; + +// this sets when to fade the sheet borders when (for performance reasons) +const SCALE_TO_SHOW_SHEET_BORDERS = 0.15; +const FADE_SCALE = 0.1; + +export class Borders extends Container { + private cellsSheet: CellsSheet; + private cellLines: Container; + private spriteLines: BorderCull[]; + + private sheetLines: Container; + private bordersFinite?: JsBordersSheet; + private bordersInfinite?: JsBordersSheet; + + dirty = false; + + constructor(cellsSheet: CellsSheet) { + super(); + this.cellsSheet = cellsSheet; + this.sheetLines = this.addChild(new Container()); + this.cellLines = this.addChild(new Container()); + this.spriteLines = []; + + events.on('bordersSheet', this.drawSheetCells); + events.on('sheetOffsets', this.sheetOffsetsChanged); + } + + destroy() { + events.off('bordersSheet', this.drawSheetCells); + events.off('sheetOffsets', this.sheetOffsetsChanged); + super.destroy(); + } + + sheetOffsetsChanged = (sheetId: string) => { + if (sheetId === this.cellsSheet.sheetId) { + this.draw(); + this.dirty = true; + } + }; + + private get sheet(): Sheet { + const sheet = sheets.getById(this.cellsSheet.sheetId); + if (!sheet) throw new Error(`Expected sheet to be defined in CellsBorders.sheet`); + return sheet; + } + + // Draws horizontal border and returns the y offset of the current border + private drawHorizontal(border: JsBorderHorizontal, bounds: Rectangle): number { + if (border.width !== null) { + const start = this.sheet.getCellOffsets(Number(border.x), Number(border.y)); + const end = this.sheet.getCellOffsets(Number(border.x + border.width), Number(border.y)); + const color = border.color; + this.spriteLines.push( + ...drawCellBorder({ + position: new Rectangle(start.x, start.y, end.x - start.x, end.y - start.y), + horizontal: { type: border.line, color }, + getSprite: border.unbounded ? this.getSpriteSheet : this.getSprite, + }) + ); + return end.y; + } else { + const start = this.sheet.getCellOffsets(Number(border.x), Number(border.y)); + const xStart = Math.max(start.x, bounds.left); + const xEnd = bounds.right; + const yStart = Math.max(start.y, bounds.top); + if (yStart > bounds.bottom || yStart < bounds.top || xStart > bounds.right) return Infinity; + drawCellBorder({ + position: new Rectangle(xStart, yStart, xEnd - xStart, 0), + horizontal: { type: border.line, color: border.color }, + getSprite: this.getSpriteSheet, + }); + return yStart; + } + } + + // Draws vertical border and returns the x offset of the current border + private drawVertical(border: JsBorderVertical, bounds: Rectangle): number { + if (border.height !== null) { + const start = this.sheet.getCellOffsets(Number(border.x), Number(border.y)); + const end = this.sheet.getCellOffsets(Number(border.x), Number(border.y + border.height)); + this.spriteLines.push( + ...drawCellBorder({ + position: new Rectangle(start.x, start.y, end.x - start.x, end.y - start.y), + vertical: { type: border.line, color: border.color }, + getSprite: border.unbounded ? this.getSpriteSheet : this.getSprite, + }) + ); + return end.x; + } else { + const start = this.sheet.getCellOffsets(Number(border.x), Number(border.y)); + const xStart = Math.max(start.x, bounds.left); + const yStart = Math.max(start.y, bounds.top); + const yEnd = bounds.bottom; + if (xStart > bounds.right || xStart < bounds.left || yStart > bounds.bottom) return Infinity; + drawCellBorder({ + position: new Rectangle(xStart, yStart, 0, yEnd - yStart), + vertical: { type: border.line, color: border.color }, + getSprite: this.getSpriteSheet, + }); + return xStart; + } + } + + drawSheetCells = (sheetId: string, borders?: JsBordersSheet): void => { + if (sheetId === this.cellsSheet.sheetId) { + if (borders) { + this.bordersFinite = { + horizontal: borders.horizontal?.filter((border) => border.width !== null && !border.unbounded) || null, + vertical: borders.vertical?.filter((border) => border.height !== null && !border.unbounded) || null, + }; + this.bordersInfinite = { + horizontal: borders.horizontal?.filter((border) => border.width === null || border.unbounded) || null, + vertical: borders.vertical?.filter((border) => border.height === null || border.unbounded) || null, + }; + } + this.draw(); + this.dirty = true; + } + }; + + private draw() { + this.cellLines.removeChildren(); + const bounds = pixiApp.viewport.getVisibleBounds(); + this.bordersFinite?.horizontal?.forEach((border) => this.drawHorizontal(border, bounds)); + this.bordersFinite?.vertical?.forEach((border) => this.drawVertical(border, bounds)); + } + + private cull() { + const bounds = pixiApp.viewport.getVisibleBounds(); + this.spriteLines.forEach((sprite) => { + sprite.sprite.visible = sprite.rectangle.intersects(bounds); + }); + } + + update() { + const viewportDirty = pixiApp.viewport.dirty; + if (!this.dirty && !viewportDirty) return; + if (pixiApp.viewport.scale.x < SCALE_TO_SHOW_SHEET_BORDERS) { + this.sheetLines.visible = false; + } else { + this.sheetLines.removeChildren(); + this.sheetLines.visible = true; + const bounds = pixiApp.viewport.getVisibleBounds(); + this.bordersInfinite?.horizontal?.forEach((border) => { + if (border.unbounded) { + let yOffset = 0, + y = border.y; + while (yOffset < bounds.bottom) { + yOffset = this.drawHorizontal({ ...border, y }, bounds); + y++; + } + } else { + this.drawHorizontal(border, bounds); + } + }); + this.bordersInfinite?.vertical?.forEach((border) => { + if (border.unbounded) { + let xOffset = 0, + x = border.x; + while (xOffset < bounds.right) { + xOffset = this.drawVertical({ ...border, x }, bounds); + x++; + } + } else { + this.drawVertical(border, bounds); + } + }); + if (pixiApp.viewport.scale.x < SCALE_TO_SHOW_SHEET_BORDERS + FADE_SCALE) { + this.sheetLines.alpha = (pixiApp.viewport.scale.x - SCALE_TO_SHOW_SHEET_BORDERS) / FADE_SCALE; + } else { + this.sheetLines.alpha = 1; + } + pixiApp.setViewportDirty(); + } + this.dirty = false; + this.cull(); + } + + private getSpriteSheet = (tiling?: boolean): Sprite | TilingSprite => { + if (tiling) { + return this.sheetLines.addChild(new TilingSprite(Texture.WHITE)); + } else { + return this.sheetLines.addChild(new Sprite(Texture.WHITE)); + } + }; + + private getSprite = (tiling?: boolean): Sprite | TilingSprite => { + if (tiling) { + return this.cellLines.addChild(new TilingSprite(Texture.WHITE)); + } else { + return this.cellLines.addChild(new Sprite(Texture.WHITE)); + } + }; +} diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts new file mode 100644 index 0000000000..07cf269c35 --- /dev/null +++ b/quadratic-client/src/app/gridGL/cells/CellsArray.ts @@ -0,0 +1,285 @@ +import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { Sheet } from '@/app/grid/sheet/Sheet'; +import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; +import { BorderCull, borderLineWidth, drawBorder, drawLine } from '@/app/gridGL/cells/drawBorders'; +import { generatedTextures } from '@/app/gridGL/generateTextures'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; +import { JsCodeCell, JsCoordinate, JsRenderCodeCell, RunError } from '@/app/quadratic-core-types'; +import { colors } from '@/app/theme/colors'; +import mixpanel from 'mixpanel-browser'; +import { Container, Graphics, ParticleContainer, Point, Rectangle, Sprite, Texture } from 'pixi.js'; + +const SPILL_HIGHLIGHT_THICKNESS = 1; +const SPILL_HIGHLIGHT_COLOR = colors.cellColorError; +const SPILL_FILL_ALPHA = 0.025; + +export class CellsArray extends Container { + private cellsSheet: CellsSheet; + private codeCells: Map; + + private particles: ParticleContainer; + // only used for the spill error indicators (lines are drawn using sprites in particles for performance) + private graphics: Graphics; + private lines: BorderCull[]; + + constructor(cellsSheet: CellsSheet) { + super(); + this.particles = this.addChild(new ParticleContainer(undefined, { vertices: true, tint: true }, undefined, true)); + this.graphics = this.addChild(new Graphics()); + this.cellsSheet = cellsSheet; + this.lines = []; + this.codeCells = new Map(); + events.on('renderCodeCells', this.renderCodeCells); + events.on('sheetOffsets', this.sheetOffsets); + events.on('updateCodeCell', this.updateCodeCell); + } + + destroy() { + events.off('renderCodeCells', this.renderCodeCells); + events.off('sheetOffsets', this.sheetOffsets); + events.off('updateCodeCell', this.updateCodeCell); + super.destroy(); + } + + private key(x: number, y: number): string { + return `${x},${y}`; + } + + private renderCodeCells = (sheetId: string, codeCells: JsRenderCodeCell[]) => { + if (sheetId === this.sheetId) { + const map = new Map(); + codeCells.forEach((cell) => map.set(this.key(cell.x, cell.y), cell)); + this.codeCells = map; + this.create(); + } + }; + + private sheetOffsets = (sheetId: string) => { + if (sheetId === this.cellsSheet.sheetId) { + this.create(); + } + }; + + private updateCodeCell = (options: { + sheetId: string; + x: number; + y: number; + renderCodeCell?: JsRenderCodeCell; + codeCell?: JsCodeCell; + }) => { + const { sheetId, x, y, renderCodeCell, codeCell } = options; + if (sheetId === this.sheetId) { + if (renderCodeCell) { + this.codeCells.set(this.key(x, y), renderCodeCell); + } else { + this.codeCells.delete(this.key(x, y)); + } + this.create(); + + if (!!codeCell && codeCell.std_err !== null && codeCell.evaluation_result) { + try { + // std_err is not null, so evaluation_result will be RunError + const runError = JSON.parse(codeCell.evaluation_result) as RunError; + // track unimplemented errors + if (typeof runError.msg === 'object' && 'Unimplemented' in runError.msg) { + mixpanel.track('[CellsArray].updateCodeCell', { + type: codeCell.language, + error: runError.msg, + }); + } + } catch (error) { + console.error('[CellsArray] Error parsing codeCell.evaluation_result', error); + } + } + } + }; + + get sheetId(): string { + return this.cellsSheet.sheetId; + } + + private create() { + this.lines = []; + this.particles.removeChildren(); + this.graphics.clear(); + this.cellsSheet.cellsMarkers.clear(); + const codeCells = this.codeCells; + if (codeCells.size === 0) { + pixiApp.cursor.dirty = true; + pixiApp.setViewportDirty(); + return; + } + + const cursor = sheets.sheet.cursor.position; + codeCells?.forEach((codeCell) => { + const cell = inlineEditorHandler.getShowing(); + const editingCell = cell && codeCell.x === cell.x && codeCell.y === cell.y && cell.sheetId === this.sheetId; + this.draw(codeCell, cursor, editingCell); + }); + if (pixiApp.cursor) { + pixiApp.cursor.dirty = true; + } + pixiApp.setViewportDirty(); + } + + updateCellsArray = () => { + this.create(); + }; + + cheapCull = (bounds: Rectangle): void => { + this.lines.forEach((line) => (line.sprite.visible = intersects.rectangleRectangle(bounds, line.rectangle))); + }; + + get sheet(): Sheet { + const sheet = sheets.getById(this.sheetId); + if (!sheet) throw new Error('Expected sheet to be defined in CellsArray.sheet'); + return sheet; + } + + private draw(codeCell: JsRenderCodeCell, cursor: JsCoordinate, editingCell?: boolean): void { + const start = this.sheet.getCellOffsets(Number(codeCell.x), Number(codeCell.y)); + + const overlapTest = new Rectangle(Number(codeCell.x), Number(codeCell.y), codeCell.w - 1, codeCell.h - 1); + if (codeCell.spill_error) { + overlapTest.width = 1; + overlapTest.height = 1; + } + + let tint = colors.independence; + if (codeCell.language === 'Python') { + tint = colors.cellColorUserPython; + } else if (codeCell.language === 'Formula') { + tint = colors.cellColorUserFormula; + } else if (codeCell.language === 'Javascript') { + tint = colors.cellColorUserJavascript; + } + + if (!pixiAppSettings.showCellTypeOutlines) { + // only show the entire array if the cursor overlaps any part of the output + if (!intersects.rectanglePoint(overlapTest, new Point(cursor.x, cursor.y))) { + this.cellsSheet.cellsMarkers.add(start, codeCell, false); + return; + } + } + + if (!editingCell) { + this.cellsSheet.cellsMarkers.add(start, codeCell, true); + } + const end = this.sheet.getCellOffsets(Number(codeCell.x) + codeCell.w, Number(codeCell.y) + codeCell.h); + if (codeCell.spill_error) { + const cursorPosition = sheets.sheet.cursor.position; + if (cursorPosition.x !== Number(codeCell.x) || cursorPosition.y !== Number(codeCell.y)) { + this.lines.push( + ...drawBorder({ + alpha: 0.5, + tint, + x: start.x, + y: start.y, + width: start.width, + height: start.height, + getSprite: this.getSprite, + top: true, + left: true, + bottom: true, + right: true, + }) + ); + } else { + this.drawDashedRectangle(new Rectangle(start.x, start.y, end.x - start.x, end.y - start.y), tint); + codeCell.spill_error?.forEach((error) => { + const rectangle = this.sheet.getCellOffsets(Number(error.x), Number(error.y)); + this.drawDashedRectangle(rectangle, SPILL_HIGHLIGHT_COLOR); + }); + } + } else { + this.drawBox(start, end, tint); + } + } + + private drawBox(start: Rectangle, end: Rectangle, tint: number) { + this.lines.push( + ...drawBorder({ + alpha: 0.5, + tint, + x: start.x, + y: start.y, + width: end.x - start.x, + height: end.y - start.y, + getSprite: this.getSprite, + top: true, + left: true, + bottom: true, + right: true, + }) + ); + const right = end.x !== start.x + start.width; + if (right) { + this.lines.push( + drawLine({ + x: start.x + start.width - borderLineWidth / 2, + y: start.y + borderLineWidth / 2, + width: borderLineWidth, + height: start.height, + alpha: 0.5, + tint, + getSprite: this.getSprite, + }) + ); + } + const bottom = end.y !== start.y + start.height; + if (bottom) { + this.lines.push( + drawLine({ + x: start.x + borderLineWidth / 2, + y: start.y + start.height - borderLineWidth / 2, + width: start.width - borderLineWidth, + height: borderLineWidth, + alpha: 0.5, + tint, + getSprite: this.getSprite, + }) + ); + } + } + + private drawDashedRectangle(rectangle: Rectangle, color: number) { + this.graphics.lineStyle(); + this.graphics.beginFill(color, SPILL_FILL_ALPHA); + this.graphics.drawRect(rectangle.left, rectangle.top, rectangle.width, rectangle.height); + this.graphics.endFill(); + + const minX = rectangle.left; + const minY = rectangle.top; + const maxX = rectangle.right; + const maxY = rectangle.bottom; + + const path = [ + [maxX, minY], + [maxX, maxY], + [minX, maxY], + [minX, minY], + ]; + + this.graphics.moveTo(minX, minY); + for (let i = 0; i < path.length; i++) { + this.graphics.lineStyle({ + width: SPILL_HIGHLIGHT_THICKNESS, + color, + texture: i % 2 === 0 ? generatedTextures.dashedHorizontal : generatedTextures.dashedVertical, + }); + this.graphics.lineTo(path[i][0], path[i][1]); + } + } + + private getSprite = (): Sprite => { + return this.particles.addChild(new Sprite(Texture.WHITE)); + }; + + isCodeCell(x: number, y: number): boolean { + return this.codeCells.has(this.key(x, y)); + } +} diff --git a/quadratic-client/src/app/gridGL/cells/CellsFills.ts b/quadratic-client/src/app/gridGL/cells/CellsFills.ts index 6948eca814..0839a683e0 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsFills.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsFills.ts @@ -1,19 +1,32 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +<<<<<<< HEAD import { JsRenderCodeCell, JsRenderFill, JsSheetFill } from '@/app/quadratic-core-types'; +======= +import { Sheet } from '@/app/grid/sheet/Sheet'; +import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { convertColorStringToTint } from '@/app/helpers/convertColor'; +import { JsRenderFill, JsSheetFill } from '@/app/quadratic-core-types'; +>>>>>>> origin/qa import { colors } from '@/app/theme/colors'; import { sharedEvents } from '@/shared/sharedEvents'; import { Container, Graphics, ParticleContainer, Rectangle, Sprite, Texture } from 'pixi.js'; +<<<<<<< HEAD import { Sheet } from '../../grid/sheet/Sheet'; import { convertColorStringToTint, getCSSVariableTint } from '../../helpers/convertColor'; import { intersects } from '../helpers/intersects'; import { pixiApp } from '../pixiApp/PixiApp'; import { CellsSheet } from './CellsSheet'; +======= +>>>>>>> origin/qa interface SpriteBounds extends Sprite { viewBounds: Rectangle; } +<<<<<<< HEAD interface ColumnRow { row: number | null; column: number | null; @@ -28,6 +41,12 @@ export class CellsFills extends Container { private cells: JsRenderFill[] = []; private metaFill?: JsSheetFill; private alternatingColors: Map = new Map(); +======= +export class CellsFills extends Container { + private cellsSheet: CellsSheet; + private cells: JsRenderFill[] = []; + private sheetFills?: JsSheetFill[]; +>>>>>>> origin/qa private cellsContainer: ParticleContainer; private alternatingColorsGraphics: Graphics; @@ -44,6 +63,7 @@ export class CellsFills extends Container { new ParticleContainer(undefined, { vertices: true, tint: true }, undefined, true) ); +<<<<<<< HEAD events.on('sheetFills', (sheetId, fills) => { if (sheetId === this.cellsSheet.sheetId) { this.cells = fills; @@ -63,27 +83,58 @@ export class CellsFills extends Container { } }); +======= + events.on('sheetFills', this.handleSheetFills); + events.on('sheetMetaFills', this.handleSheetMetaFills); +>>>>>>> origin/qa events.on('sheetOffsets', this.drawSheetCells); events.on('cursorPosition', this.setDirty); events.on('resizeHeadingColumn', this.drawCells); events.on('resizeHeadingRow', this.drawCells); +<<<<<<< HEAD sharedEvents.on('changeThemeAccentColor', this.drawAlternatingColors); pixiApp.viewport.on('zoomed', this.setDirty); pixiApp.viewport.on('moved', this.setDirty); +======= + events.on('resizeHeadingRow', this.drawSheetCells); + events.on('resizeHeadingColumn', this.drawSheetCells); + events.on('viewportChanged', this.setDirty); +>>>>>>> origin/qa } destroy() { - events.off('sheetFills', this.drawCells); - events.off('sheetMetaFills', this.drawMeta); + events.off('sheetFills', this.handleSheetFills); + events.off('sheetMetaFills', this.handleSheetMetaFills); events.off('sheetOffsets', this.drawSheetCells); events.off('cursorPosition', this.setDirty); events.off('resizeHeadingColumn', this.drawCells); events.off('resizeHeadingRow', this.drawCells); - pixiApp.viewport.off('zoomed', this.setDirty); - pixiApp.viewport.off('moved', this.setDirty); + events.off('resizeHeadingRow', this.drawSheetCells); + events.off('resizeHeadingColumn', this.drawSheetCells); + events.off('viewportChanged', this.setDirty); super.destroy(); } + private handleSheetFills = (sheetId: string, fills: JsRenderFill[]) => { + if (sheetId === this.cellsSheet.sheetId) { + this.cells = fills; + this.drawCells(); + } + }; + + private handleSheetMetaFills = (sheetId: string, fills: JsSheetFill[]) => { + if (sheetId === this.cellsSheet.sheetId) { + if (fills.length === 0) { + this.sheetFills = undefined; + this.meta.clear(); + pixiApp.setViewportDirty(); + } else { + this.sheetFills = fills; + this.setDirty(); + } + } + }; + setDirty = () => { this.dirty = true; }; @@ -102,10 +153,6 @@ export class CellsFills extends Container { } } - private isMetaEmpty(fill: JsSheetFill): boolean { - return !(fill.all || fill.columns.length || fill.rows.length); - } - private get sheet(): Sheet { const sheet = sheets.getById(this.cellsSheet.sheetId); if (!sheet) throw new Error(`Expected sheet to be defined in CellsFills.sheet`); @@ -117,7 +164,7 @@ export class CellsFills extends Container { this.cells.forEach((fill) => { const sprite = this.cellsContainer.addChild(new Sprite(Texture.WHITE)) as SpriteBounds; sprite.tint = this.getColor(fill.color); - const screen = this.sheet.getScreenRectangle(Number(fill.x), Number(fill.y), fill.w - 1, fill.h - 1); + const screen = this.sheet.getScreenRectangle(Number(fill.x), Number(fill.y), fill.w, fill.h); sprite.position.set(screen.x, screen.y); sprite.width = screen.width; sprite.height = screen.height; @@ -141,53 +188,48 @@ export class CellsFills extends Container { }; private drawMeta = () => { - if (this.metaFill) { + if (this.sheetFills) { this.meta.clear(); const viewport = pixiApp.viewport.getVisibleBounds(); - if (this.metaFill.all) { - this.meta.beginFill(this.getColor(this.metaFill.all)); - this.meta.drawRect(viewport.left, viewport.top, viewport.width, viewport.height); - this.meta.endFill(); - } - - // combine the column and row fills and sort them by their timestamp so - // they are drawn in the correct order - const columns: ColumnRow[] = this.metaFill.columns.map((entry) => ({ - column: Number(entry[0]), - row: null, - color: entry[1][0], - timestamp: Number(entry[1][1]), - })); - const rows: ColumnRow[] = this.metaFill.rows.map((entry) => ({ - column: null, - row: Number(entry[0]), - color: entry[1][0], - timestamp: Number(entry[1][1]), - })); - const fills = [...columns, ...rows].sort((a, b) => a.timestamp - b.timestamp); - - fills.forEach((fill) => { - if (fill.column !== null) { - const screen = this.sheet.offsets.getColumnPlacement(Number(fill.column)); - const left = screen.position; - const width = screen.size; - - // only draw if the column is visible on the screen - if (left >= viewport.right || left + width <= viewport.left) return; + this.sheetFills.forEach((fill) => { + const offset = this.sheet.getCellOffsets(fill.x, fill.y); + if (offset.x > viewport.right || offset.y > viewport.bottom) return; + // infinite sheet + if (fill.w == null && fill.h == null) { this.meta.beginFill(this.getColor(fill.color)); - this.meta.drawRect(left, viewport.top, width, viewport.height); + const x = Math.max(offset.x, viewport.left); + const y = Math.max(offset.y, viewport.top); + this.meta.drawRect( + x, + y, + viewport.width - (offset.x - viewport.left), + viewport.height - (offset.y - viewport.top) + ); this.meta.endFill(); - } else if (fill.row !== null) { - const screen = this.sheet.offsets.getRowPlacement(fill.row); - const top = screen.position; - const height = screen.size; + } - // only draw if the row is visible on the screen - if (top >= viewport.bottom || top + height <= viewport.top) return; + // infinite column + else if (fill.h == null && fill.w != null) { + this.meta.beginFill(this.getColor(fill.color)); + const startX = Math.max(offset.x, viewport.left); + const startY = Math.max(offset.y, viewport.top); + const end = this.sheet.offsets.getColumnPlacement(Number(fill.x) + Number(fill.w)); + let endX = end.position; + endX = Math.min(endX, viewport.right); + this.meta.drawRect(startX, startY, endX - startX, viewport.height - (startY - viewport.top)); + this.meta.endFill(); + } + // infinite row + else if (fill.w == null && fill.h != null) { this.meta.beginFill(this.getColor(fill.color)); - this.meta.drawRect(viewport.left, top, viewport.width, height); + const startX = Math.max(offset.x, viewport.left); + const startY = Math.max(offset.y, viewport.top); + const end = this.sheet.offsets.getRowPlacement(Number(fill.y) + Number(fill.h)); + let endY = end.position; + endY = Math.min(endY, viewport.bottom); + this.meta.drawRect(startX, startY, viewport.width - (startX - viewport.left), endY - startY); this.meta.endFill(); } }); diff --git a/quadratic-client/src/app/gridGL/cells/CellsSearch.ts b/quadratic-client/src/app/gridGL/cells/CellsSearch.ts index 94526f730e..669b83c497 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSearch.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSearch.ts @@ -1,9 +1,9 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { SheetPosTS } from '@/app/gridGL/types/size'; import { colors } from '@/app/theme/colors'; import { Graphics } from 'pixi.js'; -import { pixiApp } from '../pixiApp/PixiApp'; -import { SheetPosTS } from '../types/size'; export class CellsSearch extends Graphics { private sheetId: string; @@ -14,6 +14,11 @@ export class CellsSearch extends Graphics { events.on('search', this.handleSearch); } + destroy() { + events.off('search', this.handleSearch); + super.destroy(); + } + private handleSearch = (found?: SheetPosTS[], current?: number) => { this.clear(); if (found?.length) { diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts index f396ca4fd9..44efc07076 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheet.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheet.ts @@ -1,4 +1,5 @@ import { events } from '@/app/events/events'; +<<<<<<< HEAD import { Tables } from '@/app/gridGL/cells/tables/Tables'; import { JsRenderCodeCell, JsValidationWarning } from '@/app/quadratic-core-types'; import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; @@ -10,6 +11,20 @@ import { CellsImages } from './cellsImages/CellsImages'; import { CellsLabels } from './cellsLabel/CellsLabels'; import { CellsMarkers } from './CellsMarkers'; import { CellsSearch } from './CellsSearch'; +======= +import { Borders } from '@/app/gridGL/cells/Borders'; +import { CellsArray } from '@/app/gridGL/cells/CellsArray'; +import { CellsFills } from '@/app/gridGL/cells/CellsFills'; +import { CellsImage } from '@/app/gridGL/cells/cellsImages/CellsImage'; +import { CellsImages } from '@/app/gridGL/cells/cellsImages/CellsImages'; +import { CellsLabels } from '@/app/gridGL/cells/cellsLabel/CellsLabels'; +import { CellsMarkers } from '@/app/gridGL/cells/CellsMarkers'; +import { CellsSearch } from '@/app/gridGL/cells/CellsSearch'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { JsValidationWarning } from '@/app/quadratic-core-types'; +import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; +import { Container, Rectangle, Sprite } from 'pixi.js'; +>>>>>>> origin/qa export interface ErrorMarker { triangle?: Sprite; @@ -54,6 +69,11 @@ export class CellsSheet extends Container { events.on('renderValidationWarnings', this.renderValidations); } + destroy() { + events.off('renderValidationWarnings', this.renderValidations); + super.destroy(); + } + // used to render all cellsTextHashes to warm up the GPU showAll() { this.visible = true; @@ -85,8 +105,16 @@ export class CellsSheet extends Container { } adjustOffsets() { +<<<<<<< HEAD this.borders.setDirty(); this.tables.sheetOffsets(this.sheetId); +======= + this.borders.sheetOffsetsChanged(this.sheetId); + } + + updateCellsArray() { + this.cellsArray.updateCellsArray(); +>>>>>>> origin/qa } getCellsImages(): CellsImage[] { diff --git a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts index dcf7d105d0..6f053c608a 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsSheets.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsSheets.ts @@ -1,5 +1,8 @@ import { debugShowCellsHashBoxes } from '@/app/debugFlags'; import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { SheetInfo } from '@/app/quadratic-core-types'; import { RenderClientCellsTextHashClear, @@ -8,9 +11,6 @@ import { } from '@/app/web-workers/renderWebWorker/renderClientMessages'; import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; import { Container, Rectangle } from 'pixi.js'; -import { sheets } from '../../grid/controller/Sheets'; -import { pixiApp } from '../pixiApp/PixiApp'; -import { CellsSheet } from './CellsSheet'; export class CellsSheets extends Container { current?: CellsSheet; @@ -21,6 +21,12 @@ export class CellsSheets extends Container { events.on('deleteSheet', this.deleteSheet); } + destroy() { + events.off('addSheet', this.addSheet); + events.off('deleteSheet', this.deleteSheet); + super.destroy(); + } + async create() { this.removeChildren(); for (const sheet of sheets.sheets) { @@ -99,6 +105,8 @@ export class CellsSheets extends Container { } const key = `${message.hashX},${message.hashY}`; sheet.gridOverflowLines.updateHash(key, message.overflowGridLines); + + events.emit('hashContentChanged', message.sheetId, message.hashX, message.hashY); } labelMeshEntry(message: RenderClientLabelMeshEntry) { @@ -176,8 +184,13 @@ export class CellsSheets extends Container { isCursorOnCodeCell(): boolean { const cellsSheet = this.current; if (!cellsSheet) return false; +<<<<<<< HEAD const cursor = sheets.sheet.cursor.cursorPosition; return cellsSheet.tables.isTable(cursor.x, cursor.y); +======= + const cursor = sheets.sheet.cursor.position; + return cellsSheet.cellsArray.isCodeCell(cursor.x, cursor.y); +>>>>>>> origin/qa } isCursorOnCodeCellOutput(): boolean { diff --git a/quadratic-client/src/app/gridGL/cells/borders/Borders.ts b/quadratic-client/src/app/gridGL/cells/borders/Borders.ts deleted file mode 100644 index 63bd8592be..0000000000 --- a/quadratic-client/src/app/gridGL/cells/borders/Borders.ts +++ /dev/null @@ -1,411 +0,0 @@ -//! Draws both cell-based borders and sheet-based borders. The cell-based -//! borders are saved and culled during the update loop. The sheet-based borders -//! are always redrawn whenever the viewport changes. -//! -//! The logic in this is rather complicated as we need to remove overlapping -//! borders based on timestamps. For example, if you have a column with a left -//! border, before drawing that left border, you need to check if there are any -//! existing, visible cell-based, either left in that column, or right in the -//! previous column. You also need to check if there are any row-based borders -//! that have a left or right that would overlap the vertical line (and have a -//! later timestamp). -//! -//! Regrettably, you can't just draw over the border as they may be different -//! widths and this would remove any transparent background. - -import { events } from '@/app/events/events'; -import { sheets } from '@/app/grid/controller/Sheets'; -import { BorderStyleCell, BorderStyleTimestamp, JsBordersSheet } from '@/app/quadratic-core-types'; -import { Container, Rectangle, Sprite, Texture, TilingSprite } from 'pixi.js'; -import { Sheet } from '../../../grid/sheet/Sheet'; -import { intersects } from '../../helpers/intersects'; -import { pixiApp } from '../../pixiApp/PixiApp'; -import { CellsSheet } from '../CellsSheet'; -import { BorderCull, drawCellBorder } from '../drawBorders'; -import { divideLine, findPerpendicularHorizontalLines, findPerpendicularVerticalLines } from './bordersUtil'; - -// this sets when to fade the sheet borders when (for performance reasons) -const SCALE_TO_SHOW_SHEET_BORDERS = 0.15; -const FADE_SCALE = 0.1; - -export class Borders extends Container { - private cellsSheet: CellsSheet; - private cellLines: Container; - private spriteLines: BorderCull[]; - - private sheetLines: Container; - private borders?: JsBordersSheet; - - dirty = false; - - constructor(cellsSheet: CellsSheet) { - super(); - this.cellsSheet = cellsSheet; - this.sheetLines = this.addChild(new Container()); - this.cellLines = this.addChild(new Container()); - this.spriteLines = []; - - events.on('bordersSheet', this.drawSheetCells); - events.on('sheetOffsets', this.setDirty); - } - - destroy() { - events.off('bordersSheet', this.drawSheetCells); - events.off('sheetOffsets', this.setDirty); - super.destroy(); - } - - setDirty = () => (this.dirty = true); - - private get sheet(): Sheet { - const sheet = sheets.getById(this.cellsSheet.sheetId); - if (!sheet) throw new Error(`Expected sheet to be defined in CellsBorders.sheet`); - return sheet; - } - - // Draws a vertical line on the screen. This line may need to be split up if - // there are borders already drawn. - private drawScreenVerticalLine( - rowStart: number, - rowEnd: number, - column: number, - border: BorderStyleTimestamp, - rows: Record | null - ) { - // break up the line if it overlaps with vertical lines - const lines: [number, number][] = []; - if (this.borders?.vertical || rows) { - // filter the line and only include the ones that are visible - const overlaps = this.borders?.vertical - ? this.borders.vertical.filter( - (vertical) => - Number(vertical.x) === column && - intersects.lineLineOneDimension( - Number(vertical.y), - Number(vertical.y + vertical.height), - rowStart, - rowEnd - ) - ) - : []; - overlaps.push(...findPerpendicularVerticalLines(rowStart, rowEnd, rows)); - overlaps.sort((a, b) => Number(a.y) - Number(b.y)); - let current: number | undefined; - while (overlaps.length) { - const overlap = overlaps.pop(); - if (overlap) { - current = divideLine(lines, current, rowStart, rowEnd, Number(overlap.y), Number(overlap.height)); - } - } - if (current === undefined) { - lines.push([rowStart, rowEnd]); - } else if (current < rowEnd) { - lines.push([current, rowEnd]); - } - } else { - lines.push([rowStart, rowEnd]); - } - const x = this.sheet.getColumnX(column); - lines.forEach(([start, end]) => { - const yStart = this.sheet.getRowY(start); - const yEnd = this.sheet.getRowY(end); - drawCellBorder({ - position: new Rectangle(x, yStart, 0, yEnd - yStart), - vertical: { type: border.line, color: border.color }, - getSprite: this.getSpriteSheet, - }); - }); - } - - // Draws a horizontal line on the screen. This line may need to be split up if - // there are borders already drawn. - private drawScreenHorizontalLine( - columnStart: number, - columnEnd: number, - row: number, - border: BorderStyleTimestamp, - columns: Record | null - ) { - // break up the line if it overlaps with horizontal lines - const lines: [number, number][] = []; - if (this.borders?.horizontal || columns) { - // filter the line and only include the ones that are visible - const overlaps = this.borders?.horizontal - ? this.borders.horizontal.filter( - (horizontal) => - Number(horizontal.y) === row && - intersects.lineLineOneDimension( - Number(horizontal.x), - Number(horizontal.x + horizontal.width), - columnStart, - columnEnd - ) - ) - : []; - overlaps.push(...findPerpendicularHorizontalLines(columnStart, columnEnd, columns)); - overlaps.sort((a, b) => Number(a.x) - Number(b.x)); - let current: number | undefined; - while (overlaps.length) { - const overlap = overlaps.pop(); - if (overlap) { - current = divideLine(lines, current, columnStart, columnEnd, Number(overlap.x), Number(overlap.width)); - } - } - if (current === undefined) { - lines.push([columnStart, columnEnd]); - } else if (current < columnEnd) { - lines.push([current, columnEnd]); - } - } else { - lines.push([columnStart, columnEnd]); - } - const y = this.sheet.getRowY(row); - lines.forEach(([start, end]) => { - const xStart = this.sheet.getColumnX(start); - const xEnd = this.sheet.getColumnX(end); - drawCellBorder({ - position: new Rectangle(xStart, y, xEnd - xStart, 0), - horizontal: { type: border.line, color: border.color }, - getSprite: this.getSpriteSheet, - }); - }); - } - - // Takes the borders.all, .columns, and .rows, and then draws any borders - // within the visible bounds. - private drawSheetBorders() { - this.cellLines.removeChildren(); - this.sheetLines.removeChildren(); - - pixiApp.viewport.dirty = true; - - const borders = this.borders; - if (!borders) return; - - this.drawAll(); - this.drawHorizontal(); - this.drawVertical(); - - const bounds = pixiApp.viewport.getVisibleBounds(); - const offsets = sheets.sheet.offsets; - - const columnStart = offsets.getXPlacement(bounds.left); - const columnEnd = offsets.getXPlacement(bounds.right); - - const rowStart = offsets.getYPlacement(bounds.top); - const rowEnd = offsets.getYPlacement(bounds.bottom); - - if (borders.columns) { - for (let x in borders.columns) { - const xNumber = Number(BigInt(x)); - if (xNumber >= columnStart.index && xNumber <= columnEnd.index) { - const column = borders.columns[x]; - if (column) { - const left = column.left; - if (left && left.line !== 'clear') { - // need to ensure there's no right entry in x - 1 - const right = borders.columns[(xNumber - 1).toString()]?.right; - if (!right || left.timestamp > right.timestamp) { - this.drawScreenVerticalLine(rowStart.index, rowEnd.index + 1, xNumber, left, borders.rows); - } - } - const right = column.right; - if (right && right.line !== 'clear') { - // need to ensure there's no left entry in x + 1 - const left = borders.columns[(xNumber + 1).toString()]?.left; - if (!left || right.timestamp > left.timestamp) { - this.drawScreenVerticalLine(rowStart.index, rowEnd.index + 1, xNumber + 1, right, borders.rows); - } - } - const top = column.top; - if (top && top.line !== 'clear') { - for (let y = rowStart.index; y <= rowEnd.index + 1; y++) { - this.drawScreenHorizontalLine(xNumber, xNumber + 1, y, top, null); - } - } - const bottom = column.bottom; - if (bottom && bottom.line !== 'clear') { - for (let y = rowStart.index; y <= rowEnd.index + 1; y++) { - this.drawScreenHorizontalLine(xNumber, xNumber + 1, y, bottom, null); - } - } - } - } - } - } - - if (borders.rows) { - for (let y in borders.rows) { - const yNumber = Number(BigInt(y)); - if (yNumber >= rowStart.index && yNumber <= rowEnd.index) { - const row = borders.rows[y]; - if (row) { - const top = row.top; - if (top && top.line !== 'clear') { - // need to ensure there's no bottom entry in y - 1 - const bottom = borders.rows[(yNumber - 1).toString()]?.bottom; - if (!bottom || top.timestamp > bottom.timestamp) { - this.drawScreenHorizontalLine(columnStart.index, columnEnd.index + 1, yNumber, top, borders.columns); - } - } - const bottom = row.bottom; - if (bottom && bottom.line !== 'clear') { - // need to ensure there's no top entry in y + 1 - const top = borders.rows[(yNumber + 1).toString()]?.top; - if (!top || bottom.timestamp > top.timestamp) { - this.drawScreenHorizontalLine( - columnStart.index, - columnEnd.index + 1, - yNumber + 1, - bottom, - borders.columns - ); - } - } - const left = row.left; - if (left && left.line !== 'clear') { - for (let x = columnStart.index; x <= columnEnd.index + 1; x++) { - this.drawScreenVerticalLine(yNumber, yNumber + 1, x, left, null); - } - } - const right = row.right; - if (right && right.line !== 'clear') { - for (let x = columnStart.index; x <= columnEnd.index + 1; x++) { - this.drawScreenVerticalLine(yNumber, yNumber + 1, x, right, null); - } - } - } - } - } - } - } - - private drawHorizontal() { - if (!this.borders?.horizontal) return; - for (const border of this.borders.horizontal) { - if (border.line === 'clear') continue; - const start = this.sheet.getCellOffsets(Number(border.x), Number(border.y)); - const end = this.sheet.getCellOffsets(Number(border.x + border.width), Number(border.y)); - const color = border.color; - this.spriteLines.push( - ...drawCellBorder({ - position: new Rectangle(start.x, start.y, end.x - start.x, end.y - start.y), - horizontal: { type: border.line, color }, - getSprite: this.getSprite, - }) - ); - } - } - - private drawVertical() { - if (!this.borders?.vertical) return; - for (const border of this.borders.vertical) { - if (border.line === 'clear') continue; - const start = this.sheet.getCellOffsets(Number(border.x), Number(border.y)); - const end = this.sheet.getCellOffsets(Number(border.x), Number(border.y + border.height)); - const color = border.color; - this.spriteLines.push( - ...drawCellBorder({ - position: new Rectangle(start.x, start.y, end.x - start.x, end.y - start.y), - vertical: { type: border.line, color }, - getSprite: this.getSprite, - }) - ); - } - } - - private drawAll() { - if (!this.borders) return; - const all = this.borders?.all; - if (!all) return; - const bounds = pixiApp.viewport.getVisibleBounds(); - const offsets = sheets.sheet.offsets; - const columnStart = offsets.getXPlacement(bounds.left); - const columnEnd = offsets.getXPlacement(bounds.right); - const rowStart = offsets.getYPlacement(bounds.top); - const rowEnd = offsets.getYPlacement(bounds.bottom); - - // draw horizontal lines - let horizontal: BorderStyleTimestamp | undefined; - if (all.top && all.bottom) { - horizontal = all.top.timestamp > all.bottom.timestamp ? all.top : all.bottom; - } else if (all.top) { - horizontal = all.top; - } else if (all.bottom) { - horizontal = all.bottom; - } - if (horizontal) { - for (let y = rowStart.index; y <= rowEnd.index; y++) { - // todo: need to check if there's a column-based border that would overwrite this - this.drawScreenHorizontalLine(columnStart.index, columnEnd.index + 1, y, horizontal, this.borders.rows); - } - } - - // draw vertical lines - let vertical: BorderStyleTimestamp | undefined; - if (all.left && all.right) { - vertical = all.left.timestamp > all.right.timestamp ? all.left : all.right; - } else if (all.left) { - vertical = all.left; - } else if (all.right) { - vertical = all.right; - } - if (vertical) { - for (let x = columnStart.index; x <= columnEnd.index; x++) { - // todo: need to check if there's a row-based border that would overwrite this - this.drawScreenVerticalLine(rowStart.index, rowEnd.index + 1, x, vertical, this.borders.columns); - } - } - } - - drawSheetCells = (sheetId: string, borders?: JsBordersSheet): void => { - if (sheetId === this.cellsSheet.sheetId) { - this.borders = borders; - this.cellLines.removeChildren(); - this.drawHorizontal(); - this.drawVertical(); - this.dirty = true; - } - }; - - private cull() { - const bounds = pixiApp.viewport.getVisibleBounds(); - this.spriteLines.forEach((sprite) => { - sprite.sprite.visible = sprite.rectangle.intersects(bounds); - }); - } - - update() { - const viewportDirty = pixiApp.viewport.dirty; - if (!this.dirty && !viewportDirty) return; - if (pixiApp.viewport.scale.x < SCALE_TO_SHOW_SHEET_BORDERS) { - this.sheetLines.visible = false; - } else { - this.sheetLines.visible = true; - this.drawSheetBorders(); - if (pixiApp.viewport.scale.x < SCALE_TO_SHOW_SHEET_BORDERS + FADE_SCALE) { - this.sheetLines.alpha = (pixiApp.viewport.scale.x - SCALE_TO_SHOW_SHEET_BORDERS) / FADE_SCALE; - } else { - this.sheetLines.alpha = 1; - } - } - this.dirty = false; - this.cull(); - } - - private getSpriteSheet = (tiling?: boolean): Sprite | TilingSprite => { - if (tiling) { - return this.sheetLines.addChild(new TilingSprite(Texture.WHITE)); - } else { - return this.sheetLines.addChild(new Sprite(Texture.WHITE)); - } - }; - - private getSprite = (tiling?: boolean): Sprite | TilingSprite => { - if (tiling) { - return this.cellLines.addChild(new TilingSprite(Texture.WHITE)); - } else { - return this.cellLines.addChild(new Sprite(Texture.WHITE)); - } - }; -} diff --git a/quadratic-client/src/app/gridGL/cells/borders/bordersUtil.test.ts b/quadratic-client/src/app/gridGL/cells/borders/bordersUtil.test.ts deleted file mode 100644 index 9a7ef20e55..0000000000 --- a/quadratic-client/src/app/gridGL/cells/borders/bordersUtil.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { divideLine, findPerpendicularHorizontalLines, findPerpendicularVerticalLines } from './bordersUtil'; -import { BorderStyleCell } from '@/app/quadratic-core-types'; - -describe('should divide line', () => { - it('breaks in the middle', () => { - const line = [1, 30]; - const breaks = [ - [2, 3], - [10, 5], - ]; - const lines: [number, number][] = []; - let current = undefined; - - for (const b of breaks) { - current = divideLine(lines, current, line[0], line[1], b[0], b[1]); - } - if (current && current <= line[1]) { - lines.push([current, line[1] + 1]); - } - - expect(lines).toEqual([ - [1, 2], - [5, 10], - [15, 31], - ]); - }); - - it('breaks at the start', () => { - const line = [1, 30]; - const breaks = [ - [1, 3], - [10, 5], - ]; - const lines: [number, number][] = []; - let current = undefined; - - for (const b of breaks) { - current = divideLine(lines, current, line[0], line[1], b[0], b[1]); - } - if (current && current <= line[1]) { - lines.push([current, line[1] + 1]); - } - - expect(lines).toEqual([ - [4, 10], - [15, 31], - ]); - }); - - it('breaks at the end', () => { - const line = [1, 30]; - const breaks = [ - [20, 3], - [25, 40], - ]; - const lines: [number, number][] = []; - let current = undefined; - - for (const b of breaks) { - current = divideLine(lines, current, line[0], line[1], b[0], b[1]); - } - if (current && current <= line[1]) { - lines.push([current, line[1] + 1]); - } - - expect(lines).toEqual([ - [1, 20], - [23, 25], - ]); - }); -}); - -describe('should find perpendicular lines', () => { - const color = { red: 0, green: 0, blue: 0, alpha: 0 }; - - it('finds horizontal lines at start', () => { - const entries: Record = { - '1': { - top: { timestamp: 1, color, line: 'line1' }, - bottom: null, - left: null, - right: null, - }, - }; - const lines = findPerpendicularHorizontalLines(1, 3, entries); - expect(lines.length).toEqual(1); - const line1 = lines[0]; - expect(line1.x).toEqual(1n); - expect(line1.y).toEqual(0n); - expect(line1.width).toEqual(1n); - }); - - it('finds horizontal lines in the start (but bottom)', () => { - const entries: Record = { - '0': { - top: null, - bottom: { timestamp: 1, color, line: 'line1' }, - left: null, - right: null, - }, - }; - const lines = findPerpendicularHorizontalLines(1, 3, entries); - expect(lines.length).toEqual(1); - const line1 = lines[0]; - expect(line1.x).toEqual(0n); - expect(line1.y).toEqual(0n); - expect(line1.width).toEqual(1n); - }); - - it('finds horizontal lines in the middle', () => { - const entries: Record = { - '2': { - top: { timestamp: 1, color, line: 'line1' }, - bottom: null, - left: null, - right: null, - }, - }; - const lines = findPerpendicularHorizontalLines(1, 3, entries); - expect(lines.length).toEqual(1); - const line1 = lines[0]; - expect(line1.x).toEqual(2n); - expect(line1.y).toEqual(0n); - expect(line1.width).toEqual(1n); - }); - - it('finds horizontal lines in the middle (but bottom)', () => { - const entries: Record = { - '1': { - top: null, - bottom: { timestamp: 1, color, line: 'line1' }, - left: null, - right: null, - }, - }; - const lines = findPerpendicularHorizontalLines(1, 3, entries); - expect(lines.length).toEqual(1); - const line1 = lines[0]; - expect(line1.x).toEqual(1n); - expect(line1.y).toEqual(0n); - expect(line1.width).toEqual(1n); - }); - - it('finds horizontal lines in the end', () => { - const entries: Record = { - '2': { - top: { timestamp: 1, color, line: 'line1' }, - bottom: null, - left: null, - right: null, - }, - }; - const lines = findPerpendicularHorizontalLines(1, 3, entries); - expect(lines.length).toEqual(1); - const line1 = lines[0]; - expect(line1.x).toEqual(2n); - expect(line1.y).toEqual(0n); - expect(line1.width).toEqual(1n); - }); - - it('finds horizontal lines in the end (but bottom)', () => { - const entries: Record = { - '1': { - top: null, - bottom: { timestamp: 1, color, line: 'line1' }, - left: null, - right: null, - }, - }; - const lines = findPerpendicularHorizontalLines(1, 3, entries); - expect(lines.length).toEqual(1); - const line1 = lines[0]; - expect(line1.x).toEqual(1n); - expect(line1.y).toEqual(0n); - expect(line1.width).toEqual(1n); - }); -}); - -describe('should find perpendicular vertical lines', () => { - const color = { red: 0, green: 0, blue: 0, alpha: 0 }; - - it('finds vertical lines in the middle', () => { - const entries: Record = { - '2': { - top: null, - bottom: null, - left: { timestamp: 1, color, line: 'line1' }, - right: null, - }, - }; - const lines = findPerpendicularVerticalLines(1, 3, entries); - expect(lines.length).toEqual(1); - const line1 = lines[0]; - expect(line1.x).toEqual(0n); - expect(line1.y).toEqual(2n); - expect(line1.height).toEqual(1n); - }); - - it('finds vertical lines at the start', () => { - const entries: Record = { - '1': { - top: null, - bottom: null, - left: { timestamp: 1, color, line: 'line1' }, - right: null, - }, - }; - const lines = findPerpendicularVerticalLines(1, 3, entries); - expect(lines.length).toEqual(1); - const line1 = lines[0]; - expect(line1.x).toEqual(0n); - expect(line1.y).toEqual(1n); - expect(line1.height).toEqual(1n); - }); - - it('finds vertical lines at the end', () => { - const entries: Record = { - '3': { - top: null, - bottom: null, - left: { timestamp: 1, color, line: 'line1' }, - right: null, - }, - }; - const lines = findPerpendicularVerticalLines(1, 3, entries); - expect(lines.length).toEqual(1); - const line1 = lines[0]; - expect(line1.x).toEqual(0n); - expect(line1.y).toEqual(3n); - expect(line1.height).toEqual(1n); - }); - - it('finds vertical lines using right property', () => { - const entries: Record = { - '1': { - top: null, - bottom: null, - left: null, - right: { timestamp: 1, color, line: 'line1' }, - }, - }; - const lines = findPerpendicularVerticalLines(1, 3, entries); - expect(lines.length).toEqual(1); - const line1 = lines[0]; - expect(line1.x).toEqual(0n); - expect(line1.y).toEqual(1n); - expect(line1.height).toEqual(1n); - }); -}); diff --git a/quadratic-client/src/app/gridGL/cells/borders/bordersUtil.ts b/quadratic-client/src/app/gridGL/cells/borders/bordersUtil.ts deleted file mode 100644 index 43ee75e512..0000000000 --- a/quadratic-client/src/app/gridGL/cells/borders/bordersUtil.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { BorderStyleCell, JsBorderHorizontal, JsBorderVertical } from '@/app/quadratic-core-types'; - -// Breaks up a sheet-based line removing the parts that overlap. See tests for -// examples. -export function divideLine( - lines: [number, number][], - current: number | undefined, - start: number, - end: number, - overlap: number, - overlapSize: number -): number | undefined { - // handle the case where the current line is already covered by a previous - // line (this can happen b/c of perpendicular lines) - if (current && overlap < current) { - return current; - } - // If the overlaps is at the current or starting position of the line, then - // nothing needs to be done. Just move current to the next position. - if (overlap === current || overlap === start) { - return overlap + overlapSize; - } - - // If the overlap goes beyond the end of the line, then nothing needs to be - // added to lines. - if (overlap + overlapSize === end) { - return undefined; - } - - // Otherwise we add the line to lines and return the next position. - lines.push([current ?? start, overlap]); - return overlap + overlapSize; -} - -// Checks whether perpendicular lines intersect with a sheet-based line. -export function findPerpendicularHorizontalLines( - start: number, - end: number, - entries: Record | null -): JsBorderHorizontal[] { - const lines: JsBorderHorizontal[] = []; - if (entries === null) return lines; - - for (let i = start; i <= end; i++) { - // finds perpendicular intersecting lines using top/left - const current = entries[i.toString()]; - if (current) { - if (current.top) { - lines.push({ color: current.top.color, line: current.top.line, x: BigInt(i), y: 0n, width: 1n }); - } - } else { - // finds perpendicular intersecting lines using the previous bottom/right - const next = entries[(i - 1).toString()]; - if (next) { - if (next.bottom) { - lines.push({ color: next.bottom.color, line: next.bottom.line, x: BigInt(i - 1), y: 0n, width: 1n }); - } - } - } - } - return lines; -} - -// Checks whether perpendicular lines intersect with a sheet-based line. -export function findPerpendicularVerticalLines( - start: number, - end: number, - entries: Record | null -): JsBorderVertical[] { - const lines: JsBorderVertical[] = []; - if (entries === null) return lines; - - for (let i = start; i <= end; i++) { - // finds perpendicular intersecting lines using left/top - const current = entries[i.toString()]; - if (current) { - if (current.left) { - lines.push({ color: current.left.color, line: current.left.line, x: 0n, y: BigInt(i), height: 1n }); - } - } else { - // finds perpendicular intersecting lines using the previous right/bottom - const next = entries[(i - 1).toString()]; - if (next) { - if (next.right) { - lines.push({ color: next.right.color, line: next.right.line, x: 0n, y: BigInt(i - 1), height: 1n }); - } - } - } - } - return lines; -} diff --git a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts index e4012c5482..261c82b637 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsImages/CellsImages.ts @@ -1,6 +1,7 @@ //! Draw the cell images (an Image output from a code cell) import { events } from '@/app/events/events'; +<<<<<<< HEAD import { sheets } from '@/app/grid/controller/Sheets'; import { Coordinate } from '@/app/gridGL/types/size'; import { CoreClientImage } from '@/app/web-workers/quadraticCore/coreClientMessages'; @@ -9,6 +10,14 @@ import { intersects } from '../../helpers/intersects'; import { pixiApp } from '../../pixiApp/PixiApp'; import { CellsSheet } from '../CellsSheet'; import { CellsImage } from './CellsImage'; +======= +import { CellsImage } from '@/app/gridGL/cells/cellsImages/CellsImage'; +import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { CoreClientImage } from '@/app/web-workers/quadraticCore/coreClientMessages'; +import { Container, Rectangle } from 'pixi.js'; +>>>>>>> origin/qa export class CellsImages extends Container { private cellsSheet: CellsSheet; @@ -21,9 +30,9 @@ export class CellsImages extends Container { } destroy() { - super.destroy(); events.off('updateImage', this.updateImage); events.off('sheetOffsets', this.reposition); + super.destroy(); } reposition = (sheetId: string) => { diff --git a/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsLabels.ts b/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsLabels.ts index 4ef5e17eac..fc46359880 100644 --- a/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsLabels.ts +++ b/quadratic-client/src/app/gridGL/cells/cellsLabel/CellsLabels.ts @@ -9,6 +9,9 @@ import { debugShowCellsHashBoxes, debugShowCellsSheetCulling } from '@/app/debugFlags'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; +import { CellsTextHash } from '@/app/gridGL/cells/cellsLabel/CellsTextHash'; +import { CellsSheet, ErrorMarker, ErrorValidation } from '@/app/gridGL/cells/CellsSheet'; +import { sheetHashHeight, sheetHashWidth } from '@/app/gridGL/cells/CellsTypes'; import { intersects } from '@/app/gridGL/helpers/intersects'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { Link } from '@/app/gridGL/types/links'; @@ -20,9 +23,6 @@ import { import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; import type { RenderSpecial } from '@/app/web-workers/renderWebWorker/worker/cellsLabel/CellsTextHashSpecial'; import { Container, Graphics, Point, Rectangle } from 'pixi.js'; -import { CellsSheet, ErrorMarker, ErrorValidation } from '../CellsSheet'; -import { sheetHashHeight, sheetHashWidth } from '../CellsTypes'; -import { CellsTextHash } from './CellsTextHash'; export class CellsLabels extends Container { private cellsSheet: CellsSheet; @@ -48,6 +48,11 @@ export class CellsLabels extends Container { events.on('clickedToCell', this.clickedToCell); } + destroy() { + events.off('clickedToCell', this.clickedToCell); + super.destroy(); + } + get sheetId(): string { return this.cellsSheet.sheetId; } @@ -84,7 +89,7 @@ export class CellsLabels extends Container { } // Returns whether the cell has content by checking CellsTextHashContent. - hasCell(column: number, row: number): boolean { + hasCell = (column: number, row: number): boolean => { const hashX = Math.floor(column / sheetHashWidth); const hashY = Math.floor(row / sheetHashHeight); const key = `${hashX},${hashY}`; @@ -93,7 +98,19 @@ export class CellsLabels extends Container { return cellsTextHash.content.hasContent(column, row); } return false; - } + }; + + // Returns whether the rect has content by checking CellsTextHashContent. + hasCellInRect = (rect: Rectangle): boolean => { + for (let column = rect.x; column <= rect.x + rect.width; column++) { + for (let row = rect.y; row <= rect.y + rect.height; row++) { + if (this.hasCell(column, row)) { + return true; + } + } + } + return false; + }; // received a new LabelMeshEntry to add to a CellsTextHash addLabelMeshEntry(message: RenderClientLabelMeshEntry) { diff --git a/quadratic-client/src/app/gridGL/helpers/selectCells.ts b/quadratic-client/src/app/gridGL/helpers/selectCells.ts deleted file mode 100644 index adeac4e3cb..0000000000 --- a/quadratic-client/src/app/gridGL/helpers/selectCells.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - getVisibleLeftColumn, - getVisibleTopRow, - isColumnVisible, - isRowVisible, -} from '@/app/gridGL/interaction/viewportHelper'; -import { Rectangle } from 'pixi.js'; -import { sheets } from '../../grid/controller/Sheets'; - -export function selectAllCells() { - const sheet = sheets.sheet; - const cursor = sheet.cursor; - - // if we've already selected all, then select the content within the sheet - if (cursor.columnRow?.all) { - const bounds = sheet.getBounds(true); - if (bounds) { - cursor.changePosition({ - columnRow: null, - multiCursor: [new Rectangle(bounds.left, bounds.top, bounds.width + 1, bounds.height + 1)], - cursorPosition: { x: bounds.left, y: bounds.top }, - }); - } else { - cursor.changePosition({ - columnRow: null, - multiCursor: null, - cursorPosition: { x: 0, y: 0 }, - }); - } - } else { - cursor.changePosition({ columnRow: { all: true }, multiCursor: null }); - } -} - -/** - * Selects columns. Cursor position is set to the last column selected or the - * passed column. - * @param column if column is set, then that column is used as the cursor - * position, otherwise it uses the last entry in columns - */ -export async function selectColumns(columns: number[], column = columns[columns.length - 1], keepExisting = false) { - // remove duplicates - columns = columns.filter((item, pos) => columns.indexOf(item) === pos); - - const sheet = sheets.sheet; - const cursor = sheet.cursor; - - const multiCursor = keepExisting ? cursor?.multiCursor : null; - const rows = keepExisting ? cursor.columnRow?.rows : undefined; - - if (columns.length === 0) { - cursor.changePosition({ columnRow: rows ? { rows } : null, multiCursor }); - return; - } - - // Find a row to select based on viewport. 1. if 0 is visible, use that; 2. if - // not use, the first row from the top of the viewport. - let row: number; - if (isRowVisible(0)) { - row = 0; - } else { - row = getVisibleTopRow(); - } - cursor.changePosition({ columnRow: { columns, rows }, cursorPosition: { x: column, y: row }, multiCursor }); -} - -export async function selectRows(rows: number[], row = rows[rows.length - 1], keepExisting = false) { - // remove duplicates - rows = rows.filter((item, pos) => rows.indexOf(item) === pos); - - const sheet = sheets.sheet; - const cursor = sheet.cursor; - - const multiCursor = keepExisting ? cursor.multiCursor : null; - const columns = keepExisting ? cursor.columnRow?.columns : undefined; - - if (rows.length === 0) { - cursor.changePosition({ columnRow: columns ? { columns } : null, multiCursor }); - return; - } - - // Find a column to select based on viewport. 1. if 0 is visible, use that; 2. if - // not use, the first column from the left of the viewport. - let column: number; - if (isColumnVisible(0)) { - column = 0; - } else { - column = getVisibleLeftColumn(); - } - - cursor.changePosition({ columnRow: { rows, columns }, cursorPosition: { x: column, y: row }, multiCursor }); -} diff --git a/quadratic-client/src/app/gridGL/helpers/selectCells.ts.deprecated b/quadratic-client/src/app/gridGL/helpers/selectCells.ts.deprecated new file mode 100644 index 0000000000..93530101af --- /dev/null +++ b/quadratic-client/src/app/gridGL/helpers/selectCells.ts.deprecated @@ -0,0 +1,92 @@ +// import { +// getVisibleLeftColumn, +// getVisibleTopRow, +// isColumnVisible, +// isRowVisible, +// } from '@/app/gridGL/interaction/viewportHelper'; +// import { Rectangle } from 'pixi.js'; +// import { sheets } from '../../grid/controller/Sheets'; + +// export function selectAllCells() { +// const sheet = sheets.sheet; +// const cursor = sheet.cursor; + +// // if we've already selected all, then select the content within the sheet +// if (cursor.columnRow?.all) { +// const bounds = sheet.getBounds(true); +// if (bounds) { +// cursor.changePosition({ +// columnRow: null, +// multiCursor: [new Rectangle(bounds.left, bounds.top, bounds.width + 1, bounds.height + 1)], +// cursorPosition: { x: bounds.left, y: bounds.top }, +// }); +// } else { +// cursor.changePosition({ +// columnRow: null, +// multiCursor: null, +// cursorPosition: { x: 0, y: 0 }, +// }); +// } +// } else { +// cursor.changePosition({ columnRow: { all: true }, multiCursor: null }); +// } +// } + +// /** +// * Selects columns. Cursor position is set to the last column selected or the +// * passed column. +// * @param column if column is set, then that column is used as the cursor +// * position, otherwise it uses the last entry in columns +// */ +// export async function selectColumns(columns: number[], column = columns[columns.length - 1], keepExisting = false) { +// // remove duplicates +// columns = columns.filter((item, pos) => columns.indexOf(item) === pos); + +// const sheet = sheets.sheet; +// const cursor = sheet.cursor; + +// const multiCursor = keepExisting ? cursor?.multiCursor : null; +// const rows = keepExisting ? cursor.columnRow?.rows : undefined; + +// if (columns.length === 0) { +// cursor.changePosition({ columnRow: rows ? { rows } : null, multiCursor }); +// return; +// } + +// // Find a row to select based on viewport. 1. if 0 is visible, use that; 2. if +// // not use, the first row from the top of the viewport. +// let row: number; +// if (isRowVisible(0)) { +// row = 0; +// } else { +// row = getVisibleTopRow(); +// } +// cursor.changePosition({ columnRow: { columns, rows }, cursorPosition: { x: column, y: row }, multiCursor }); +// } + +// export async function selectRows(rows: number[], row = rows[rows.length - 1], keepExisting = false) { +// // remove duplicates +// rows = rows.filter((item, pos) => rows.indexOf(item) === pos); + +// const sheet = sheets.sheet; +// const cursor = sheet.cursor; + +// const multiCursor = keepExisting ? cursor.multiCursor : null; +// const columns = keepExisting ? cursor.columnRow?.columns : undefined; + +// if (rows.length === 0) { +// cursor.changePosition({ columnRow: columns ? { columns } : null, multiCursor }); +// return; +// } + +// // Find a column to select based on viewport. 1. if 0 is visible, use that; 2. if +// // not use, the first column from the left of the viewport. +// let column: number; +// if (isColumnVisible(0)) { +// column = 0; +// } else { +// column = getVisibleLeftColumn(); +// } + +// cursor.changePosition({ columnRow: { rows, columns }, cursorPosition: { x: column, y: row }, multiCursor }); +// } diff --git a/quadratic-client/src/app/gridGL/helpers/selection.ts b/quadratic-client/src/app/gridGL/helpers/selection.ts new file mode 100644 index 0000000000..6528d9a72d --- /dev/null +++ b/quadratic-client/src/app/gridGL/helpers/selection.ts @@ -0,0 +1,48 @@ +import { sheets } from '@/app/grid/controller/Sheets'; +import { CellRefRange } from '@/app/quadratic-core-types'; +import { Rectangle } from 'pixi.js'; + +// returns rectangle representing the range in col/row coordinates +export function getRangeRectangleFromCellRefRange({ range }: CellRefRange): Rectangle { + const { col, row } = range.start; + let startCol = Number(col.coord); + if (startCol === -1) startCol = 1; + let startRow = Number(row.coord); + if (startRow === -1) startRow = 1; + + const end = range.end; + let endCol = Number(end.col.coord); + if (endCol === -1) endCol = Infinity; + let endRow = Number(end.row.coord); + if (endRow === -1) endRow = Infinity; + + // normalize the coordinates + const minCol = Math.min(startCol, endCol); + const minRow = Math.min(startRow, endRow); + const maxCol = Math.max(startCol, endCol); + const maxRow = Math.max(startRow, endRow); + + return new Rectangle(minCol, minRow, maxCol - minCol + 1, maxRow - minRow + 1); +} + +// returns rectangle representing the range in screen coordinates +export function getRangeScreenRectangleFromCellRefRange(range: CellRefRange): Rectangle { + const colRowRect = getRangeRectangleFromCellRefRange(range); + + const sheet = sheets.sheet; + const { left, top } = sheet.getCellOffsets(colRowRect.left, colRowRect.top); + let right = Infinity; + let bottom = Infinity; + + if (colRowRect.right !== Infinity) { + const { position } = sheet.offsets.getColumnPlacement(colRowRect.right); + right = position; + } + if (colRowRect.bottom !== Infinity) { + const { position } = sheet.offsets.getRowPlacement(colRowRect.bottom); + bottom = position; + } + + const rangeRect = new Rectangle(left, top, right - left, bottom - top); + return rangeRect; +} diff --git a/quadratic-client/src/app/gridGL/helpers/zoom.ts b/quadratic-client/src/app/gridGL/helpers/zoom.ts index 4b0f82266e..85f4abc11f 100644 --- a/quadratic-client/src/app/gridGL/helpers/zoom.ts +++ b/quadratic-client/src/app/gridGL/helpers/zoom.ts @@ -1,51 +1,74 @@ -import { ZOOM_ANIMATION_TIME_MS, ZOOM_BUFFER } from '@/shared/constants/gridConstants'; -import { Point, Rectangle } from 'pixi.js'; -import { sheets } from '../../grid/controller/Sheets'; -import { pixiApp } from '../pixiApp/PixiApp'; -import { intersects } from './intersects'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { ZOOM_ANIMATION_TIME_MS } from '@/shared/constants/gridConstants'; +import { Point } from 'pixi.js'; -export async function zoomToFit() { +export function zoomReset() { + pixiApp.viewport.reset(); +} + +function clampZoom(center: Point, scale: number) { const viewport = pixiApp.viewport; - const sheet = sheets.sheet; - const gridBounds = sheet.getBounds(false); + const headingSize = pixiApp.headings.headingSize; + const oldScale = viewport.scale.x; + const { width, height } = viewport.getVisibleBounds(); + + // clamp left + const left = center.x - width / 2 / (scale / oldScale); + if (left < -headingSize.width / scale) { + const delta = -left - headingSize.width / scale; + center.x += delta; + } + + // clamp top + const top = center.y - height / 2 / (scale / oldScale); + if (top < -headingSize.height / scale) { + const delta = -top - headingSize.height / scale; + center.y += delta; + } + + viewport.animate({ + time: ZOOM_ANIMATION_TIME_MS, + position: center, + scale, + }); +} + +export function zoomToFit() { + const gridBounds = sheets.sheet.getBounds(false); if (gridBounds) { - const screenRectangle = sheet.getScreenRectangle(gridBounds.x, gridBounds.y, gridBounds.width, gridBounds.height); + const screenRectangle = sheets.sheet.getScreenRectangleFromRect(gridBounds); + const { screenWidth, screenHeight } = pixiApp.viewport; + const headingSize = pixiApp.headings.headingSize; - // calc scale, and leave a little room on the top and sides - let scale = viewport.findFit(screenRectangle.width * ZOOM_BUFFER, screenRectangle.height * ZOOM_BUFFER); + const sx = (screenWidth - headingSize.width) / screenRectangle.width; + const sy = (screenHeight - headingSize.height) / screenRectangle.height; + let scale = Math.min(sx, sy); // Don't zoom in more than a factor of 2 - if (scale > 2) scale = 2; - - viewport.animate({ - time: ZOOM_ANIMATION_TIME_MS, - position: new Point( - screenRectangle.x + screenRectangle.width / 2, - screenRectangle.y + screenRectangle.height / 2 - ), - scale, - }); + scale = Math.min(scale, 2); + + const screenCenter = new Point( + screenRectangle.x + screenRectangle.width / 2 - headingSize.width / 2 / scale, + screenRectangle.y + screenRectangle.height / 2 - headingSize.height / 2 / scale + ); + + const center = new Point(screenCenter.x - headingSize.width, screenCenter.y - headingSize.height); + clampZoom(center, scale); } else { - viewport.animate({ - time: ZOOM_ANIMATION_TIME_MS, - position: new Point(0, 0), - scale: 1, - }); + clampZoom(new Point(0, 0), 1); } } export function zoomInOut(scale: number): void { - const cursorPosition = sheets.sheet.cursor.getCursor(); + const cursorPosition = sheets.sheet.cursor.position; const visibleBounds = pixiApp.viewport.getVisibleBounds(); - // If the center of the cell cursor's position is visible, then zoom to that point const cursorWorld = sheets.sheet.getCellOffsets(cursorPosition.x, cursorPosition.y); const center = new Point(cursorWorld.x + cursorWorld.width / 2, cursorWorld.y + cursorWorld.height / 2); - pixiApp.viewport.animate({ - time: ZOOM_ANIMATION_TIME_MS, - scale, - position: intersects.rectanglePoint(visibleBounds, center) ? center : undefined, - }); + const newCenter = intersects.rectanglePoint(visibleBounds, center) ? center : pixiApp.viewport.center; + clampZoom(newCenter, scale); } export function zoomIn() { @@ -61,34 +84,25 @@ export function zoomTo100() { } export function zoomToSelection(): void { - let screenRectangle: Rectangle; const sheet = sheets.sheet; - if (sheet.cursor.multiCursor) { - const rectangles = sheet.cursor.multiCursor; - let minX = Infinity, - minY = Infinity, - maxX = -Infinity, - maxY = -Infinity; - for (const rectangle of rectangles) { - minX = Math.min(minX, rectangle.left); - minY = Math.min(minY, rectangle.top); - maxX = Math.max(maxX, rectangle.right); - maxY = Math.max(maxY, rectangle.bottom); - } - screenRectangle = sheet.getScreenRectangle(minX, minY, maxX - minX, maxY - minY); - } else { - const cursor = sheet.cursor.cursorPosition; - screenRectangle = sheet.getScreenRectangle(cursor.x, cursor.y, 1, 1); - } + const rectangle = sheet.cursor.getLargestRectangle(); + const screenRectangle = sheet.getScreenRectangleFromRect(rectangle); + const headingSize = pixiApp.headings.headingSize; + // calc scale, and leave a little room on the top and sides - let scale = pixiApp.viewport.findFit(screenRectangle.width * ZOOM_BUFFER, screenRectangle.height * ZOOM_BUFFER); + let scale = pixiApp.viewport.findFit( + screenRectangle.width + headingSize.width, + screenRectangle.height + headingSize.height + ); // Don't zoom in more than a factor of 2 - if (scale > 2) scale = 2; + scale = Math.min(scale, 2); - pixiApp.viewport.animate({ - time: ZOOM_ANIMATION_TIME_MS, - position: new Point(screenRectangle.x + screenRectangle.width / 2, screenRectangle.y + screenRectangle.height / 2), - scale, - }); + const screenCenter = new Point( + screenRectangle.x + screenRectangle.width / 2, + screenRectangle.y + screenRectangle.height / 2 + ); + + const center = new Point(screenCenter.x - headingSize.width, screenCenter.y - headingSize.height); + clampZoom(center, scale); } diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCell.ts index 04447bb300..c27b01093b 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 { 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'; @@ -16,7 +17,7 @@ import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; function inCodeEditor(codeEditorState: CodeEditorState, cursor: SheetCursor): boolean { if (!codeEditorState.showCodeEditor) return false; - const cursorPosition = cursor.cursorPosition; + const cursorPosition = cursor.position; const selectedX = codeEditorState.codeCell.pos.x; const selectedY = codeEditorState.codeCell.pos.y; @@ -26,10 +27,7 @@ function inCodeEditor(codeEditorState: CodeEditorState, cursor: SheetCursor): bo } // selectedCell is inside multi-cursor - if (cursor.multiCursor?.some((cursor) => cursor.contains(selectedX, selectedY))) { - return true; - } - return false; + return cursor.contains(selectedX, selectedY); } export function keyboardCell(event: React.KeyboardEvent): boolean { @@ -40,50 +38,64 @@ export function keyboardCell(event: React.KeyboardEvent): boolean { const sheet = sheets.sheet; const cursor = sheet.cursor; - const cursorPosition = cursor.cursorPosition; + const cursorPosition = cursor.position; const hasPermission = hasPermissionToEditFile(editorInteractionState.permissions); // Move cursor right, don't clear selection if (matchShortcut(Action.MoveCursorRightWithSelection, event)) { - cursor.changePosition({ - keyboardMovePosition: { - x: cursorPosition.x + 1, - y: cursorPosition.y, - }, - cursorPosition: { - x: cursorPosition.x + 1, - y: cursorPosition.y, - }, - }); + cursor.moveTo(cursorPosition.x + 1, cursorPosition.y); return true; } // Move cursor left, don't clear selection if (matchShortcut(Action.MoveCursorLeftWithSelection, event)) { - cursor.changePosition({ - keyboardMovePosition: { - x: cursorPosition.x - 1, - y: cursorPosition.y, - }, - cursorPosition: { - x: cursorPosition.x - 1, - y: cursorPosition.y, - }, - }); + cursor.moveTo(cursorPosition.x - 1, cursorPosition.y); return true; } // 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.position; + quadraticCore.getCodeCell(sheets.sheet.id, x, y).then((code) => { + if (code) { + 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, + cursorMode: cell ? CursorMode.Edit : CursorMode.Enter, + }); + }); + } + }); + return true; + } + } + + // Edit cell - navigate text + if (matchShortcut(Action.ToggleArrowMode, event)) { + if (!inlineEditorHandler.isEditingFormula()) { + const { x, y } = sheets.sheet.cursor.position; + 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: '', + cursorMode: CursorMode.Edit, + }); } 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, cursorMode: CursorMode.Edit }); }); } }); @@ -126,11 +138,12 @@ export function keyboardCell(event: React.KeyboardEvent): boolean { // Triggers Validation UI if (matchShortcut(Action.TriggerCell, event)) { - const p = sheets.sheet.cursor.cursorPosition; + const p = sheets.sheet.cursor.position; events.emit('triggerCell', p.x, p.y, true); } if (isAllowedFirstChar(event.key)) { +<<<<<<< HEAD const cursorPosition = cursor.cursorPosition; quadraticCore.getCodeCell(sheets.sheet.id, cursorPosition.x, cursorPosition.y).then((code) => { // open code cell unless this is the actual code cell (but not an import, @@ -141,8 +154,20 @@ export function keyboardCell(event: React.KeyboardEvent): boolean { (Number(code.x) !== cursorPosition.x || Number(code.y) !== cursorPosition.y) ) { doubleClickCell({ column: Number(code.x), row: Number(code.y), language: code.language, cell: '' }); +======= + const { x, y } = cursor.position; + 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) !== x || Number(code.y) !== y)) { + doubleClickCell({ + column: Number(code.x), + row: Number(code.y), + language: code.language, + cell: '', + }); +>>>>>>> origin/qa } else { - pixiAppSettings.changeInput(true, event.key); + pixiAppSettings.changeInput(true, event.key, CursorMode.Enter); } }); return true; diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCode.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCode.ts index 1e7f98aca8..26db4454b4 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCode.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardCode.ts @@ -16,8 +16,8 @@ export function keyboardCode(event: React.KeyboardEvent): boolean { if (matchShortcut(Action.ExecuteCode, event)) { quadraticCore.rerunCodeCells( sheets.sheet.id, - sheets.sheet.cursor.cursorPosition.x, - sheets.sheet.cursor.cursorPosition.y, + sheets.sheet.cursor.position.x, + sheets.sheet.cursor.position.y, sheets.getCursorPosition() ); return true; @@ -37,8 +37,8 @@ export function keyboardCode(event: React.KeyboardEvent): boolean { // Insert cell reference if (codeEditorState.showCodeEditor && matchShortcut(Action.InsertCellReference, event)) { - const { sheetId, pos, language } = codeEditorState.codeCell; - insertCellRef(pos, sheetId, language); + const { sheetId, language } = codeEditorState.codeCell; + insertCellRef(sheetId, language); return true; } diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts index 7f0d19a7f2..d892b264bc 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardPosition.ts @@ -3,27 +3,24 @@ import { Action } from '@/app/actions/actions'; import { sheets } from '@/app/grid/controller/Sheets'; +<<<<<<< HEAD import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; import { moveViewport } from '@/app/gridGL/interaction/viewportHelper'; import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; +======= +import { moveViewport, pageUpDown } from '@/app/gridGL/interaction/viewportHelper'; +>>>>>>> origin/qa import { matchShortcut } from '@/app/helpers/keyboardShortcuts.js'; import { JumpDirection } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; -import { Rectangle } from 'pixi.js'; function setCursorPosition(x: number, y: number) { - const newPos = { x, y }; - sheets.sheet.cursor.changePosition({ - multiCursor: null, - columnRow: null, - cursorPosition: newPos, - keyboardMovePosition: newPos, - ensureVisible: newPos, - }); + sheets.sheet.cursor.moveTo(x, y); } +<<<<<<< HEAD // todo: The QuadraticCore checks should be a single call within Rust instead of // having TS handle the logic (this will reduce the number of calls into // quadraticCore) @@ -34,18 +31,19 @@ function setCursorPosition(x: number, y: number) { // - if on a filled cell but the next cell is empty then select to the first cell with a value // - if there are no more cells then select the next cell over (excel selects to the end of the sheet; we don’t have an end (yet) so right now I select one cell over) // the above checks are always made relative to the original cursor position (the highlighted cell) +======= +// handle cases for meta/ctrl keys +>>>>>>> origin/qa async function jumpCursor(direction: JumpDirection, select: boolean) { const cursor = sheets.sheet.cursor; const sheetId = sheets.sheet.id; - // holds either the existing multiCursor or creates a new one based on cursor position - const multiCursor = cursor.multiCursor ?? [new Rectangle(cursor.cursorPosition.x, cursor.cursorPosition.y, 1, 1)]; + const keyboardX = cursor.position.x; + const keyboardY = cursor.position.y; - // the last multiCursor entry, which is what we change with the keyboard - const lastMultiCursor = multiCursor[multiCursor.length - 1]; - const keyboardX = cursor.keyboardMovePosition.x; - const keyboardY = cursor.keyboardMovePosition.y; + const position = await quadraticCore.jumpCursor(sheetId, { x: keyboardX, y: keyboardY }, direction); +<<<<<<< HEAD const position = await quadraticCore.jumpCursor(sheetId, { x: keyboardX, y: keyboardY }, direction); // something went wrong @@ -319,38 +317,28 @@ async function jumpCursor(direction: JumpDirection, select: boolean) { */ } +======= + // something went wrong + if (!position) { + console.error('Failed to jump cursor'); + return; + } +>>>>>>> origin/qa -// use arrow to select when shift key is pressed -function expandSelection(deltaX: number, deltaY: number) { - const cursor = sheets.sheet.cursor; - - const downPosition = cursor.cursorPosition; - const movePosition = cursor.keyboardMovePosition; - - // holds either the existing multiCursor or creates a new one based on cursor position - const multiCursor = cursor.multiCursor ?? [new Rectangle(cursor.cursorPosition.x, cursor.cursorPosition.y, 1, 1)]; - - // the last multiCursor entry, which is what we change with the keyboard - const lastMultiCursor = multiCursor[multiCursor.length - 1]; + const col = Math.max(1, position.x); + const row = Math.max(1, position.y); - const newMovePosition = { x: movePosition.x + deltaX, y: movePosition.y + deltaY }; - lastMultiCursor.x = downPosition.x < newMovePosition.x ? downPosition.x : newMovePosition.x; - lastMultiCursor.y = downPosition.y < newMovePosition.y ? downPosition.y : newMovePosition.y; - lastMultiCursor.width = Math.abs(newMovePosition.x - downPosition.x) + 1; - lastMultiCursor.height = Math.abs(newMovePosition.y - downPosition.y) + 1; - cursor.changePosition({ - columnRow: null, - multiCursor, - keyboardMovePosition: newMovePosition, - ensureVisible: { x: newMovePosition.x, y: newMovePosition.y }, - }); - if (!inlineEditorHandler.cursorIsMoving) { - pixiAppSettings.changeInput(false); + if (select) { + cursor.selectTo(col, row, true); + } else { + cursor.moveTo(col, row); } } function moveCursor(deltaX: number, deltaY: number) { + const clamp = sheets.sheet.clamp; const cursor = sheets.sheet.cursor; +<<<<<<< HEAD const newPos = { x: cursor.cursorPosition.x + deltaX, y: cursor.cursorPosition.y + deltaY }; // need to adjust the cursor position if it is inside an image cell @@ -404,6 +392,25 @@ function moveCursor(deltaX: number, deltaY: number) { keyboardMovePosition: newPos, cursorPosition: newPos, }); +======= + const newPos = { + x: Math.max(clamp.left, cursor.position.x + deltaX), + y: Math.max(clamp.left, cursor.position.y + deltaY), + }; + if (newPos.x > clamp.right) { + newPos.x = clamp.right; + } + if (newPos.y > clamp.bottom) { + newPos.y = clamp.bottom; + } + cursor.moveTo(newPos.x, newPos.y); +} + +function selectTo(deltaX: number, deltaY: number) { + const cursor = sheets.sheet.cursor; + const selectionEnd = cursor.selectionEnd; + cursor.selectTo(selectionEnd.x + deltaX, selectionEnd.y + deltaY, false); +>>>>>>> origin/qa } export function keyboardPosition(event: KeyboardEvent): boolean { @@ -421,13 +428,17 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Expand selection up if (matchShortcut(Action.ExpandSelectionUp, event)) { - expandSelection(0, -1); + selectTo(0, -1); return true; } // Expand selection to the top of the content block of cursor cell if (matchShortcut(Action.ExpandSelectionContentTop, event)) { +<<<<<<< HEAD jumpCursor('Down', true); +======= + jumpCursor('Up', true); +>>>>>>> origin/qa return true; } @@ -445,7 +456,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Expand selection down if (matchShortcut(Action.ExpandSelectionDown, event)) { - expandSelection(0, 1); + selectTo(0, 1); return true; } @@ -469,7 +480,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Expand selection left if (matchShortcut(Action.ExpandSelectionLeft, event)) { - expandSelection(-1, 0); + selectTo(-1, 0); return true; } @@ -493,7 +504,7 @@ export function keyboardPosition(event: KeyboardEvent): boolean { // Expand selection right if (matchShortcut(Action.ExpandSelectionRight, event)) { - expandSelection(1, 0); + selectTo(1, 0); return true; } @@ -504,8 +515,8 @@ export function keyboardPosition(event: KeyboardEvent): boolean { } // Move cursor to A0, reset viewport position with A0 at top left - if (matchShortcut(Action.GotoA0, event)) { - setCursorPosition(0, 0); + if (matchShortcut(Action.GotoA1, event)) { + setCursorPosition(1, 1); moveViewport({ topLeft: { x: 0, y: 0 }, force: true }); return true; } @@ -515,16 +526,36 @@ export function keyboardPosition(event: KeyboardEvent): boolean { const sheet = sheets.sheet; const bounds = sheet.getBounds(true); if (bounds) { +<<<<<<< HEAD setCursorPosition(bounds.right, bounds.bottom); } else { setCursorPosition(1, 1); +======= + const y = bounds.bottom - 1; + quadraticCore + .findNextColumn({ + sheetId: sheet.id, + columnStart: bounds.right - 1, + row: y, + reverse: true, + withContent: true, + }) + .then((x) => { + x = x ?? bounds.right - 1; + setCursorPosition(x, y); + }); +>>>>>>> origin/qa } return true; } // Move cursor to the start of the row content if (matchShortcut(Action.GotoRowStart, event)) { +<<<<<<< HEAD setCursorPosition(1, sheets.sheet.cursor.cursorPosition.y); +======= + sheets.sheet.cursor.moveTo(1, sheets.sheet.cursor.position.y); +>>>>>>> origin/qa return true; } @@ -532,19 +563,41 @@ export function keyboardPosition(event: KeyboardEvent): boolean { if (matchShortcut(Action.GotoRowEnd, event)) { const sheet = sheets.sheet; const bounds = sheet.getBounds(true); +<<<<<<< HEAD setCursorPosition(bounds?.right ?? 1, sheets.sheet.cursor.cursorPosition.y); +======= + if (bounds) { + const y = sheet.cursor.position.y; + quadraticCore + .findNextColumn({ + sheetId: sheet.id, + columnStart: bounds.right - 1, + row: y, + reverse: true, + withContent: true, + }) + .then((x) => { + x = x ?? bounds.right - 1; + quadraticCore.cellHasContent(sheet.id, x, y).then((hasContent) => { + if (hasContent) { + setCursorPosition(x, y); + } + }); + }); + } +>>>>>>> origin/qa return true; } // Move viewport up if (matchShortcut(Action.PageUp, event)) { - moveViewport({ pageUp: true }); + pageUpDown(true); return true; } // Move viewport down if (matchShortcut(Action.PageDown, event)) { - moveViewport({ pageDown: true }); + pageUpDown(false); return true; } diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardSearch.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardSearch.ts index c851dbeacd..8bd34e0bad 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardSearch.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardSearch.ts @@ -12,8 +12,11 @@ export function keyboardSearch(event: React.KeyboardEvent): boolean if (matchShortcut(Action.FindInCurrentSheet, event) || matchShortcut(Action.FindInAllSheets, event)) { event.preventDefault(); if (editorInteractionState.showSearch) { - const search = document.getElementById('search-input'); - search?.focus(); + const search = document.getElementById('search-input') as HTMLInputElement; + if (search) { + search.focus(); + search.select(); + } } else { setEditorInteractionState((prev) => ({ ...prev, diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardSelect.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardSelect.ts index 0cc48822ab..822cbebe1e 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardSelect.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardSelect.ts @@ -1,48 +1,25 @@ import { Action } from '@/app/actions/actions'; import { sheets } from '@/app/grid/controller/Sheets.js'; -import { selectAllCells, selectColumns, selectRows } from '@/app/gridGL/helpers/selectCells'; import { matchShortcut } from '@/app/helpers/keyboardShortcuts.js'; export function keyboardSelect(event: React.KeyboardEvent): boolean { + const cursor = sheets.sheet.cursor; + // Select all if (matchShortcut(Action.SelectAll, event)) { - selectAllCells(); + cursor.selectAll(); return true; } // Select column if (matchShortcut(Action.SelectColumn, event)) { - const cursor = sheets.sheet.cursor; - if (cursor.columnRow?.all || cursor.columnRow?.rows?.length) { - selectAllCells(); - } else { - let columns = new Set(cursor.columnRow?.columns); - columns.add(cursor.cursorPosition.x); - cursor.multiCursor?.forEach((rect) => { - for (let x = rect.x; x < rect.x + rect.width; x++) { - columns.add(x); - } - }); - selectColumns(Array.from(columns), cursor.cursorPosition.x); - } + cursor.setColumnsSelected(); return true; } // Select row if (matchShortcut(Action.SelectRow, event)) { - const cursor = sheets.sheet.cursor; - if (cursor.columnRow?.all || cursor.columnRow?.columns?.length) { - selectAllCells(); - } else { - let row = new Set(cursor.columnRow?.rows); - row.add(cursor.cursorPosition.y); - cursor.multiCursor?.forEach((rect) => { - for (let y = rect.y; y < rect.y + rect.height; y++) { - row.add(y); - } - }); - selectRows(Array.from(row), cursor.cursorPosition.y); - } + cursor.setRowsSelected(); return true; } diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardViewport.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardViewport.ts index 36ae0643fa..5563e5f147 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardViewport.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/keyboardViewport.ts @@ -1,5 +1,6 @@ import { hasPermissionToEditFile } from '@/app/actions'; import { Action } from '@/app/actions/actions'; +import { viewActionsSpec } from '@/app/actions/viewActionsSpec'; import { debug } from '@/app/debugFlags'; import { sheets } from '@/app/grid/controller/Sheets.js'; import { zoomIn, zoomOut, zoomTo100, zoomToFit, zoomToSelection } from '@/app/gridGL/helpers/zoom'; @@ -53,6 +54,12 @@ export function keyboardViewport(event: React.KeyboardEvent): boole return true; } + // Toggle global AI chat + if (matchShortcut(Action.ToggleAIAnalyst, event)) { + viewActionsSpec[Action.ToggleAIAnalyst].run(); + return true; + } + // Toggle presentation mode if (matchShortcut(Action.TogglePresentationMode, event)) { setGridSettings({ ...gridSettings, presentationMode: !gridSettings.presentationMode }); @@ -184,54 +191,35 @@ export function keyboardViewport(event: React.KeyboardEvent): boole // Fill right // Disabled in debug mode, to allow page reload - if (!debug && matchShortcut(Action.FillRight, event)) { + if ((!debug && matchShortcut(Action.FillRight, event)) || (debug && event.ctrlKey && event.key === 'r')) { const cursor = sheets.sheet.cursor; - if (cursor.columnRow?.all || cursor.columnRow?.rows) return true; - if (cursor.columnRow?.columns && cursor.multiCursor) return true; - if (cursor.columnRow?.columns) { - if (cursor.columnRow.columns.length > 1) return true; - const column = cursor.columnRow.columns[0]; - const bounds = sheets.sheet.getBounds(false); - if (!bounds) return true; - quadraticCore.autocomplete( - sheets.current, - column - 1, - bounds.top, - column - 1, - bounds.bottom, - column - 1, - bounds.top, - column, - bounds.bottom - ); - } else if (cursor.multiCursor) { - if (cursor.multiCursor.length > 1) return true; - const rectangle = cursor.multiCursor[0]; - if (rectangle.width > 1) return true; - quadraticCore.autocomplete( - sheets.current, - rectangle.x - 1, - rectangle.top, - rectangle.x - 1, - rectangle.bottom, - rectangle.x - 1, - rectangle.top, - rectangle.x, - rectangle.bottom - ); - } else { - const position = cursor.cursorPosition; - quadraticCore.autocomplete( - sheets.current, - position.x - 1, - position.y, - position.x - 1, - position.y, - position.x - 1, - position.y, - position.x, - position.y - ); + const rect = cursor.getSingleRectangleOrCursor(); + if (rect) { + if (rect.width === 1) { + quadraticCore.autocomplete( + sheets.current, + rect.left - 1, + rect.top, + rect.left - 1, + rect.bottom - 1, + rect.left, + rect.top, + rect.left, + rect.bottom - 1 + ); + } else { + quadraticCore.autocomplete( + sheets.current, + rect.left, + rect.top, + rect.left, + rect.bottom - 1, + rect.left + 1, + rect.top, + rect.right - 1, + rect.bottom - 1 + ); + } } return true; @@ -240,52 +228,33 @@ export function keyboardViewport(event: React.KeyboardEvent): boole // Fill down if (matchShortcut(Action.FillDown, event)) { const cursor = sheets.sheet.cursor; - if (cursor.columnRow?.all || cursor.columnRow?.columns) return true; - if (cursor.columnRow?.rows && cursor.multiCursor) return true; - if (cursor.columnRow?.rows) { - if (cursor.columnRow.rows.length > 1) return true; - const row = cursor.columnRow.rows[0]; - const bounds = sheets.sheet.getBounds(false); - if (!bounds) return true; - quadraticCore.autocomplete( - sheets.current, - bounds.left, - row - 1, - bounds.right, - row - 1, - bounds.left, - row - 1, - bounds.right, - row - ); - } else if (cursor.multiCursor) { - if (cursor.multiCursor.length > 1) return true; - const rectangle = cursor.multiCursor[0]; - if (rectangle.height > 1) return true; - quadraticCore.autocomplete( - sheets.current, - rectangle.left, - rectangle.top - 1, - rectangle.right, - rectangle.top - 1, - rectangle.left, - rectangle.top - 1, - rectangle.right, - rectangle.top - ); - } else { - const position = cursor.cursorPosition; - quadraticCore.autocomplete( - sheets.current, - position.x, - position.y - 1, - position.x, - position.y - 1, - position.x, - position.y - 1, - position.x, - position.y - ); + const rect = cursor.getSingleRectangleOrCursor(); + if (rect) { + if (rect.height === 1) { + quadraticCore.autocomplete( + sheets.current, + rect.left, + rect.top - 1, + rect.right - 1, + rect.top - 1, + rect.left, + rect.top, + rect.right - 1, + rect.top + ); + } else { + quadraticCore.autocomplete( + sheets.current, + rect.left, + rect.top, + rect.right - 1, + rect.top, + rect.left, + rect.top + 1, + rect.right - 1, + rect.bottom - 1 + ); + } } return true; diff --git a/quadratic-client/src/app/gridGL/interaction/keyboard/useKeyboard.ts b/quadratic-client/src/app/gridGL/interaction/keyboard/useKeyboard.ts index 19d3198552..3ec8ecdda8 100644 --- a/quadratic-client/src/app/gridGL/interaction/keyboard/useKeyboard.ts +++ b/quadratic-client/src/app/gridGL/interaction/keyboard/useKeyboard.ts @@ -55,13 +55,13 @@ export const useKeyboard = (): { const cursor = sheet.cursor; const today = new Date(); const formattedDate = `${today.getFullYear()}/${today.getMonth() + 1}/${today.getDate()}`; - quadraticCore.setCellValue(sheet.id, cursor.cursorPosition.x, cursor.cursorPosition.y, formattedDate); + quadraticCore.setCellValue(sheet.id, cursor.position.x, cursor.position.y, formattedDate); } else if (matchShortcut(Action.InsertTodayTime, event)) { const sheet = sheets.sheet; const cursor = sheet.cursor; const today = new Date(); const formattedTime = `${today.getHours()}:${today.getMinutes()}:${today.getSeconds()}`; - quadraticCore.setCellValue(sheet.id, cursor.cursorPosition.x, cursor.cursorPosition.y, formattedTime); + quadraticCore.setCellValue(sheet.id, cursor.position.x, cursor.position.y, formattedTime); } // Prevent these commands if "command" key is being pressed diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerAutoComplete.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerAutoComplete.ts index 3f0ee73a9c..693f49f948 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerAutoComplete.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerAutoComplete.ts @@ -1,25 +1,23 @@ import { PanMode } from '@/app/atoms/gridPanModeAtom'; import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; +import { JsCoordinate } from '@/app/quadratic-core-types'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { Point, Rectangle } from 'pixi.js'; import { isMobile } from 'react-device-detect'; -import { sheets } from '../../../grid/controller/Sheets'; -import { intersects } from '../../helpers/intersects'; -import { pixiApp } from '../../pixiApp/PixiApp'; -import { pixiAppSettings } from '../../pixiApp/PixiAppSettings'; -import { Coordinate } from '../../types/size'; export type StateVertical = 'expandDown' | 'expandUp' | 'shrink' | undefined; export type StateHorizontal = 'expandRight' | 'expandLeft' | 'shrink' | undefined; export class PointerAutoComplete { private selection?: Rectangle; - private endCell?: Coordinate; + private endCell?: JsCoordinate; private stateHorizontal: StateHorizontal; private stateVertical: StateVertical; - private toVertical?: number; - private toHorizontal?: number; private screenSelection?: Rectangle; cursor?: string; active = false; @@ -32,21 +30,14 @@ export class PointerAutoComplete { if (pixiAppSettings.panMode !== PanMode.Disabled) return false; - if (cursor.multiCursor && cursor.multiCursor.length > 1) return false; + this.selection = cursor.getSingleRectangleOrCursor(); + if (!this.selection) return false; // handle dragging from the corner if (intersects.rectanglePoint(pixiApp.cursor.indicator, world)) { this.active = true; events.emit('cellMoving', true); - this.selection = cursor.multiCursor - ? cursor.multiCursor[0] - : new Rectangle(cursor.cursorPosition.x, cursor.cursorPosition.y, 1, 1); - this.screenSelection = sheet.getScreenRectangle( - this.selection.left, - this.selection.top, - this.selection.width, - this.selection.height - ); + this.screenSelection = sheet.getScreenRectangleFromRect(this.selection); cursor.changeBoxCells(true); return true; @@ -60,8 +51,6 @@ export class PointerAutoComplete { this.stateHorizontal = undefined; this.stateVertical = undefined; this.endCell = undefined; - this.toHorizontal = undefined; - this.toVertical = undefined; this.selection = undefined; this.screenSelection = undefined; this.active = false; @@ -92,7 +81,7 @@ export class PointerAutoComplete { this.endCell = { x: column, y: row }; const boxCellsRectangle = selection.clone(); - const deleteRectangles = []; + const deleteRectangles: Rectangle[] = []; // Note: there is weirdness as to the rectangle size because the cell in // which the cursor is hovering is where we want to expand/shrink to. @@ -102,66 +91,58 @@ export class PointerAutoComplete { // if at bottom or single height then don't do anything if (row === selection.bottom - 1 || (row === selection.top && selection.top === selection.bottom)) { - this.toVertical = undefined; this.stateVertical = undefined; - boxCellsRectangle.height = selection.height - 1; + boxCellsRectangle.height = selection.height; } // if at top or between top and bottom then we shrink - else if (row >= selection.top && row + 1 < selection.bottom) { + else if (row >= selection.top && row < selection.bottom - 1) { this.stateVertical = 'shrink'; - this.toVertical = row; - boxCellsRectangle.height = row - selection.top; - deleteRectangles.push(new Rectangle(selection.x, row + 1, selection.width - 1, selection.bottom - row - 2)); + boxCellsRectangle.height = row - selection.top + 1; + deleteRectangles.push(new Rectangle(selection.x, row + 1, selection.width, selection.bottom - 1 - row)); } // if above top, then we expand up else if (row < selection.top) { this.stateVertical = 'expandUp'; - this.toVertical = row; boxCellsRectangle.y = row; - boxCellsRectangle.height = selection.bottom - row - 1; + boxCellsRectangle.height = selection.bottom - row; } // if below bottom, then we expand down - else if (row >= selection.bottom) { + else if (row >= selection.bottom - 1) { this.stateVertical = 'expandDown'; - this.toVertical = row; - boxCellsRectangle.height = row - selection.y; + boxCellsRectangle.height = row + 1 - selection.y; } // Handle changes in column // if at right or single width then don't do anything if (column === selection.right - 1 || (column === selection.left && selection.left === selection.right)) { - this.toHorizontal = undefined; this.stateHorizontal = undefined; - boxCellsRectangle.width = selection.width - 1; + boxCellsRectangle.width = selection.width; } // if at left or between left and right then we shrink - else if (column >= selection.left && column + 1 < selection.right) { + else if (column >= selection.left && column < selection.right - 1) { this.stateHorizontal = 'shrink'; - this.toHorizontal = column; - boxCellsRectangle.width = column - selection.left; + boxCellsRectangle.width = column - selection.left + 1; deleteRectangles.push( - new Rectangle(column + 1, selection.y, selection.right - column - 2, row - selection.y) + new Rectangle(column + 1, selection.y, selection.right - 1 - column, row - selection.y + 1) ); } // if to the left of the selection then we expand left else if (column < selection.left) { this.stateHorizontal = 'expandLeft'; - this.toHorizontal = column; boxCellsRectangle.x = column; - boxCellsRectangle.width = selection.right - column - 1; + boxCellsRectangle.width = selection.right - column; } // if to the right of the selection then we expand right - else if (column >= selection.right) { + else if (column >= selection.right - 1) { this.stateHorizontal = 'expandRight'; - this.toHorizontal = column; - boxCellsRectangle.width = column - selection.x; + boxCellsRectangle.width = column + 1 - selection.x; } pixiApp.boxCells.populate({ @@ -235,16 +216,13 @@ export class PointerAutoComplete { } // update the selection - const cursor = sheets.sheet.cursor; - if (newRectangle.width === 1 && newRectangle.height === 1) { - cursor.changePosition({ columnRow: null, multiCursor: null }); - } else { - cursor.changePosition({ - columnRow: null, - multiCursor: [newRectangle], - ensureVisible: false, - }); - } + sheets.sheet.cursor.selectRect( + newRectangle.left, + newRectangle.top, + newRectangle.right - 1, + newRectangle.bottom - 1, + false + ); } this.reset(); return true; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts index c56b9c7934..c8dc5dc098 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerCellMoving.ts @@ -174,14 +174,11 @@ export class PointerCellMoving { } private pointerMoveHover(world: Point): boolean { - const sheet = sheets.sheet; - const rectangles = sheet.cursor.getRectangles(); - // we do not move if there are multiple rectangles (for now) - if (rectangles.length > 1) return false; - const rectangle = rectangles[0]; + const rectangle = sheets.sheet.cursor.getSingleRectangleOrCursor(); + if (!rectangle) return false; - const origin = sheet.cursor.getCursor(); + const origin = sheets.sheet.cursor.position; const column = origin.x; const row = origin.y; @@ -234,6 +231,7 @@ export class PointerCellMoving { this.movingCells && (this.startCell.x !== this.movingCells.toColumn || this.startCell.y !== this.movingCells.toRow) ) { +<<<<<<< HEAD const table = getTable(); const rectangle = sheets.sheet.cursor.getLargestMultiCursorRectangle(); @@ -242,36 +240,32 @@ export class PointerCellMoving { rectangle.height = table.h; } +======= + const rectangle = sheets.sheet.cursor.getLargestRectangle(); +>>>>>>> origin/qa quadraticCore.moveCells( - rectToSheetRect( - new Rectangle(rectangle.x, rectangle.y, rectangle.width - 1, rectangle.height - 1), - sheets.sheet.id - ), + rectToSheetRect(rectangle, sheets.sheet.id), this.movingCells.toColumn, this.movingCells.toRow, sheets.sheet.id ); - // if we moved the code cell, we need to repopulate the code editor with - // unsaved content. - if (pixiAppSettings.unsavedEditorChanges) { - const { codeCell } = pixiAppSettings.codeEditorState; - if ( - codeCell.sheetId === sheets.current && - intersects.rectanglePoint(rectangle, new Point(codeCell.pos.x, codeCell.pos.y)) - ) { - pixiAppSettings.setCodeEditorState?.({ - ...pixiAppSettings.codeEditorState, - initialCode: pixiAppSettings.unsavedEditorChanges ?? '', - codeCell: { - ...codeCell, - pos: { - x: codeCell.pos.x + this.movingCells.toColumn - this.movingCells.column, - y: codeCell.pos.y + this.movingCells.toRow - this.movingCells.row, - }, + const { showCodeEditor, codeCell } = pixiAppSettings.codeEditorState; + if ( + showCodeEditor && + codeCell.sheetId === sheets.current && + intersects.rectanglePoint(rectangle, new Point(codeCell.pos.x, codeCell.pos.y)) + ) { + pixiAppSettings.setCodeEditorState?.({ + ...pixiAppSettings.codeEditorState, + codeCell: { + ...codeCell, + pos: { + x: codeCell.pos.x + this.movingCells.toColumn - this.movingCells.column, + y: codeCell.pos.y + this.movingCells.toRow - this.movingCells.row, }, - }); - } + }, + }); } } this.reset(); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts index 53b8f50267..132b7fe637 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerDown.ts @@ -2,6 +2,7 @@ import { ContextMenuType } from '@/app/atoms/contextMenuAtom'; import { PanMode } from '@/app/atoms/gridPanModeAtom'; import { events } from '@/app/events/events'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; +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'; @@ -25,6 +26,9 @@ export class PointerDown { private pointerMoved = false; private doubleClickTimeout?: number; + // used to track the unselect rectangle + unselectDown?: Rectangle; + // flag that ensures that if pointerUp triggers during setTimeout, pointerUp is still called (see below) private afterShowInput?: boolean; @@ -66,12 +70,8 @@ export class PointerDown { // If right click and we have a multi cell selection. // If the user has clicked inside the selection. if (isRightClick) { - if (!cursor.includesCell(column, row)) { - cursor.changePosition({ - cursorPosition: { x: column, y: row }, - multiCursor: null, - ensureVisible: false, - }); + if (!cursor.contains(column, row)) { + cursor.moveTo(column, row, false); // hack to ensure that the context menu opens after the cursor changes // position (otherwise it may close immediately) setTimeout(() => events.emit('contextMenu', { type: ContextMenuType.Grid, world, column, row })); @@ -94,117 +94,67 @@ 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 }); + doubleClickCell({ column, row, cell, cursorMode: cell ? CursorMode.Edit : CursorMode.Enter }); } this.active = false; return; } } + // do nothing if we have text is invalid in the input + if (!(await this.isInputValid())) return; - // Select cells between pressed and cursor position. Uses last multiCursor - // or creates a multiCursor. - if (event.shiftKey) { - // do nothing if we have text is invalid in the input - if (!(await this.isInputValid())) return; - - const { column, row } = sheet.getColumnRowFromScreen(world.x, world.y); - const cursorPosition = cursor.cursorPosition; - if (column !== cursorPosition.x || row !== cursorPosition.y) { - // make origin top left, and terminal bottom right - const originX = cursorPosition.x < column ? cursorPosition.x : column; - const originY = cursorPosition.y < row ? cursorPosition.y : row; - const termX = cursorPosition.x > column ? cursorPosition.x : column; - const termY = cursorPosition.y > row ? cursorPosition.y : row; - const newRectangle = new Rectangle(originX, originY, termX - originX + 1, termY - originY + 1); - - if (cursor.multiCursor?.length) { - const multiCursor = [...cursor.multiCursor]; - multiCursor[multiCursor.length - 1] = newRectangle; - cursor.changePosition({ - columnRow: event.metaKey || event.ctrlKey ? undefined : null, - keyboardMovePosition: { x: column, y: row }, - multiCursor, - ensureVisible: false, - }); - } else { - cursor.changePosition({ - keyboardMovePosition: { x: column, y: row }, - multiCursor: [newRectangle], - ensureVisible: false, - }); - } - } - this.active = true; - this.position = new Point(cursorPosition.x, cursorPosition.y); - this.previousPosition = new Point(column, row); - this.pointerMoved = false; + if ((event.ctrlKey || event.metaKey) && cursor.contains(column, row)) { + this.unselectDown = new Rectangle(column, row, 0, 0); + pixiApp.cursor.dirty = true; return; } - // select another multiCursor range - if (!this.active && (event.metaKey || event.ctrlKey)) { - if (!(await this.isInputValid())) return; - - const cursorPosition = cursor.cursorPosition; - if (cursor.multiCursor || column !== cursorPosition.x || row !== cursorPosition.y) { - event.stopPropagation(); - const multiCursor = cursor.multiCursor ?? [new Rectangle(cursorPosition.x, cursorPosition.y, 1, 1)]; - multiCursor.push(new Rectangle(column, row, 1, 1)); - cursor.changePosition({ - cursorPosition: { x: column, y: row }, - multiCursor, - ensureVisible: false, - }); - this.active = true; - this.position = new Point(column, row); - - // Keep track of multiCursor previous position - this.previousPosition = new Point(column, row); - - this.pointerMoved = false; - return; + // If the user is holding cmd/ctrl and the cell is already selected, then we start the un-selection. + if (event.shiftKey) { + cursor.selectTo(column, row, event.metaKey || event.ctrlKey); + } else { + // If the input is rejected, we cannot move the cursor + if (await inlineEditorHandler.handleCellPointerDown()) { + cursor.moveTo(column, row, event.metaKey || event.ctrlKey); + } else { + inlineEditorMonaco.focus(); } } - - this.active = true; - this.position = new Point(column, row); - - // Keep track of multiCursor previous position this.previousPosition = new Point(column, row); - - // Move cursor to mouse down position - // For single click, hide multiCursor - - // If the input is rejected, we cannot move the cursor - if (await inlineEditorHandler.handleCellPointerDown()) { - cursor.changePosition({ - keyboardMovePosition: { x: column, y: row }, - cursorPosition: { x: column, y: row }, - multiCursor: - (event.metaKey || event.ctrlKey) && cursor.multiCursor - ? cursor.multiCursor.slice(0, cursor.multiCursor.length - 1) - : null, - columnRow: event.metaKey || event.ctrlKey ? cursor.columnRow : null, - ensureVisible: false, - }); - } else { - inlineEditorMonaco.focus(); - } events.emit('clickedToCell', column, row, world); this.pointerMoved = false; + this.position = new Point(column, row); + this.active = true; } pointerMove(world: Point, event: PointerEvent): void { if (pixiAppSettings.panMode !== PanMode.Disabled) return; - if (!this.active) return; - const { viewport } = pixiApp; const sheet = sheets.sheet; - const cursor = sheet.cursor; + + if (this.unselectDown) { + const { column, row } = sheet.getColumnRowFromScreen(world.x, world.y); + this.unselectDown.width = column - this.unselectDown.left; + this.unselectDown.height = row - this.unselectDown.top; + + // this is necessary to ensure the rectangle always has width/height + if (this.unselectDown.width < 0) this.unselectDown.width -= 1; + if (this.unselectDown.height < 0) this.unselectDown.height -= 1; + + pixiApp.cursor.dirty = true; + return; + } + + if (!this.active) return; if (!this.pointerMoved && this.positionRaw) { if ( @@ -213,6 +163,8 @@ export class PointerDown { ) { this.pointerMoved = true; this.clearDoubleClick(); + } else { + return; } } @@ -224,56 +176,13 @@ export class PointerDown { // calculate mouse move position const { column, row } = sheet.getColumnRowFromScreen(world.x, world.y); - const columnRow = event.metaKey || event.ctrlKey ? undefined : null; + if (column !== this.previousPosition.x || row !== this.previousPosition.y) { + sheet.cursor.selectTo(column, row, event.ctrlKey || event.metaKey); + this.previousPosition = new Point(column, row); - // cursor start and end in the same cell - if (column === this.position.x && row === this.position.y) { - // hide multi cursor when only selecting one cell - if (cursor.multiCursor && cursor.multiCursor.length === 1) { - cursor.changePosition({ - columnRow, - keyboardMovePosition: { x: this.position.x, y: this.position.y }, - cursorPosition: { x: this.position.x, y: this.position.y }, - multiCursor: null, - ensureVisible: false, - }); - } - this.previousPosition = new Point(this.position.x, this.position.y); if (inlineEditorHandler.isOpen() && !inlineEditorHandler.isEditingFormula()) { pixiAppSettings.changeInput(false); } - } else { - // cursor origin and terminal are not in the same cell - - // make origin top left, and terminal bottom right - const originX = this.position.x < column ? this.position.x : column; - const originY = this.position.y < row ? this.position.y : row; - const termX = this.position.x > column ? this.position.x : column; - const termY = this.position.y > row ? this.position.y : row; - - // determine if the cursor has moved from the previous event - const hasMoved = !(this.previousPosition.x === column && this.previousPosition.y === row); - - // only set state if changed - // this reduces the number of hooks fired - if (hasMoved) { - // update multiCursor - const multiCursor = cursor.multiCursor ? cursor.multiCursor.slice(0, cursor.multiCursor.length - 1) : []; - multiCursor.push(new Rectangle(originX, originY, termX - originX + 1, termY - originY + 1)); - cursor.changePosition({ - columnRow, - keyboardMovePosition: { x: column, y: row }, - cursorPosition: { x: this.position.x, y: this.position.y }, - multiCursor, - ensureVisible: false, - }); - if (inlineEditorHandler.isOpen() && !inlineEditorHandler.isEditingFormula()) { - pixiAppSettings.changeInput(false); - } - - // update previousPosition - this.previousPosition = new Point(column, row); - } } } @@ -285,6 +194,18 @@ export class PointerDown { event.stopPropagation(); } + if (this.unselectDown) { + sheets.sheet.cursor.excludeCells( + this.unselectDown.left, + this.unselectDown.top, + this.unselectDown.right, + this.unselectDown.bottom + ); + this.unselectDown = undefined; + pixiApp.cursor.dirty = true; + return; + } + if (this.afterShowInput) { window.setTimeout(() => this.pointerUp(), 0); this.afterShowInput = false; diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts index adce122be1..8b86472497 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHeading.ts @@ -11,7 +11,6 @@ import { isMac } from '@/shared/utils/isMac'; import { InteractivePointerEvent, Point } from 'pixi.js'; import { hasPermissionToEditFile } from '../../../actions'; import { sheets } from '../../../grid/controller/Sheets'; -import { selectAllCells, selectColumns, selectRows } from '../../helpers/selectCells'; import { zoomToFit } from '../../helpers/zoom'; import { pixiApp } from '../../pixiApp/PixiApp'; import { pixiAppSettings } from '../../pixiApp/PixiAppSettings'; @@ -19,18 +18,6 @@ import { DOUBLE_CLICK_TIME } from './pointerUtils'; const MINIMUM_COLUMN_SIZE = 20; -// Returns an array with all numbers inclusive of start to end -function fillArray(start: number, end: number): number[] { - const result = []; - if (start > end) { - [start, end] = [end, start]; - } - for (let i = start; i <= end; i++) { - result.push(i); - } - return result; -} - export interface ResizeHeadingColumnEvent extends CustomEvent { detail: number; } @@ -78,6 +65,7 @@ export class PointerHeading { // exit out of inline editor inlineEditorHandler.closeIfOpen(); + const cursor = sheets.sheet.cursor; const hasPermission = hasPermissionToEditFile(pixiAppSettings.editorInteractionState.permissions); const headingResize = !hasPermission ? undefined : headings.intersectsHeadingGridLine(world); @@ -112,7 +100,7 @@ export class PointerHeading { this.downTimeout = undefined; zoomToFit(); } else { - selectAllCells(); + cursor.selectAll(event.shiftKey); this.downTimeout = window.setTimeout(() => { if (this.downTimeout) { this.downTimeout = undefined; @@ -121,14 +109,13 @@ export class PointerHeading { } } - const cursor = sheets.sheet.cursor; - // Selects multiple columns or rows. If ctrl/meta is pressed w/o shift, // then it add or removes the clicked column or row. If shift is pressed, // then it selects all columns or rows between the last clicked column or // row and the current one. const isRightClick = (event as MouseEvent).button === 2 || (isMac && (event as MouseEvent).button === 0 && event.ctrlKey); +<<<<<<< HEAD if (event.ctrlKey || event.metaKey || isRightClick) { if (intersects.column !== null) { let column = intersects.column; @@ -226,6 +213,16 @@ export class PointerHeading { } else if (intersects.row !== null) { selectRows([intersects.row]); } +======= + const bounds = pixiApp.viewport.getVisibleBounds(); + const headingSize = pixiApp.headings.headingSize; + if (intersects.column !== null) { + const top = sheets.sheet.getRowFromScreen(bounds.top + headingSize.height); + cursor.selectColumn(intersects.column, event.ctrlKey || event.metaKey, event.shiftKey, isRightClick, top); + } else if (intersects.row !== null) { + const left = sheets.sheet.getColumnFromScreen(bounds.left); + cursor.selectRow(intersects.row, event.ctrlKey || event.metaKey, event.shiftKey, isRightClick, left); +>>>>>>> origin/qa } } diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHtmlCells.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHtmlCells.ts index 46701b9598..7f0ed101f5 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerHtmlCells.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerHtmlCells.ts @@ -5,7 +5,7 @@ import { HtmlCell } from '@/app/gridGL/HTMLGrid/htmlCells/HtmlCell'; import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; import { DOUBLE_CLICK_TIME } from '@/app/gridGL/interaction/pointer/pointerUtils.js'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; -import { InteractionEvent, Rectangle } from 'pixi.js'; +import { InteractionEvent } from 'pixi.js'; export class PointerHtmlCells { private resizing: HtmlCell | undefined; // cell that is being resized @@ -83,7 +83,7 @@ export class PointerHtmlCells { // select code cell, move chart to top and start double click timer if (target === 'body') { const cursor = sheets.sheet.cursor; - const cursorPosition = cursor.cursorPosition; + // double click if (this.clicked === cell) { this.clicked = undefined; @@ -93,24 +93,13 @@ export class PointerHtmlCells { // click with meta / ctrl key // select cell and add to selection else if (event.metaKey || event.ctrlKey) { - const multiCursor = cursor.multiCursor - ? [...cursor.multiCursor] - : [new Rectangle(cursorPosition.x, cursorPosition.y, 1, 1)]; - multiCursor.push(new Rectangle(cell.x, cell.y, 1, 1)); - cursor.changePosition({ - cursorPosition: { x: cell.x, y: cell.y }, - multiCursor, - }); + cursor.moveTo(cell.x, cell.y, true); } // click without meta / ctrl key // select cell and clear selection else { this.active = cell; - cursor.changePosition({ - cursorPosition: { x: cell.x, y: cell.y }, - columnRow: null, - multiCursor: null, - }); + cursor.moveTo(cell.x, cell.y); } // move chart to top, useful in case of overlapping charts htmlCellsHandler.movetoTop(cell); diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/PointerLink.ts b/quadratic-client/src/app/gridGL/interaction/pointer/PointerLink.ts index 8ba08f3429..94a59a1482 100644 --- a/quadratic-client/src/app/gridGL/interaction/pointer/PointerLink.ts +++ b/quadratic-client/src/app/gridGL/interaction/pointer/PointerLink.ts @@ -72,8 +72,7 @@ export class PointerLink { }; pointerDown = (world: Point, event: PointerEvent): boolean => { - const { multiCursor } = sheets.sheet.cursor; - if (matchShortcut(Action.CmdClick, event) && !multiCursor) { + if (matchShortcut(Action.CmdClick, event) && !sheets.sheet.cursor.isMultiCursor()) { const link = this.checkHoverLink(world); if (link?.pos) { quadraticCore.getDisplayCell(pixiApp.cellsSheets.current?.sheetId ?? '', link.pos.x, link.pos.y).then((url) => { diff --git a/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts b/quadratic-client/src/app/gridGL/interaction/pointer/doubleClickCell.ts index f0df515e1a..6300cae9ab 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 { 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'; @@ -11,8 +12,9 @@ export async function doubleClickCell(options: { row: number; language?: CodeCellLanguage; cell?: string; + cursorMode?: CursorMode; }) { - const { language, cell, column, row } = options; + const { language, cell, column, row, cursorMode } = options; if (inlineEditorHandler.isEditingFormula()) return; if (multiplayer.cellIsBeingEdited(column, row, sheets.sheet.id)) return; @@ -28,6 +30,7 @@ export async function doubleClickCell(options: { pixiAppSettings.setCodeEditorState({ ...pixiAppSettings.codeEditorState, escapePressed: false, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -41,20 +44,26 @@ export async function doubleClickCell(options: { }); } else { if (hasPermission && formula) { - const cursor = sheets.sheet.cursor.cursorPosition; + const cursor = sheets.sheet.cursor.position; // ensure we're in the right cell (which may change if we double clicked on a CodeRun) if (cursor.x !== column || cursor.y !== row) { - sheets.sheet.cursor.changePosition({ cursorPosition: { x: column, y: row } }); + sheets.sheet.cursor.moveTo(column, row); } +<<<<<<< HEAD pixiAppSettings.changeInput(true, cell); } else if (hasPermission && file_import) { pixiAppSettings.changeInput(true, cell); +======= + + pixiAppSettings.changeInput(true, cell, cursorMode); +>>>>>>> origin/qa } else { pixiAppSettings.setCodeEditorState({ ...pixiAppSettings.codeEditorState, showCodeEditor: true, escapePressed: false, + diffEditorContent: undefined, waitingForEditorClose: { codeCell: { sheetId: sheets.current, @@ -80,6 +89,6 @@ export async function doubleClickCell(options: { annotationState: `calendar${value.kind === 'date time' ? '-time' : ''}`, }); } - pixiAppSettings.changeInput(true, cell); + pixiAppSettings.changeInput(true, cell, cursorMode); } } diff --git a/quadratic-client/src/app/gridGL/interaction/viewportHelper.ts b/quadratic-client/src/app/gridGL/interaction/viewportHelper.ts index 004f72f943..cab5eed3d2 100644 --- a/quadratic-client/src/app/gridGL/interaction/viewportHelper.ts +++ b/quadratic-client/src/app/gridGL/interaction/viewportHelper.ts @@ -1,10 +1,9 @@ -import { HEADING_SIZE } from '@/shared/constants/gridConstants'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { intersects } from '@/app/gridGL/helpers/intersects'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; +import { JsCoordinate } from '@/app/quadratic-core-types'; import { Point } from 'pixi.js'; -import { sheets } from '../../grid/controller/Sheets'; -import { intersects } from '../helpers/intersects'; -import { pixiApp } from '../pixiApp/PixiApp'; -import { pixiAppSettings } from '../pixiApp/PixiAppSettings'; -import { Coordinate } from '../types/size'; export function getVisibleTopRow(): number { const viewport = pixiApp.viewport.getVisibleBounds(); @@ -62,11 +61,49 @@ export function isColumnVisible(column: number): boolean { return true; } +// Makes a rect visible in the viewport +export function rectVisible(min: JsCoordinate, max: JsCoordinate): boolean { + // returns true if the rect is visible in the viewport + const { viewport, headings } = pixiApp; + const sheet = sheets.sheet; + const headingSize = headings.headingSize; + + const topLeftCell = sheet.getCellOffsets(min.x, min.y); + const bottomRightCell = sheet.getCellOffsets(max.x, max.y); + let is_off_screen = false; + + if (bottomRightCell.right > viewport.right) { + viewport.right = bottomRightCell.right; + is_off_screen = true; + } + if (topLeftCell.left + headingSize.width < viewport.left) { + viewport.left = topLeftCell.left - headingSize.width / viewport.scale.x; + is_off_screen = true; + } + + if (bottomRightCell.bottom > viewport.bottom) { + viewport.bottom = bottomRightCell.bottom; + is_off_screen = true; + } + if (topLeftCell.top + headingSize.height < viewport.top) { + viewport.top = topLeftCell.top - headingSize.height / viewport.scale.x; + is_off_screen = true; + } + + return !is_off_screen; +} + +export function ensureRectVisible(min: JsCoordinate, max: JsCoordinate) { + if (!rectVisible(min, max)) { + pixiApp.viewportChanged(); + } +} + // Makes a cell visible in the viewport export function cellVisible( - coordinate: Coordinate = { - x: sheets.sheet.cursor.cursorPosition.x, - y: sheets.sheet.cursor.cursorPosition.y, + coordinate: JsCoordinate = { + x: sheets.sheet.cursor.position.x, + y: sheets.sheet.cursor.position.y, } ): boolean { // returns true if the cursor is visible in the viewport @@ -97,7 +134,7 @@ export function cellVisible( } // Ensures the cursor is always visible -export function ensureVisible(visible: Coordinate | undefined) { +export function ensureVisible(visible: JsCoordinate | undefined) { if (!cellVisible(visible)) { pixiApp.viewportChanged(); } @@ -113,20 +150,16 @@ export function ensureVisible(visible: Coordinate | undefined) { * @param [options.pageDown] move viewport down one page * @param [options.force] force viewport to move even if cell is already visible */ -export function moveViewport(options: { - center?: Coordinate; - topLeft?: Coordinate; - pageUp?: boolean; - pageDown?: boolean; - force?: boolean; -}): void { - const { center, topLeft, pageUp, pageDown, force } = options; - if (!center && !topLeft && !pageUp && !pageDown) return; +export function moveViewport(options: { center?: JsCoordinate; topLeft?: JsCoordinate; force?: boolean }): void { + const { center, topLeft, force } = options; + if (!center && !topLeft) return; const sheet = sheets.sheet; const bounds = pixiApp.viewport.getVisibleBounds(); const zoom = pixiApp.viewport.scale.x; - const adjust = pixiAppSettings.showHeadings ? HEADING_SIZE / zoom : 0; + const { width, height } = pixiApp.headings.headingSize; + const adjustX = width / zoom; + const adjustY = height / zoom; if (center) { const cell = sheet.getCellOffsets(center.x, center.y); @@ -134,19 +167,15 @@ export function moveViewport(options: { pixiApp.viewport.moveCenter(cell.x + cell.width / 2, cell.y + cell.height / 2); } else if (topLeft) { const cell = sheet.getCellOffsets(topLeft.x, topLeft.y); - if (!force && intersects.rectanglePoint(bounds, new Point(cell.x - adjust, cell.y - adjust))) return; - pixiApp.viewport.moveCorner(cell.x - adjust, cell.y - adjust); - } else if (pageUp) { - pixiApp.viewport.moveCorner(bounds.x, bounds.y - (bounds.height - adjust)); - } else if (pageDown) { - pixiApp.viewport.moveCorner(bounds.x, bounds.y + (bounds.height - adjust)); + if (!force && intersects.rectanglePoint(bounds, new Point(cell.x - adjustX, cell.y - adjustY))) return; + pixiApp.viewport.moveCorner(cell.x - adjustX, cell.y - adjustY); } pixiApp.viewportChanged(); } export function getShareUrlParams(): string { - let url = `x=${sheets.sheet.cursor.cursorPosition.x}&y=${sheets.sheet.cursor.cursorPosition.y}`; + let url = `x=${sheets.sheet.cursor.position.x}&y=${sheets.sheet.cursor.position.y}`; if (sheets.sheet !== sheets.getFirst()) { url += `&sheet=${sheets.sheet.name}`; } @@ -159,3 +188,20 @@ export function getShareUrlParams(): string { } return url; } + +// Moves the cursor up or down one page +export function pageUpDown(up: boolean) { + const cursorRect = pixiApp.cursor.cursorRectangle; + const { viewport } = pixiApp; + if (cursorRect) { + const distanceTopToCursorTop = cursorRect.top - viewport.top; + const newY = cursorRect.y + pixiApp.viewport.screenHeightInWorldPixels * (up ? -1 : 1); + const newRow = Math.max(1, sheets.sheet.getColumnRowFromScreen(0, newY).row); + const cursor = sheets.sheet.cursor; + cursor.moveTo(cursor.position.x, newRow, false); + const newCursorY = sheets.sheet.getRowY(newRow); + const gridHeadings = pixiApp.headings.headingSize.height / pixiApp.viewport.scale.y; + pixiApp.viewport.y = Math.min(gridHeadings, -newCursorY + distanceTopToCursorTop); + pixiApp.viewportChanged(); + } +} diff --git a/quadratic-client/src/app/gridGL/pixiApp/MomentumScrollDetector.ts b/quadratic-client/src/app/gridGL/pixiApp/MomentumScrollDetector.ts new file mode 100644 index 0000000000..75fb7fc9a2 --- /dev/null +++ b/quadratic-client/src/app/gridGL/pixiApp/MomentumScrollDetector.ts @@ -0,0 +1,58 @@ +//! This is a momentum scroll detector that uses a simple algorithm to detect if +//! the user is scrolling with momentum. It is not perfect and may not work in +//! all cases, but it is a good start. + +const SAMPLE_SIZE = 5; +const DELTA_THRESHOLD = 1; +const TIMING_TOLERANCE_MS = 50; + +interface SavedWheelEvent { + time: number; + delta: number; + deltaMode: number; +} + +export class MomentumScrollDetector { + private wheelEvents: SavedWheelEvent[] = []; + + constructor() { + window.addEventListener('wheel', this.handleWheel, { passive: true }); + } + + destroy() { + window.removeEventListener('wheel', this.handleWheel); + } + + handleWheel = (e: WheelEvent) => { + const now = Date.now(); + this.addEvent({ + time: now, + delta: Math.abs(e.deltaY), + deltaMode: e.deltaMode, + }); + }; + + addEvent(event: SavedWheelEvent) { + this.wheelEvents.push(event); + while (this.wheelEvents.length > SAMPLE_SIZE) { + this.wheelEvents.shift(); + } + } + + hasMomentumScroll() { + if (this.wheelEvents.length < SAMPLE_SIZE) return false; + + const hasSmoothing = this.wheelEvents.every((event, i, events) => { + if (i === 0) return true; + return event.delta <= events[i - 1].delta * DELTA_THRESHOLD; + }); + + const hasConsistentTiming = this.wheelEvents.every((event, i, events) => { + if (i === 0) return true; + const timeDelta = event.time - events[i - 1].time; + return timeDelta < TIMING_TOLERANCE_MS; + }); + + return hasSmoothing && hasConsistentTiming; + } +} diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts index 81d1d51f43..71f2e22169 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiApp.ts @@ -1,4 +1,4 @@ -import { editorInteractionStateDefault } from '@/app/atoms/editorInteractionStateAtom'; +import { defaultEditorInteractionState } from '@/app/atoms/editorInteractionStateAtom'; import { events } from '@/app/events/events'; import { copyToClipboardEvent, @@ -7,7 +7,7 @@ import { } from '@/app/grid/actions/clipboard/clipboard'; import { sheets } from '@/app/grid/controller/Sheets'; import { htmlCellsHandler } from '@/app/gridGL/HTMLGrid/htmlCells/htmlCellsHandler'; -import { AxesLines } from '@/app/gridGL/UI/AxesLines'; +import { Background } from '@/app/gridGL/UI/Background'; import { Cursor } from '@/app/gridGL/UI/Cursor'; import { GridLines } from '@/app/gridGL/UI/GridLines'; import { HtmlPlaceholders } from '@/app/gridGL/UI/HtmlPlaceholders'; @@ -22,17 +22,17 @@ import { CellsSheet } from '@/app/gridGL/cells/CellsSheet'; import { CellsSheets } from '@/app/gridGL/cells/CellsSheets'; import { Pointer } from '@/app/gridGL/interaction/pointer/Pointer'; import { ensureVisible } from '@/app/gridGL/interaction/viewportHelper'; +import { MomentumScrollDetector } from '@/app/gridGL/pixiApp/MomentumScrollDetector'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { Update } from '@/app/gridGL/pixiApp/Update'; -import { Viewport } from '@/app/gridGL/pixiApp/Viewport'; import { urlParams } from '@/app/gridGL/pixiApp/urlParams/urlParams'; -import { Coordinate } from '@/app/gridGL/types/size'; +import { Viewport } from '@/app/gridGL/pixiApp/viewport/Viewport'; import { getCSSVariableTint } from '@/app/helpers/convertColor'; import { isEmbed } from '@/app/helpers/isEmbed'; +import { JsCoordinate } from '@/app/quadratic-core-types'; import { colors } from '@/app/theme/colors'; import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer'; import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; -import { HEADING_SIZE } from '@/shared/constants/gridConstants'; import { sharedEvents } from '@/shared/sharedEvents'; import { Container, Graphics, Rectangle, Renderer, utils } from 'pixi.js'; import './pixiApp.css'; @@ -52,8 +52,13 @@ export class PixiApp { canvas!: HTMLCanvasElement; viewport!: Viewport; +<<<<<<< HEAD gridLines: GridLines; axesLines!: AxesLines; +======= + background: Background; + gridLines!: GridLines; +>>>>>>> origin/qa cursor!: Cursor; cellHighlights!: CellHighlights; multiplayerCursor!: UIMultiPlayerCursor; @@ -75,6 +80,7 @@ export class PixiApp { validations: UIValidations; renderer!: Renderer; + momentumDetector: MomentumScrollDetector; stage = new Container(); loading = true; destroyed = false; @@ -99,6 +105,8 @@ export class PixiApp { this.overHeadingsColumnsHeaders = new Container(); this.overHeadingsTableNames = new Container(); this.viewport = new Viewport(); + this.background = new Background(); + this.momentumDetector = new MomentumScrollDetector(); } init() { @@ -120,17 +128,17 @@ export class PixiApp { } // called after RenderText has no more updates to send - firstRenderComplete() { + firstRenderComplete = () => { if (this.waitingForFirstRender) { // perform a render to warm up the GPU this.cellsSheets.showAll(sheets.sheet.id); - pixiApp.renderer.render(pixiApp.stage); + this.renderer.render(this.stage); this.waitingForFirstRender(); this.waitingForFirstRender = undefined; } else { this.alreadyRendered = true; } - } + }; private initCanvas() { this.canvas = document.createElement('canvas'); @@ -154,10 +162,13 @@ export class PixiApp { // this holds the viewport's contents this.viewportContents = this.viewport.addChild(new Container()); + this.background = this.viewportContents.addChild(this.background); + // useful for debugging at viewport locations this.debug = this.viewportContents.addChild(new Graphics()); this.cellsSheets = this.viewportContents.addChild(this.cellsSheets); +<<<<<<< HEAD this.gridLines = this.viewportContents.addChild(this.gridLines); // this is a hack to ensure that table column names appears over the column @@ -166,6 +177,9 @@ export class PixiApp { this.viewportContents.addChild(gridHeadings.gridHeadingsRows); this.axesLines = this.viewportContents.addChild(new AxesLines()); +======= + this.gridLines = this.viewportContents.addChild(new GridLines()); +>>>>>>> origin/qa this.boxCells = this.viewportContents.addChild(new BoxCells()); this.multiplayerCursor = this.viewportContents.addChild(new UIMultiPlayerCursor()); this.cursor = this.viewportContents.addChild(new Cursor()); @@ -203,6 +217,22 @@ export class PixiApp { document.removeEventListener('cut', cutToClipboardEvent); } + // calculate sheet rectangle, without heading, factoring in scale + getViewportRectangle(): Rectangle { + const headingSize = this.headings.headingSize; + const scale = this.viewport.scale.x; + + const viewportBounds = this.viewport.getVisibleBounds(); + const rectangle = new Rectangle( + viewportBounds.left + headingSize.width / scale, + viewportBounds.top + headingSize.height / scale, + viewportBounds.width - headingSize.width / scale, + viewportBounds.height - headingSize.height / scale + ); + + return rectangle; + } + setViewportDirty(): void { this.viewport.dirty = true; } @@ -210,13 +240,17 @@ export class PixiApp { viewportChanged = (): void => { this.viewport.dirty = true; this.gridLines.dirty = true; - this.axesLines.dirty = true; this.headings.dirty = true; this.cursor.dirty = true; this.cellHighlights.dirty = true; this.cellsSheets?.cull(this.viewport.getVisibleBounds()); - sheets.sheet.cursor.viewport = this.viewport.lastViewport!; - multiplayer.sendViewport(this.saveMultiplayerViewport()); + + // we only set the viewport if update has completed firstRenderComplete + // (otherwise we can't get this.headings.headingSize) -- this is a hack + if (this.update.firstRenderComplete) { + sheets.sheet.cursor.viewport = this.viewport.lastViewport!; + multiplayer.sendViewport(this.saveMultiplayerViewport()); + } }; attach(parent: HTMLDivElement): void { @@ -246,7 +280,6 @@ export class PixiApp { const accentColor = getCSSVariableTint('primary'); this.accentColor = accentColor; this.gridLines.dirty = true; - this.axesLines.dirty = true; this.headings.dirty = true; this.cursor.dirty = true; this.cellHighlights.dirty = true; @@ -262,7 +295,6 @@ export class PixiApp { this.renderer.resize(width, height); this.viewport.resize(width, height); this.gridLines.dirty = true; - this.axesLines.dirty = true; this.headings.dirty = true; this.cursor.dirty = true; this.cellHighlights.dirty = true; @@ -272,7 +304,6 @@ export class PixiApp { // called before and after a render prepareForCopying(options?: { gridLines?: boolean; cull?: Rectangle }): Container { this.gridLines.visible = options?.gridLines ?? false; - this.axesLines.visible = false; this.cursor.visible = false; this.cellHighlights.visible = false; this.multiplayerCursor.visible = false; @@ -288,7 +319,6 @@ export class PixiApp { cleanUpAfterCopying(culled?: boolean): void { this.gridLines.visible = true; - this.axesLines.visible = true; this.cursor.visible = true; this.cellHighlights.visible = true; this.multiplayerCursor.visible = true; @@ -312,16 +342,13 @@ export class PixiApp { reset(): void { this.viewport.scale.set(1); - const { x, y } = this.getStartingViewport(); - this.viewport.position.set(x, y); - pixiAppSettings.setEditorInteractionState?.(editorInteractionStateDefault); + pixiAppSettings.setEditorInteractionState?.(defaultEditorInteractionState); } rebuild() { this.paused = true; this.viewport.dirty = true; this.gridLines.dirty = true; - this.axesLines.dirty = true; this.headings.dirty = true; this.cursor.dirty = true; this.cellHighlights.dirty = true; @@ -332,14 +359,6 @@ export class PixiApp { this.setViewportDirty(); } - getStartingViewport(): { x: number; y: number } { - if (pixiAppSettings.showHeadings) { - return { x: HEADING_SIZE + 1, y: HEADING_SIZE + 1 }; - } else { - return { x: 1, y: 1 }; - } - } - saveMultiplayerViewport(): string { const viewport = this.viewport; return JSON.stringify({ @@ -350,7 +369,7 @@ export class PixiApp { }); } - updateCursorPosition(visible: boolean | Coordinate = true) { + updateCursorPosition(visible: boolean | JsCoordinate = true) { this.cursor.dirty = true; this.cellHighlights.dirty = true; this.headings.dirty = true; diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index 511131daa7..3d5518c8a9 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -1,4 +1,6 @@ +import { AIAnalystState, defaultAIAnalystState } from '@/app/atoms/aiAnalystAtom'; import { CodeEditorState, defaultCodeEditorState } from '@/app/atoms/codeEditorAtom'; +<<<<<<< HEAD import { ContextMenuOptions, ContextMenuState, @@ -6,15 +8,19 @@ import { defaultContextMenuState, } from '@/app/atoms/contextMenuAtom'; import { EditorInteractionState, editorInteractionStateDefault } from '@/app/atoms/editorInteractionStateAtom'; +======= +import { defaultEditorInteractionState, EditorInteractionState } from '@/app/atoms/editorInteractionStateAtom'; +>>>>>>> origin/qa import { defaultGridPanMode, GridPanMode, PanMode } from '@/app/atoms/gridPanModeAtom'; import { defaultGridSettings, GridSettings } from '@/app/atoms/gridSettingsAtom'; import { defaultInlineEditor, InlineEditorState } from '@/app/atoms/inlineEditorAtom'; import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; +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'; +import { GlobalSnackbar, SnackbarOptions } from '@/shared/components/GlobalSnackbarProvider'; import { ApiTypes } from 'quadratic-shared/typesAndSchemas'; import { SetterOrUpdater } from 'recoil'; @@ -33,6 +39,10 @@ class PixiAppSettings { private lastSettings: GridSettings; private _panMode: PanMode; private _input: Input; + private waitingForSnackbar: { + message: JSX.Element | string; + options: SnackbarOptions; + }[] = []; // Keeps track of code editor content. This is used when moving code cells to // keep track of any unsaved changes, and keyboardCell. @@ -46,7 +56,7 @@ class PixiAppSettings { gridSettings = defaultGridSettings; setGridSettings?: SetterOrUpdater; - editorInteractionState = editorInteractionStateDefault; + editorInteractionState = defaultEditorInteractionState; setEditorInteractionState?: SetterOrUpdater; addGlobalSnackbar?: GlobalSnackbar['addGlobalSnackbar']; @@ -57,8 +67,13 @@ class PixiAppSettings { codeEditorState = defaultCodeEditorState; setCodeEditorState?: SetterOrUpdater; +<<<<<<< HEAD contextMenu = defaultContextMenuState; setContextMenu?: SetterOrUpdater; +======= + aiAnalystState = defaultAIAnalystState; + setAIAnalystState?: SetterOrUpdater; +>>>>>>> origin/qa constructor() { const settings = localStorage.getItem('viewSettings'); @@ -75,7 +90,7 @@ class PixiAppSettings { } destroy() { - window.removeEventListener('gridSettings', this.getSettings); + events.off('gridSettings', this.getSettings); } private getSettings = (): void => { @@ -86,7 +101,6 @@ class PixiAppSettings { this.settings = defaultGridSettings; } pixiApp.gridLines.dirty = true; - pixiApp.axesLines.dirty = true; pixiApp.headings.dirty = true; // todo: not sure what to do with this... @@ -143,12 +157,14 @@ class PixiAppSettings { this.setCodeEditorState = setCodeEditorState; } + updateAIAnalystState(aiAnalystState: AIAnalystState, setAIAnalystState: SetterOrUpdater): void { + this.aiAnalystState = aiAnalystState; + this.setAIAnalystState = setAIAnalystState; + } + get showGridLines(): boolean { return !this.settings.presentationMode && this.settings.showGridLines; } - get showGridAxes(): boolean { - return !this.settings.presentationMode && this.settings.showGridAxes; - } get showHeadings(): boolean { return !this.settings.presentationMode && this.settings.showHeadings; } @@ -187,7 +203,7 @@ class PixiAppSettings { } } - changeInput(input: boolean, initialValue?: string) { + changeInput(input: boolean, initialValue?: string, cursorMode?: CursorMode) { if (input === false) { multiplayer.sendEndCellEdit(); } @@ -200,8 +216,8 @@ class PixiAppSettings { pixiApp.cellsSheets.showLabel(this._input.x, this._input.y, this._input.sheetId, true); } if (input === true) { - const x = sheets.sheet.cursor.cursorPosition.x; - const y = sheets.sheet.cursor.cursorPosition.y; + const x = sheets.sheet.cursor.position.x; + const y = sheets.sheet.cursor.position.y; if (multiplayer.cellIsBeingEdited(x, y, sheets.sheet.id)) { this._input = { show: false }; } else { @@ -214,7 +230,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, cursorMode); } get input() { @@ -225,6 +241,7 @@ class PixiAppSettings { return this._panMode; } +<<<<<<< HEAD updateContextMenu(contextMenu: ContextMenuState, setContextMenu: SetterOrUpdater) { this.contextMenu = contextMenu; this.setContextMenu = setContextMenu; @@ -241,6 +258,22 @@ class PixiAppSettings { (this.contextMenu.type === ContextMenuType.Table || this.contextMenu.type === ContextMenuType.TableColumn) && this.contextMenu.rename ); +======= + setGlobalSnackbar(addGlobalSnackbar: GlobalSnackbar['addGlobalSnackbar']) { + this.addGlobalSnackbar = addGlobalSnackbar; + for (const snackbar of this.waitingForSnackbar) { + this.addGlobalSnackbar(snackbar.message, snackbar.options); + } + this.waitingForSnackbar = []; + } + + snackbar(message: string, options: SnackbarOptions) { + if (this.addGlobalSnackbar) { + this.addGlobalSnackbar(message, options); + } else { + this.waitingForSnackbar.push({ message, options }); + } +>>>>>>> origin/qa } } diff --git a/quadratic-client/src/app/gridGL/pixiApp/Update.ts b/quadratic-client/src/app/gridGL/pixiApp/Update.ts index 242f91e38a..1dea371da2 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/Update.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/Update.ts @@ -1,7 +1,3 @@ -import { events } from '@/app/events/events'; -import { sheets } from '@/app/grid/controller/Sheets'; -import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; -import { Point } from 'pixi.js'; import { debugShowFPS, debugShowWhyRendering } from '../../debugFlags'; import { FPS } from '../helpers/Fps'; import { @@ -17,15 +13,8 @@ import { thumbnail } from './thumbnail'; export class Update { private raf?: number; private fps?: FPS; - private lastViewportPosition: Point = new Point(); - // setting this to 0 ensures that on initial render, the viewport is properly scaled and updated - private lastViewportScale = 0; - - private lastScreenWidth = 0; - private lastScreenHeight = 0; - - private lastSheetId = ''; + firstRenderComplete = false; constructor() { if (debugShowFPS) { @@ -46,6 +35,7 @@ export class Update { } } +<<<<<<< HEAD sendRenderViewport() { const bounds = pixiApp.viewport.getVisibleBounds(); const scale = pixiApp.viewport.scale.x; @@ -86,6 +76,8 @@ export class Update { } } +======= +>>>>>>> origin/qa // update loop w/debug checks private update = (): void => { if (pixiApp.destroyed) return; @@ -100,12 +92,10 @@ export class Update { this.raf = requestAnimationFrame(this.update); return; } - - this.updateViewport(); + pixiApp.viewport.updateViewport(); let rendererDirty = pixiApp.gridLines.dirty || - pixiApp.axesLines.dirty || pixiApp.headings.dirty || pixiApp.boxCells.dirty || pixiApp.multiplayerCursor.dirty || @@ -120,7 +110,6 @@ export class Update { `dirty: ${[ pixiApp.viewport.dirty && 'viewport', pixiApp.gridLines.dirty && 'gridLines', - pixiApp.axesLines.dirty && 'axesLines', pixiApp.headings.dirty && 'headings', pixiApp.boxCells.dirty && 'boxCells', pixiApp.multiplayerCursor.dirty && 'multiplayerCursor', @@ -138,8 +127,6 @@ export class Update { debugTimeReset(); pixiApp.gridLines.update(); debugTimeCheck('[Update] gridLines'); - pixiApp.axesLines.update(); - debugTimeCheck('[Update] axesLines'); pixiApp.headings.update(pixiApp.viewport.dirty); debugTimeCheck('[Update] headings'); pixiApp.boxCells.update(); @@ -157,6 +144,8 @@ export class Update { pixiApp.cellsSheets.update(pixiApp.viewport.dirty); debugTimeCheck('[Update] cellsSheets'); pixiApp.validations.update(pixiApp.viewport.dirty); + debugTimeCheck('[Update] backgrounds'); + pixiApp.background.update(pixiApp.viewport.dirty); if (pixiApp.viewport.dirty || rendererDirty) { debugTimeReset(); @@ -172,6 +161,11 @@ export class Update { thumbnail.check(); } + if (!this.firstRenderComplete) { + this.firstRenderComplete = true; + pixiApp.viewport.loadViewport(); + } + this.raf = requestAnimationFrame(this.update); this.fps?.update(); }; diff --git a/quadratic-client/src/app/gridGL/pixiApp/copyAsPNG.ts b/quadratic-client/src/app/gridGL/pixiApp/copyAsPNG.ts index 2ad806f701..d6d8066ac1 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/copyAsPNG.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/copyAsPNG.ts @@ -19,58 +19,17 @@ export const copyAsPNG = async (): Promise => { }); } - let column, width, row, height; - const sheet = sheets.sheet; - const cursor = sheet.cursor; - if (cursor.multiCursor) { - const selection = cursor.getLargestMultiCursorRectangle(); - column = selection.left; - row = selection.top; - width = selection.width; - height = selection.height; - } else if (cursor.columnRow) { - if (cursor.columnRow.all) { - const bounds = sheet.getBounds(false); - if (bounds) { - column = bounds.left; - row = bounds.top; - width = bounds.width + 1; - height = bounds.height + 1; - } - } else if (cursor.columnRow.columns?.length) { - const columns = cursor.columnRow.columns.sort((a, b) => a - b); - const bounds = await quadraticCore.getColumnsBounds(sheet.id, columns[0], columns[columns.length - 1]); - column = columns[0]; - width = columns[columns.length - 1] - columns[0] + 1; - row = bounds?.min ?? 0; - height = bounds ? bounds.max - bounds.min + 1 : 1; - } else if (cursor.columnRow.rows?.length) { - const rows = cursor.columnRow.rows.sort((a, b) => a - b); - const bounds = await quadraticCore.getRowsBounds(sheet.id, rows[0], rows[rows.length - 1]); - row = rows[0]; - height = rows[rows.length - 1] - rows[0] + 1; - column = bounds?.min ?? 0; - width = bounds ? bounds.max - bounds.min + 1 : 1; - } - } else { - column = cursor.cursorPosition.x; - row = cursor.cursorPosition.y; - width = height = 0; - } - if (column === undefined || row === undefined || width === undefined || height === undefined) { - column = 0; - row = 0; - width = 1; - height = 1; - } - const rectangle = sheet.getScreenRectangle(column, row, width - 1, height - 1); + const rect = await quadraticCore.finiteRectFromSelection(sheets.sheet.cursor.save()); + if (!rect) return null; + + const screenRect = sheets.sheet.getScreenRectangle(rect.x, rect.y, rect.width, rect.height); // captures bottom-right border size - rectangle.width += borderSize * 2; - rectangle.height += borderSize * 2; + screenRect.width += borderSize * 2; + screenRect.height += borderSize * 2; - let imageWidth = rectangle.width * resolution, - imageHeight = rectangle.height * resolution; + let imageWidth = screenRect.width * resolution, + imageHeight = screenRect.height * resolution; if (Math.max(imageWidth, imageHeight) > maxTextureSize) { if (imageWidth > imageHeight) { imageHeight = imageHeight * (maxTextureSize / imageWidth); @@ -88,8 +47,8 @@ export const copyAsPNG = async (): Promise => { // todo // app.cells.drawCells(app.sheet, rectangle, false); const transform = new Matrix(); - transform.translate(-rectangle.x + borderSize / 2, -rectangle.y + borderSize / 2); - const scale = imageWidth / (rectangle.width * resolution); + transform.translate(-screenRect.x + borderSize / 2, -screenRect.y + borderSize / 2); + const scale = imageWidth / (screenRect.width * resolution); transform.scale(scale, scale); renderer.render(pixiApp.viewportContents, { transform }); pixiApp.cleanUpAfterCopying(); diff --git a/quadratic-client/src/app/gridGL/pixiApp/messages.tsx b/quadratic-client/src/app/gridGL/pixiApp/messages.tsx new file mode 100644 index 0000000000..5131f4f380 --- /dev/null +++ b/quadratic-client/src/app/gridGL/pixiApp/messages.tsx @@ -0,0 +1,20 @@ +import { DOCUMENTATION_NEGATIVE_OFFSETS } from '@/shared/constants/urls'; + +export const messages: Record = { + negative_offsets: ( + + Quadratic no longer supports zero or negative rows and columns. The data in your file was shifted. You may need to + update Python or Javascript code to reflect this change. See{' '} + + this blog post + {' '} + for more information. + + ), +}; diff --git a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsDev.ts b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsDev.ts index cdcd499b40..fac0a26684 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsDev.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsDev.ts @@ -2,8 +2,6 @@ import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; -import { SheetCursorSave } from '@/app/grid/sheet/SheetCursor'; -import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { CodeCellLanguage } from '@/app/quadratic-core-types'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; @@ -13,7 +11,7 @@ const URL_STATE_PARAM = 'state'; const WAIT_FOR_SET_EDITOR_INTERACTION_STATE_TIMEOUT_MS = 100; interface SheetState { - cursor: SheetCursorSave; + cursor: string; viewport?: IViewportTransformState; } @@ -111,8 +109,8 @@ export class UrlParamsDev { private loadCodeAndRun() { if (this.state.insertAndRunCodeInNewSheet) { - const x = 0; - const y = 0; + const x = 1; + const y = 1; const sheetId = sheets.current; const { language, codeString } = this.state.insertAndRunCodeInNewSheet; @@ -166,8 +164,7 @@ export class UrlParamsDev { events.on('changeSheet', this.updateSheet); events.on('codeEditor', this.updateCode); events.on('validation', this.updateValidation); - pixiApp.viewport.on('moved', this.updateCursorViewport); - pixiApp.viewport.on('zoomed', this.updateCursorViewport); + events.on('viewportChangedReady', this.updateCursorViewport); } private updateCursorViewport = () => { diff --git a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts index 25901ad188..aaa9f94ace 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts @@ -31,11 +31,7 @@ export class UrlParamsUser { const x = parseInt(params.get('x') ?? ''); const y = parseInt(params.get('y') ?? ''); if (!isNaN(x) && !isNaN(y)) { - sheets.sheet.cursor.changePosition({ - cursorPosition: { x, y }, - keyboardMovePosition: { x, y }, - ensureVisible: true, - }); + sheets.sheet.cursor.moveTo(x, y); } } @@ -50,7 +46,7 @@ export class UrlParamsUser { if (!pixiAppSettings.setEditorInteractionState) { throw new Error('Expected setEditorInteractionState to be set in urlParams.loadCode'); } - const { x, y } = sheets.sheet.cursor.cursorPosition; + const { x, y } = sheets.sheet.cursor.position; pixiAppSettings.setCodeEditorState?.((prev) => ({ ...prev, showCodeEditor: true, @@ -99,7 +95,7 @@ export class UrlParamsUser { // otherwise we use the normal cursor else { - const cursor = sheets.sheet.cursor.cursorPosition; + const cursor = sheets.sheet.cursor.position; url.set('x', cursor.x.toString()); url.set('y', cursor.y.toString()); if (sheets.sheet !== sheets.getFirst()) { diff --git a/quadratic-client/src/app/gridGL/pixiApp/viewport/Decelerate.ts b/quadratic-client/src/app/gridGL/pixiApp/viewport/Decelerate.ts new file mode 100644 index 0000000000..e1f23098da --- /dev/null +++ b/quadratic-client/src/app/gridGL/pixiApp/viewport/Decelerate.ts @@ -0,0 +1,286 @@ +import { Plugin, Viewport } from 'pixi-viewport'; + +export const DECELERATE_OUT_OF_BOUNDS_FACTOR = 0.8; +const MIN_SPEED = 0.1; + +export interface IDecelerateOptions { + /** + * Percent to decelerate after movement. This should be between 0 and 1, exclusive. + * + * @default 0.95 + */ + friction?: number; + + /** + * Percent to decelerate when past boundaries (only applicable when viewport.bounce() is active) + * + * @default 0.8 + */ + bounce?: number; + + /** + * Minimum velocity before stopping/reversing acceleration + * + * @default 0.01 + */ + minSpeed?: number; +} + +/** Viewport position snapshot that's saved by {@link DeceleratePlugin} to estimate panning velocity. */ +export interface IDecelerateSnapshot { + /** x-coordinate of the viewport. */ + x: number; + + /** y-coordinate of the viewport. */ + y: number; + + /** Time at which this snapshot was taken. */ + time: number; +} + +const DEFAULT_DECELERATE_OPTIONS: Required = { + friction: 0.98, + bounce: 0.8, + minSpeed: MIN_SPEED, +}; + +/** + * Time period of decay (1 frame) + * + * @internal + * @ignore + */ +const TP = 16; + +/** + * Plugin to decelerate viewport velocity smoothly after panning ends. + * + * @public + */ +export class Decelerate extends Plugin { + /** Options used to initialize this plugin. */ + public readonly options: Required; + + /** + * x-component of the velocity of viewport provided by this plugin, at the current time. + * + * This is measured in px/frame, where a frame is normalized to 16 milliseconds. + */ + public x!: number | null; + + /** + * y-component of the velocity of the viewport provided by this plugin, at the current time. + * + * This is measured in px/frame, where a frame is normalized to 16 milliseconds. + */ + public y!: number | null; + + /** + * The decay factor for the x-component of the viewport. + * + * The viewport's velocity decreased by this amount each 16 milliseconds. + */ + public percentChangeX!: number; + + /** + * The decay factor for the y-component of the viewport. + * + * The viewport's velocity decreased by this amount each 16 milliseconds. + */ + public percentChangeY!: number; + + /** Saved list of recent viewport position snapshots, to estimate velocity. */ + protected saved: Array; + + /** The time since the user released panning of the viewport. */ + protected timeSinceRelease: number; + + /** + * This is called by {@link Viewport.decelerate}. + */ + constructor(parent: Viewport, options: IDecelerateOptions = {}) { + super(parent); + + this.options = Object.assign({}, DEFAULT_DECELERATE_OPTIONS, options); + this.saved = []; + this.timeSinceRelease = 0; + + this.reset(); + this.parent.on('moved', (data) => this.handleMoved(data)); + + this.x = null; + this.y = null; + } + + public wheel(): boolean { + this.saved = []; + this.x = this.y = null; + return false; + } + + public down(): boolean { + this.saved = []; + this.x = this.y = null; + + return false; + } + + public isActive(): boolean { + return !!(this.x || this.y); + } + + public move(): boolean { + if (this.paused) { + return false; + } + + const count = this.parent.input.count(); + + if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true))) { + this.saved.push({ x: this.parent.x, y: this.parent.y, time: performance.now() }); + + if (this.saved.length > 60) { + this.saved.splice(0, 30); + } + } + + // Silently recording viewport positions + return false; + } + + /** Listener to viewport's "moved" event. */ + protected handleMoved(e: any): void { + if (this.saved.length) { + const last = this.saved[this.saved.length - 1]; + + if (e.type === 'clamp-x' && e.original) { + if (last.x === e.original.x) { + last.x = this.parent.x; + } + } else if (e.type === 'clamp-y' && e.original) { + if (last.y === e.original.y) { + last.y = this.parent.y; + } + } + } + } + + public up(): boolean { + if (this.parent.input.count() === 0 && this.saved.length) { + const now = performance.now(); + + for (const save of this.saved) { + if (save.time >= now - 100) { + const time = now - save.time; + + this.x = (this.parent.x - save.x) / time; + this.y = (this.parent.y - save.y) / time; + this.percentChangeX = this.percentChangeY = this.options.friction; + this.timeSinceRelease = 0; + break; + } + } + } + + return false; + } + + /** + * Manually activate deceleration, starting from the (x, y) velocity components passed in the options. + * + * @param {object} options + * @param {number} [options.x] - Specify x-component of initial velocity. + * @param {number} [options.y] - Specify y-component of initial velocity. + */ + public activate(options: { x?: number; y?: number }): void { + options = options || {}; + + if (typeof options.x !== 'undefined') { + this.x = options.x; + this.percentChangeX = this.options.friction; + } + if (typeof options.y !== 'undefined') { + this.y = options.y; + this.percentChangeY = this.options.friction; + } + } + + public update(elapsed: number): void { + if (this.paused) { + return; + } + + /* + * See https://github.com/davidfig/pixi-viewport/issues/271 for math. + * + * The viewport velocity (this.x, this.y) decays exponentially by the the decay factor + * (this.percentChangeX, this.percentChangeY) each frame. This velocity function is integrated + * to calculate the displacement. + */ + + const moved = this.x !== null || this.y !== null; + + const ti = this.timeSinceRelease; + const tf = this.timeSinceRelease + elapsed; + + if (this.x !== null) { + // add additional percent change if we're in the negative direction + let percentChangeX = this.percentChangeX; + if (this.parent.x > 0) { + percentChangeX = DECELERATE_OUT_OF_BOUNDS_FACTOR; + } + + const k = this.percentChangeX; + const lnk = Math.log(k); + + // Apply velocity delta on the viewport x-coordinate. + this.parent.x += ((this.x * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP)); + + // Apply decay on x-component of velocity + this.x *= Math.pow(percentChangeX, elapsed / TP); + } + if (this.y !== null) { + // add additional percent change if we're in the negative direction + let percentChangeY = this.percentChangeY; + if (this.parent.y > 0) { + percentChangeY = DECELERATE_OUT_OF_BOUNDS_FACTOR; + } + + const k = percentChangeY; + const lnk = Math.log(k); + + // Apply velocity delta on the viewport y-coordinate. + this.parent.y += ((this.y * TP) / lnk) * (Math.pow(k, tf / TP) - Math.pow(k, ti / TP)); + + // Apply decay on y-component of velocity + this.y *= Math.pow(percentChangeY, elapsed / TP); + } + + this.timeSinceRelease += elapsed; + + // End decelerate velocity once it goes under a certain amount of precision. + if (this.x !== null && this.y !== null) { + if (this.x && this.y) { + if (Math.abs(this.x) < this.options.minSpeed && Math.abs(this.y) < this.options.minSpeed) { + this.x = 0; + this.y = 0; + } + } else { + if (Math.abs(this.x || 0) < this.options.minSpeed) { + this.x = 0; + } + if (Math.abs(this.y || 0) < this.options.minSpeed) { + this.y = 0; + } + } + + if (moved) { + this.parent.emit('moved', { viewport: this.parent, type: 'decelerate' }); + } + } + } + + public reset(): void { + this.x = this.y = null; + } +} diff --git a/quadratic-client/src/app/gridGL/pixiApp/viewport/Drag.ts b/quadratic-client/src/app/gridGL/pixiApp/viewport/Drag.ts new file mode 100644 index 0000000000..2737bff8af --- /dev/null +++ b/quadratic-client/src/app/gridGL/pixiApp/viewport/Drag.ts @@ -0,0 +1,478 @@ +//! Cloned from pixi-viewport Drag plugin to add clamping only for changes when +//! dragging (zoom has different clamping). + +import { SCALE_OUT_OF_BOUNDS_SCROLL } from '@/app/gridGL/pixiApp/viewport/Wheel'; +import { JsCoordinate } from '@/app/quadratic-core-types'; +import { Decelerate, Plugin, Viewport } from 'pixi-viewport'; +import { InteractionEvent, Point } from 'pixi.js'; + +/** Options for {@link Drag}. */ +export interface IDragOptions { + /** + * direction to drag + * + * @default "all" + */ + direction?: string; + + /** + * whether click to drag is active + * + * @default true + */ + pressDrag?: boolean; + + /** + * Use wheel to scroll in direction (unless wheel plugin is active) + * + * @default true + */ + wheel?: boolean; + + /** + * number of pixels to scroll with each wheel spin + * + * @default 1 + */ + wheelScroll?: number; + + /** + * reverse the direction of the wheel scroll + * + * @default false + */ + reverse?: boolean; + + /** + * clamp wheel(to avoid weird bounce with mouse wheel). Can be 'x' or 'y' or `true`. + * + * @default false + */ + clampWheel?: boolean | string; + + /** + * where to place world if too small for screen + * + * @default "center" + */ + underflow?: string; + + /** + * factor to multiply drag to increase the speed of movement + * + * @default 1 + */ + factor?: number; + + /** + * Changes which mouse buttons trigger drag. + * + * Use: 'all', 'left', right' 'middle', or some combination, like, 'middle-right'; you may want to set + * `viewport.options.disableOnContextMenu` if you want to use right-click dragging. + * + * @default "all" + */ + mouseButtons?: 'all' | string; + + /** + * Array containing {@link key|https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code} codes of + * keys that can be pressed for the drag to be triggered, e.g.: ['ShiftLeft', 'ShiftRight'}. + * + * @default null + */ + keyToPress?: string[] | null; + + /** + * Ignore keyToPress for touch events. + * + * @default false + */ + ignoreKeyToPressOnTouch?: boolean; + + /** + * Scaling factor for non-DOM_DELTA_PIXEL scrolling events. + * + * @default 20 + */ + lineHeight?: number; + + /** + * Swap x and y axes when scrolling. + * + * @default false + */ + wheelSwapAxes?: boolean; +} + +const DEFAULT_DRAG_OPTIONS: Required = { + direction: 'all', + pressDrag: true, + wheel: true, + wheelScroll: 1, + reverse: false, + clampWheel: false, + underflow: 'center', + factor: 1, + mouseButtons: 'all', + keyToPress: null, + ignoreKeyToPressOnTouch: false, + lineHeight: 20, + wheelSwapAxes: false, +}; + +/** + * Plugin to enable panning/dragging of the viewport to move around. + * + * @public + */ +export class Drag extends Plugin { + /** Options used to initialize this plugin, cannot be modified later. */ + public readonly options: Readonly>; + + /** Flags when viewport is moving. */ + protected moved: boolean; + + /** Factor to apply from {@link IDecelerateOptions}'s reverse. */ + protected reverse: 1 | -1; + + /** Holds whether dragging is enabled along the x-axis. */ + protected xDirection: boolean; + + /** Holds whether dragging is enabled along the y-axis. */ + protected yDirection: boolean; + + /** Flags whether the keys required to drag are pressed currently. */ + protected keyIsPressed: boolean; + + /** Holds whether the left, center, and right buttons are required to pan. */ + protected mouse!: [boolean, boolean, boolean]; + + /** Underflow factor along x-axis */ + protected underflowX!: -1 | 0 | 1; + + /** Underflow factor along y-axis */ + protected underflowY!: -1 | 0 | 1; + + /** Last pointer position while panning. */ + protected last?: JsCoordinate | null; + + /** The ID of the pointer currently panning the viewport. */ + protected current?: number; + + /** Array of event-handlers for window */ + private windowEventHandlers: Array<{ event: string; handler: (e: any) => void }> = []; + + /** + * This is called by {@link Viewport.drag}. + */ + constructor(parent: Viewport, options = {}) { + super(parent); + + this.options = Object.assign({}, DEFAULT_DRAG_OPTIONS, options); + this.moved = false; + this.reverse = this.options.reverse ? 1 : -1; + this.xDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'x'; + this.yDirection = !this.options.direction || this.options.direction === 'all' || this.options.direction === 'y'; + this.keyIsPressed = false; + + this.parseUnderflow(); + this.mouseButtons(this.options.mouseButtons); + + if (this.options.keyToPress) { + this.handleKeyPresses(this.options.keyToPress); + } + } + + /** + * Handles keypress events and set the keyIsPressed boolean accordingly + * + * @param {array} codes - key codes that can be used to trigger drag event + */ + protected handleKeyPresses(codes: string[]): void { + const keydownHandler = (e: KeyboardEvent) => { + if (codes.includes(e.code)) { + this.keyIsPressed = true; + } + }; + + const keyupHandler = (e: KeyboardEvent) => { + if (codes.includes(e.code)) { + this.keyIsPressed = false; + } + }; + + this.addWindowEventHandler('keyup', keyupHandler); + this.addWindowEventHandler('keydown', keydownHandler); + } + + private addWindowEventHandler(event: string, handler: (e: any) => void): void { + if (typeof window === 'undefined') return; + window.addEventListener(event, handler); + this.windowEventHandlers.push({ event, handler }); + } + + public override destroy(): void { + if (typeof window === 'undefined') return; + this.windowEventHandlers.forEach(({ event, handler }) => { + window.removeEventListener(event, handler); + }); + } + + /** + * initialize mousebuttons array + * @param {string} buttons + */ + protected mouseButtons(buttons: string): void { + if (!buttons || buttons === 'all') { + this.mouse = [true, true, true]; + } else { + this.mouse = [buttons.indexOf('left') !== -1, buttons.indexOf('middle') !== -1, buttons.indexOf('right') !== -1]; + } + } + + protected parseUnderflow(): void { + const clamp = this.options.underflow.toLowerCase(); + + if (clamp === 'center') { + this.underflowX = 0; + this.underflowY = 0; + } else { + if (clamp.includes('left')) { + this.underflowX = -1; + } else if (clamp.includes('right')) { + this.underflowX = 1; + } else { + this.underflowX = 0; + } + if (clamp.includes('top')) { + this.underflowY = -1; + } else if (clamp.includes('bottom')) { + this.underflowY = 1; + } else { + this.underflowY = 0; + } + } + } + + /** + * @param {PIXI.FederatedPointerEvent} event + * @returns {boolean} + */ + protected checkButtons(event: InteractionEvent): boolean { + const isMouse = event.data.pointerType === 'mouse'; + const count = this.parent.input.count(); + + if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true))) { + if (!isMouse || this.mouse[event.data.button]) { + return true; + } + } + + return false; + } + + /** + * @param {PIXI.FederatedPointerEvent} event + * @returns {boolean} + */ + protected checkKeyPress(event: InteractionEvent): boolean { + return ( + !this.options.keyToPress || + this.keyIsPressed || + (this.options.ignoreKeyToPressOnTouch && event.data.pointerType === 'touch') + ); + } + + public down(event: InteractionEvent): boolean { + if (this.paused || !this.options.pressDrag) { + return false; + } + if (this.checkButtons(event) && this.checkKeyPress(event)) { + this.last = { x: event.data.global.x, y: event.data.global.y }; + this.current = event.data.pointerId; + + return true; + } + this.last = null; + + return false; + } + + get active(): boolean { + return this.moved; + } + + public move(event: InteractionEvent): boolean { + if (this.paused || !this.options.pressDrag) { + return false; + } + if (this.last && this.current === event.data.pointerId) { + const x = event.data.global.x; + const y = event.data.global.y; + const count = this.parent.input.count(); + + if (count === 1 || (count > 1 && !this.parent.plugins.get('pinch', true))) { + const distX = x - this.last.x; + const distY = y - this.last.y; + + if ( + this.moved || + (this.xDirection && this.parent.input.checkThreshold(distX)) || + (this.yDirection && this.parent.input.checkThreshold(distY)) + ) { + const newPoint = { x, y }; + const deltaX = newPoint.x - this.last.x; + this.parent.x += deltaX * (deltaX > 1 && this.parent.x > 0 ? SCALE_OUT_OF_BOUNDS_SCROLL : 1); + const deltaY = newPoint.y - this.last.y; + this.parent.y += deltaY * (deltaY > 1 && this.parent.x > 0 ? SCALE_OUT_OF_BOUNDS_SCROLL : 1); + this.last = newPoint; + if (!this.moved) { + this.parent.emit('drag-start', { + event, + screen: new Point(this.last.x, this.last.y), + world: this.parent.toWorld(new Point(this.last.x, this.last.y)), + viewport: this.parent, + }); + } + this.moved = true; + this.parent.emit('moved', { viewport: this.parent, type: 'drag' }); + + return true; + } + } else { + this.moved = false; + } + } + + return false; + } + + public up(event: InteractionEvent): boolean { + if (this.paused) { + return false; + } + + const touches = this.parent.input.touches; + + if (touches.length === 1) { + const pointer = touches[0]; + + if (pointer.last) { + this.last = { x: pointer.last.x, y: pointer.last.y }; + this.current = pointer.id; + } + this.moved = false; + + return true; + } else if (this.last) { + if (this.moved) { + const screen = new Point(this.last.x, this.last.y); + + this.parent.emit('drag-end', { + event, + screen, + world: this.parent.toWorld(screen), + viewport: this.parent, + }); + this.last = null; + this.moved = false; + + return true; + } + } + + return false; + } + + public wheel(event: WheelEvent): boolean { + if (this.paused) { + return false; + } + + if (this.options.wheel) { + const wheel = this.parent.plugins.get('wheel', true); + + if (!wheel || (!wheel.options.wheelZoom && !event.ctrlKey)) { + const step = event.deltaMode ? this.options.lineHeight : 1; + + const deltas = [event.deltaX, event.deltaY]; + const [deltaX, deltaY] = this.options.wheelSwapAxes ? deltas.reverse() : deltas; + + if (this.xDirection) { + this.parent.x += deltaX * step * this.options.wheelScroll * this.reverse; + } + if (this.yDirection) { + this.parent.y += deltaY * step * this.options.wheelScroll * this.reverse; + } + if (this.options.clampWheel) { + this.clamp(); + } + this.parent.emit('wheel-scroll', this.parent); + this.parent.emit('moved', { viewport: this.parent, type: 'wheel' }); + if (!this.parent.options.passiveWheel) { + event.preventDefault(); + } + if (this.parent.options.stopPropagation) { + event.stopPropagation(); + } + + return true; + } + } + + return false; + } + + public resume(): void { + this.last = null; + this.paused = false; + } + + public clamp(): void { + const decelerate: Partial = this.parent.plugins.get('decelerate', true) || {}; + + if (this.options.clampWheel !== 'y') { + if (this.parent.screenWorldWidth < this.parent.screenWidth) { + switch (this.underflowX) { + case -1: + this.parent.x = 0; + break; + case 1: + this.parent.x = this.parent.screenWidth - this.parent.screenWorldWidth; + break; + default: + this.parent.x = (this.parent.screenWidth - this.parent.screenWorldWidth) / 2; + } + } else if (this.parent.left < 0) { + this.parent.x = 0; + decelerate.x = 0; + } else if (this.parent.right > this.parent.worldWidth) { + this.parent.x = -this.parent.worldWidth * this.parent.scale.x + this.parent.screenWidth; + decelerate.x = 0; + } + } + if (this.options.clampWheel !== 'x') { + if (this.parent.screenWorldHeight < this.parent.screenHeight) { + switch (this.underflowY) { + case -1: + this.parent.y = 0; + break; + case 1: + this.parent.y = this.parent.screenHeight - this.parent.screenWorldHeight; + break; + default: + this.parent.y = (this.parent.screenHeight - this.parent.screenWorldHeight) / 2; + } + } else { + if (this.parent.top < 0) { + this.parent.y = 0; + decelerate.y = 0; + } + if (this.parent.bottom > this.parent.worldHeight) { + this.parent.y = -this.parent.worldHeight * this.parent.scale.y + this.parent.screenHeight; + decelerate.y = 0; + } + } + } + } +} diff --git a/quadratic-client/src/app/gridGL/pixiApp/viewport/Viewport.ts b/quadratic-client/src/app/gridGL/pixiApp/viewport/Viewport.ts new file mode 100644 index 0000000000..42e2d70ca2 --- /dev/null +++ b/quadratic-client/src/app/gridGL/pixiApp/viewport/Viewport.ts @@ -0,0 +1,244 @@ +import { events } from '@/app/events/events'; +import { sheets } from '@/app/grid/controller/Sheets'; +import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp'; +import { Decelerate } from '@/app/gridGL/pixiApp/viewport/Decelerate'; +import { Drag } from '@/app/gridGL/pixiApp/viewport/Drag'; +import { HORIZONTAL_SCROLL_KEY, Wheel, ZOOM_KEY } from '@/app/gridGL/pixiApp/viewport/Wheel'; +import { renderWebWorker } from '@/app/web-workers/renderWebWorker/renderWebWorker'; +import { Viewport as PixiViewport } from 'pixi-viewport'; +import { Point, Rectangle } from 'pixi.js'; +import { isMobile } from 'react-device-detect'; + +const MULTIPLAYER_VIEWPORT_EASE_TIME = 100; +const MINIMUM_VIEWPORT_SCALE = 0.01; +const MAXIMUM_VIEWPORT_SCALE = 10; +const WHEEL_ZOOM_PERCENT = 1.5; + +const WAIT_TO_SNAP_TIME = 200; +const SNAPPING_TIME = 150; + +type SnapState = 'waiting' | 'snapping' | undefined; + +export class Viewport extends PixiViewport { + private lastViewportPosition: Point = new Point(); + + // setting this to 0 ensures that on initial render, the viewport is properly scaled and updated + private lastViewportScale = 0; + + private lastScreenWidth = 0; + private lastScreenHeight = 0; + + private lastSheetId = ''; + + private waitForZoomEnd = false; + + private snapState?: SnapState; + private snapTimeout?: number; + + constructor() { + super(); + this.plugins.add( + 'drag', + new Drag(this, { + pressDrag: true, + wheel: false, // handled by Wheel plugin below + keyToPress: ['Space'], + }) + ); + this.plugins.add('decelerate', new Decelerate(this)); + this.pinch().clampZoom({ + minScale: MINIMUM_VIEWPORT_SCALE, + maxScale: MAXIMUM_VIEWPORT_SCALE, + }); + this.plugins.add( + 'wheel', + new Wheel(this, { + trackpadPinch: true, + wheelZoom: true, + percent: WHEEL_ZOOM_PERCENT, + keyToPress: [...ZOOM_KEY, ...HORIZONTAL_SCROLL_KEY], + }) + ); + if (!isMobile) { + this.plugins.add( + 'drag-middle-mouse', + new Drag(this, { + pressToDrag: true, + mouseButtons: 'middle', + wheel: 'false', + }) + ); + } + + // hack to ensure pointermove works outside of canvas + this.off('pointerout'); + + this.on('moved', this.viewportChanged); + this.on('zoomed', this.viewportChanged); + this.on('wait-for-zoom-end', this.handleWaitForZoomEnd); + this.on('zoom-end', this.handleZoomEnd); + this.on('pinch-start', this.handleWaitForZoomEnd); + this.on('pinch-end', this.handleZoomEnd); + this.on('snap-end', this.handleSnapEnd); + } + + private viewportChanged = () => { + events.emit('viewportChanged'); + }; + + destroy() { + this.off('moved', this.viewportChanged); + this.off('zoomed', this.viewportChanged); + this.off('wait-for-zoom-end', this.handleWaitForZoomEnd); + this.off('zoom-end', this.handleZoomEnd); + this.off('pinch-start', this.handleWaitForZoomEnd); + this.off('pinch-end', this.handleZoomEnd); + this.off('snap-end', this.handleSnapEnd); + } + + loadViewport() { + const vp = sheets.sheet.cursor.viewport; + if (vp) { + this.position.set(vp.x, vp.y); + this.scale.set(vp.scaleX, vp.scaleY); + this.dirty = true; + } + } + + loadMultiplayerViewport(options: { x: number; y: number; bounds: Rectangle; sheetId: string }): void { + const { x, y, bounds } = options; + let width: number | undefined; + let height: number | undefined; + + // ensure the entire follow-ee's bounds is visible to the current user + if (this.screenWidth / this.screenHeight > bounds.width / bounds.height) { + height = bounds.height; + } else { + width = bounds.width; + } + if (sheets.current !== options.sheetId) { + sheets.current = options.sheetId; + this.moveCenter(new Point(x, y)); + } else { + this.animate({ + position: new Point(x, y), + width, + height, + removeOnInterrupt: true, + time: MULTIPLAYER_VIEWPORT_EASE_TIME, + }); + } + this.dirty = true; + } + + // resets the viewport to start + reset() { + const headings = pixiApp.headings.headingSize; + this.position.set(headings.width, headings.height); + this.dirty = true; + } + + sendRenderViewport() { + const bounds = this.getVisibleBounds(); + const scale = this.scale.x; + renderWebWorker.updateViewport(sheets.sheet.id, bounds, scale); + } + + private startSnap = () => { + const headings = pixiApp.headings.headingSize; + let x: number; + let y: number; + let snap = false; + if (this.x > headings.width) { + x = -headings.width / this.scaled; + snap = true; + } else { + x = -this.x / this.scaled; + } + if (this.y > headings.height) { + y = -headings.height / this.scaled; + snap = true; + } else { + y = -this.y / this.scaled; + } + if (snap) { + this.snap(x, y, { + topLeft: true, + time: SNAPPING_TIME, + ease: 'easeOutSine', + removeOnComplete: true, + interrupt: true, + }); + this.snapState = 'snapping'; + } else { + this.snapState = undefined; + } + }; + + updateViewport(): void { + let dirty = false; + if (this.lastViewportScale !== this.scale.x) { + this.lastViewportScale = this.scale.x; + dirty = true; + + // this is used to trigger changes to ZoomDropdown + events.emit('zoom', this.scale.x); + } + if (this.lastViewportPosition.x !== this.x || this.lastViewportPosition.y !== this.y) { + this.lastViewportPosition.x = this.x; + this.lastViewportPosition.y = this.y; + dirty = true; + } + if (this.lastScreenWidth !== this.screenWidth || this.lastScreenHeight !== this.screenHeight) { + this.lastScreenWidth = this.screenWidth; + this.lastScreenHeight = this.screenHeight; + dirty = true; + } + if (this.lastSheetId !== sheets.sheet.id) { + this.lastSheetId = sheets.sheet.id; + dirty = true; + } + if (dirty) { + pixiApp.viewportChanged(); + this.sendRenderViewport(); + + // signals to react that the viewport has changed (so it can update any + // related positioning) + events.emit('viewportChangedReady'); + + // Clear both timeout and state when interrupting a waiting snap + this.snapTimeout = undefined; + this.snapState = undefined; + } else if (!this.waitForZoomEnd) { + if (!this.snapState) { + const headings = pixiApp.headings.headingSize; + if (this.x > headings.width || this.y > headings.height) { + if (pixiApp.momentumDetector.hasMomentumScroll()) { + this.startSnap(); + } else { + this.snapTimeout = Date.now(); + this.snapState = 'waiting'; + } + } + } else if (this.snapState === 'waiting' && this.snapTimeout) { + if (Date.now() - this.snapTimeout > WAIT_TO_SNAP_TIME) { + this.startSnap(); + } + } + } + } + + private handleWaitForZoomEnd = () => { + this.waitForZoomEnd = true; + }; + + private handleZoomEnd = () => { + this.waitForZoomEnd = false; + this.startSnap(); + }; + + private handleSnapEnd = () => { + this.snapState = undefined; + this.snapTimeout = undefined; + }; +} diff --git a/quadratic-client/src/app/gridGL/pixiOverride/Wheel.ts b/quadratic-client/src/app/gridGL/pixiApp/viewport/Wheel.ts similarity index 75% rename from quadratic-client/src/app/gridGL/pixiOverride/Wheel.ts rename to quadratic-client/src/app/gridGL/pixiApp/viewport/Wheel.ts index 31d56a775a..ff59b01754 100644 --- a/quadratic-client/src/app/gridGL/pixiOverride/Wheel.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/viewport/Wheel.ts @@ -1,7 +1,16 @@ +//! Cloned from pixi-viewport Wheel plugin. + +import { events } from '@/app/events/events'; +import { CELL_HEIGHT } from '@/shared/constants/gridConstants'; import { isMac } from '@/shared/utils/isMac'; import { IPointData, Point } from '@pixi/math'; import { Plugin, Viewport } from 'pixi-viewport'; +export const SCALE_OUT_OF_BOUNDS_SCROLL = 0.1; + +const MAX_RUBBER_BAND_GAP = 50; +const RUBBER_BAND_DECELERATION_POWER = 5; + /** Options for {@link Wheel}. */ export interface IWheelOptions { /** @@ -107,6 +116,11 @@ export class Wheel extends Plugin { /** Flags whether the keys required to horizontal scrolling are currently pressed. */ protected horizontalScrollKeyIsPressed: boolean; + /** Flags whether the viewport is currently zooming using ctrl key + wheel. */ + protected currentlyZooming: boolean; + + protected headingSize: { width: number; height: number } = { width: CELL_HEIGHT, height: CELL_HEIGHT }; + /** * This is called by {@link Viewport.wheel}. */ @@ -115,16 +129,25 @@ export class Wheel extends Plugin { this.options = Object.assign({}, DEFAULT_WHEEL_OPTIONS, options); this.zoomKeyIsPressed = false; this.horizontalScrollKeyIsPressed = false; - + this.currentlyZooming = false; if (this.options.keyToPress) { this.handleKeyPresses(this.options.keyToPress); } window.addEventListener('blur', this.handleBlur); + window.addEventListener('pointerup', this.checkAndEmitZoomEnd); + window.addEventListener('pointerdown', this.checkAndEmitZoomEnd); + window.addEventListener('pointerout', this.checkAndEmitZoomEnd); + window.addEventListener('pointerleave', this.checkAndEmitZoomEnd); + + events.on('headingSize', (width, height) => { + this.headingSize = { width, height }; + }); } private handleBlur = (): void => { this.zoomKeyIsPressed = false; this.horizontalScrollKeyIsPressed = false; + this.checkAndEmitZoomEnd(); }; /** @@ -140,6 +163,7 @@ export class Wheel extends Plugin { if (this.isHorizontalScrollKey(e.code)) { this.horizontalScrollKeyIsPressed = true; } + this.checkAndEmitZoomEnd(); }); window.addEventListener('keyup', (e) => { @@ -149,6 +173,7 @@ export class Wheel extends Plugin { if (this.isHorizontalScrollKey(e.code)) { this.horizontalScrollKeyIsPressed = false; } + this.checkAndEmitZoomEnd(); }); } @@ -176,6 +201,18 @@ export class Wheel extends Plugin { return HORIZONTAL_SCROLL_KEY.includes(key); } + protected emitWaitForZoomEnd = (): void => { + this.currentlyZooming = true; + this.parent.emit('wait-for-zoom-end'); + }; + + protected checkAndEmitZoomEnd = (): void => { + if (this.currentlyZooming) { + this.currentlyZooming = false; + this.parent.emit('zoom-end'); + } + }; + public update(): void { if (this.smoothing) { const point = this.smoothingCenter; @@ -301,6 +338,7 @@ export class Wheel extends Plugin { this.parent.scale.y *= change; } this.parent.emit('zoomed', { viewport: this.parent, type: 'wheel' }); + this.emitWaitForZoomEnd(); const clamp = this.parent.plugins.get('clamp-zoom', true); if (clamp) { @@ -325,16 +363,60 @@ export class Wheel extends Plugin { } else if (e.ctrlKey && this.options.trackpadPinch) { this.pinch(e, adjust); } else { - const step = 1; - const deltas = [e.deltaX, e.deltaY]; - let [deltaX, deltaY] = deltas; + const [deltaX, deltaY] = deltas; + + const { x: viewportX, y: viewportY } = this.parent; + const { width: headingWidth, height: headingHeight } = this.headingSize; + + const stepX = deltaX < 0 && viewportX > 0 ? SCALE_OUT_OF_BOUNDS_SCROLL : 1; + const stepY = deltaY < 0 && viewportY > 0 ? SCALE_OUT_OF_BOUNDS_SCROLL : 1; + + // Calculate the scroll amount + let dx = (this.horizontalScrollKeyIsPressed && !isMac ? deltaY : deltaX) * stepX * -1; + let dy = (this.horizontalScrollKeyIsPressed && !isMac ? 0 : deltaY) * stepY * -1; + + // Calculate actual position of the viewport after scrolling + const nextX = viewportX + dx - headingWidth; + const nextY = viewportY + dy - headingHeight; + + if (viewportX >= headingWidth) { + // going beyond the heading, decelerate + const factorX = this.getDecelerationFactor(nextX); + dx *= factorX; + } else if (nextX > 0) { + // snap to the edge + dx -= nextX; + } + + if (viewportY >= headingHeight) { + // going beyond the heading, decelerate + const factorY = this.getDecelerationFactor(nextY); + dy *= factorY; + } else if (nextY > 0) { + // snap to the edge + dy -= nextY; + } + + this.parent.x = viewportX + dx; + this.parent.y = viewportY + dy; - this.parent.x += (this.horizontalScrollKeyIsPressed && !isMac ? deltaY : deltaX) * step * -1; - this.parent.y += deltaY * step * -1 * (this.horizontalScrollKeyIsPressed && !isMac ? 0 : 1); this.parent.emit('wheel-scroll', this.parent); this.parent.emit('moved', { viewport: this.parent, type: 'wheel' }); } return !this.parent.options.passiveWheel; } + + private getDecelerationFactor(value: number): number { + // No deceleration needed if the value is less than or equal to 0 + if (value <= 0) return 1; + + // Normalize the gap to be between 0 and 1 + const normalizedGap = Math.min(value / MAX_RUBBER_BAND_GAP, 1); + + // Calculate the deceleration factor using a power function + const factor = Math.pow(1 - normalizedGap, RUBBER_BAND_DECELERATION_POWER); + + return factor; + } } diff --git a/quadratic-client/src/app/gridGL/types/codeCell.ts b/quadratic-client/src/app/gridGL/types/codeCell.ts index e1e3109033..b8e2c4c334 100644 --- a/quadratic-client/src/app/gridGL/types/codeCell.ts +++ b/quadratic-client/src/app/gridGL/types/codeCell.ts @@ -1,8 +1,7 @@ -import { Coordinate } from '@/app/gridGL/types/size'; -import { CodeCellLanguage } from '@/app/quadratic-core-types'; +import { CodeCellLanguage, JsCoordinate } from '@/app/quadratic-core-types'; export interface CodeCell { sheetId: string; - pos: Coordinate; + pos: JsCoordinate; language: CodeCellLanguage; } diff --git a/quadratic-client/src/app/gridGL/types/links.ts b/quadratic-client/src/app/gridGL/types/links.ts index b38f073c36..1d3a070812 100644 --- a/quadratic-client/src/app/gridGL/types/links.ts +++ b/quadratic-client/src/app/gridGL/types/links.ts @@ -1,7 +1,7 @@ -import { Coordinate } from '@/app/gridGL/types/size'; +import { JsCoordinate } from '@/app/quadratic-core-types'; import { Rectangle } from 'pixi.js'; export interface Link { - pos: Coordinate; + pos: JsCoordinate; textRectangle: Rectangle; } diff --git a/quadratic-client/src/app/gridGL/types/size.ts b/quadratic-client/src/app/gridGL/types/size.ts index 7b2428126c..423ed69f9e 100644 --- a/quadratic-client/src/app/gridGL/types/size.ts +++ b/quadratic-client/src/app/gridGL/types/size.ts @@ -1,35 +1,21 @@ +import type { JsCoordinate } from '@/app/quadratic-core-types'; +import type { Rectangle } from 'pixi.js'; + export interface SheetPosTS { x: number; y: number; sheetId: string; } -export interface Coordinate { - x: number; - y: number; -} - export interface Size { width: number; height: number; } -export interface Rectangle { - x: number; - y: number; - width: number; - height: number; -} - -export function coordinateEqual(a: Coordinate, b: Coordinate): boolean { +export function coordinateEqual(a: JsCoordinate, b: JsCoordinate): boolean { return a.x === b.x && a.y === b.y; } -export interface MinMax { - min: number; - max: number; -} - export interface DrawRects { rects: Rectangle[]; tint: number; diff --git a/quadratic-client/src/app/helpers/codeCellLanguage.ts b/quadratic-client/src/app/helpers/codeCellLanguage.ts index 0d6ee14227..a70034cb08 100644 --- a/quadratic-client/src/app/helpers/codeCellLanguage.ts +++ b/quadratic-client/src/app/helpers/codeCellLanguage.ts @@ -1,6 +1,8 @@ import { CodeCellLanguage } from '@/app/quadratic-core-types'; -const codeCellsById = { +export type CodeCellType = 'Python' | 'Javascript' | 'Formula' | 'Connection'; + +export const codeCellsById = { Formula: { id: 'Formula', label: 'Formula', type: undefined }, Javascript: { id: 'Javascript', label: 'JavaScript', type: undefined }, Python: { id: 'Python', label: 'Python', type: undefined }, @@ -33,14 +35,14 @@ export const getCodeCell = (language?: CodeCellLanguage) => { return undefined; }; -export const getLanguage = (language?: CodeCellLanguage) => { +export const getLanguage = (language?: CodeCellLanguage): CodeCellType => { if (typeof language === 'string') { return language; } else if (typeof language === 'object') { return 'Connection'; } - return 'Formula'; + return 'Python'; }; // For languages that monaco supports, see https://github.com/microsoft/monaco-editor/tree/c321d0fbecb50ab8a5365fa1965476b0ae63fc87/src/basic-languages @@ -61,7 +63,7 @@ export const getLanguageForMonaco = (language?: CodeCellLanguage): string => { } } - return 'formula'; + return 'python'; }; export const getConnectionUuid = (language?: CodeCellLanguage): string | undefined => { diff --git a/quadratic-client/src/app/helpers/formulaNotation.ts b/quadratic-client/src/app/helpers/formulaNotation.ts index ad7d8c2368..8aa28f7cee 100644 --- a/quadratic-client/src/app/helpers/formulaNotation.ts +++ b/quadratic-client/src/app/helpers/formulaNotation.ts @@ -1,29 +1,12 @@ -import { sheets } from '@/app/grid/controller/Sheets'; -import { Coordinate } from '@/app/gridGL/types/size'; -import { CursorCell } from '@/app/gridGL/UI/Cursor'; -import { StringId } from '@/app/helpers/getKey'; -import { CellRefId } from '@/app/ui/menus/CodeEditor/hooks/useEditorCellHighlights'; +import type { JsCellsAccessed, JsCoordinate, Span } from '@/app/quadratic-core-types'; -export function getCoordinatesFromStringId(stringId: StringId): [number, number] { - // required for type inference - const [x, y] = stringId.split(',').map((val) => parseInt(val)); - return [x, y]; -} - -export interface CellPosition { +interface CellPosition { x: { type: 'Relative' | 'Absolute'; coord: number }; y: { type: 'Relative' | 'Absolute'; coord: number }; sheet?: string; } -export type Span = { start: number; end: number }; - -export type CellRefCoord = { - x: { type: 'Relative' | 'Absolute'; coord: number }; - y: { type: 'Relative' | 'Absolute'; coord: number }; -}; - -export type CellRef = +type CellRef = | { type: 'CellRange'; start: CellPosition; @@ -46,48 +29,49 @@ export type ParseFormulaReturnType = { }[]; }; -export function getCellFromFormulaNotation(sheetId: string, cellRefId: CellRefId, editorCursorPosition: Coordinate) { - const isSimpleCell = !cellRefId.includes(':'); +export function parseFormulaReturnToCellsAccessed( + parseFormulaReturn: ParseFormulaReturnType, + codeCellPos: JsCoordinate, + codeCellSheetId: string +): JsCellsAccessed[] { + const isAbsolute = (type: 'Relative' | 'Absolute') => type === 'Absolute'; + const jsCellsAccessed: JsCellsAccessed[] = []; - if (isSimpleCell) { - const [x, y] = getCoordinatesFromStringId(cellRefId); - return getCellWithLimit(sheetId, editorCursorPosition, y, x); - } - const [startCell, endCell] = cellRefId.split(':') as [StringId, StringId]; - const [startCellX, startCellY] = getCoordinatesFromStringId(startCell); - const [endCellX, endCellY] = getCoordinatesFromStringId(endCell); - - return { - startCell: getCellWithLimit(sheetId, editorCursorPosition, startCellY, startCellX), - endCell: getCellWithLimit(sheetId, editorCursorPosition, endCellY, endCellX), - }; -} + for (const cellRef of parseFormulaReturn.cell_refs) { + const start = cellRef.cell_ref.type === 'CellRange' ? cellRef.cell_ref.start : cellRef.cell_ref.pos; + const end = cellRef.cell_ref.type === 'CellRange' ? cellRef.cell_ref.end : cellRef.cell_ref.pos; + const cellsAccessed: JsCellsAccessed = { + sheetId: cellRef.sheet ?? codeCellSheetId, + ranges: [ + { + range: { + start: { + col: { + coord: BigInt(isAbsolute(start.x.type) ? start.x.coord : start.x.coord + codeCellPos.x), + is_absolute: isAbsolute(start.x.type), + }, + row: { + coord: BigInt(isAbsolute(start.y.type) ? start.y.coord : start.y.coord + codeCellPos.y), + is_absolute: isAbsolute(start.y.type), + }, + }, + end: { + col: { + coord: BigInt(isAbsolute(end.x.type) ? end.x.coord : end.x.coord + codeCellPos.x), + is_absolute: isAbsolute(end.x.type), + }, + row: { + coord: BigInt(isAbsolute(end.y.type) ? end.y.coord : end.y.coord + codeCellPos.y), + is_absolute: isAbsolute(end.y.type), + }, + }, + }, + }, + ], + }; -function getCellWithLimit( - sheetId: string, - editorCursorPosition: Coordinate, - row: number, - column: number, - offset = 20000 -): CursorCell { - // getCell is slow with more than 9 digits, so limit if column or row is > editorCursorPosition + an offset - // If it's a single cell to be highlighted, it won't be visible anyway, and if it's a range - // It will highlight beyond the what's visible in the viewport - return sheets.sheet.getCellOffsets( - Math.min(column, editorCursorPosition.x + offset), - Math.min(row, editorCursorPosition.y + offset) - ); -} + jsCellsAccessed.push(cellsAccessed); + } -export function isCellRangeTypeGuard(obj: any): obj is { startCell: CursorCell; endCell: CursorCell } { - return ( - typeof obj === 'object' && - obj !== null && - 'startCell' in obj && - typeof obj.startCell === 'object' && - obj.startCell !== null && - 'endCell' in obj && - typeof obj.endCell === 'object' && - obj.endCell !== null - ); + return jsCellsAccessed; } diff --git a/quadratic-client/src/app/keyboard/defaults.ts b/quadratic-client/src/app/keyboard/defaults.ts index 44087411f7..ca567c321b 100644 --- a/quadratic-client/src/app/keyboard/defaults.ts +++ b/quadratic-client/src/app/keyboard/defaults.ts @@ -273,7 +273,7 @@ export const defaultShortcuts: ActionShortcut = { mac: [[MacModifiers.Cmd, MacModifiers.Shift, Keys.ArrowRight]], windows: [[WindowsModifiers.Ctrl, WindowsModifiers.Shift, Keys.ArrowRight]], }, - [Action.GotoA0]: { + [Action.GotoA1]: { mac: [[MacModifiers.Cmd, Keys.Home]], windows: [[WindowsModifiers.Ctrl, Keys.Home]], }, @@ -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]], @@ -353,4 +357,8 @@ export const defaultShortcuts: ActionShortcut = { mac: [[MacModifiers.Ctrl, Keys.Semicolon]], windows: [[WindowsModifiers.Ctrl, WindowsModifiers.Shift, Keys.Semicolon]], }, + [Action.ToggleAIAnalyst]: { + mac: [[MacModifiers.Cmd, Keys.K]], + windows: [[WindowsModifiers.Ctrl, Keys.K]], + }, }; diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index db76e9bc85..3ad96d7f03 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -1,84 +1,103 @@ // This file is automatically generated by quadratic-core/src/bin/export_types.rs // Do not modify it manually. +export interface A1Selection { sheet_id: SheetId, cursor: Pos, ranges: Array, } export interface ArraySize { w: number, h: number, } export type Axis = "X" | "Y"; export type BorderSelection = "all" | "inner" | "outer" | "horizontal" | "vertical" | "left" | "top" | "right" | "bottom" | "clear"; +export type BorderSide = "Top" | "Bottom" | "Left" | "Right"; export interface BorderStyle { color: Rgba, line: CellBorderLine, } export interface BorderStyleCell { top: BorderStyleTimestamp | null, bottom: BorderStyleTimestamp | null, left: BorderStyleTimestamp | null, right: BorderStyleTimestamp | null, } export interface BorderStyleTimestamp { color: Rgba, line: CellBorderLine, timestamp: SmallTimestamp, } +export interface CellA1Response { cells: Array, x: bigint, y: bigint, w: bigint, h: bigint, } export type CellAlign = "center" | "left" | "right"; export type CellBorderLine = "line1" | "line2" | "line3" | "dotted" | "dashed" | "double" | "clear"; export interface CellFormatSummary { bold: boolean | null, italic: boolean | null, commas: boolean | null, textColor: string | null, fillColor: string | null, align: CellAlign | null, verticalAlign: CellVerticalAlign | null, wrap: CellWrap | null, dateTime: string | null, cellType: CellType | null, underline: boolean | null, strikeThrough: boolean | null, } -export interface CellRef { sheet: string | null, x: CellRefCoord, y: CellRefCoord, } -export type CellRefCoord = { "type": "Relative", "coord": bigint } | { "type": "Absolute", "coord": bigint }; +export interface CellRefCoord { coord: bigint, is_absolute: boolean, } +export type CellRefRange = { range: RefRangeBounds, }; +export interface CellRefRangeEnd { col: CellRefCoord, row: CellRefCoord, } export type CellVerticalAlign = "top" | "middle" | "bottom"; export type CellWrap = "overflow" | "wrap" | "clip"; export type CodeCellLanguage = "Python" | "Formula" | { "Connection": { kind: ConnectionKind, id: string, } } | "Javascript" | "Import"; export interface ColumnRow { column: number, row: number, } export type ConnectionKind = "POSTGRES" | "MYSQL" | "MSSQL" | "SNOWFLAKE"; export type DateTimeRange = { "DateRange": [bigint | null, bigint | null] } | { "DateEqual": Array } | { "DateNotEqual": Array } | { "TimeRange": [number | null, number | null] } | { "TimeEqual": Array } | { "TimeNotEqual": Array }; +<<<<<<< HEAD export interface Duration { months: number, seconds: number, } export interface Format { align: CellAlign | null, vertical_align: CellVerticalAlign | null, wrap: CellWrap | null, numeric_format: NumericFormat | null, numeric_decimals: number | null, numeric_commas: boolean | null, bold: boolean | null, italic: boolean | null, text_color: string | null, fill_color: string | null, date_time: string | null, underline: boolean | null, strike_through: boolean | null, render_size: RenderSize | null, } +======= +export interface Format { align: CellAlign | null, vertical_align: CellVerticalAlign | null, wrap: CellWrap | null, numeric_format: NumericFormat | null, numeric_decimals: number | null, numeric_commas: boolean | null, bold: boolean | null, italic: boolean | null, text_color: string | null, fill_color: string | null, render_size: RenderSize | null, date_time: string | null, underline: boolean | null, strike_through: boolean | null, } +>>>>>>> origin/qa export type GridBounds = { "type": "empty" } | { "type": "nonEmpty" } & Rect; -export interface Instant { seconds: number, } -export interface JsBorderHorizontal { color: Rgba, line: CellBorderLine, x: bigint, y: bigint, width: bigint, } -export interface JsBorderVertical { color: Rgba, line: CellBorderLine, x: bigint, y: bigint, height: bigint, } -export interface JsBordersSheet { all: BorderStyleCell | null, columns: Record | null, rows: Record | null, horizontal: Array | null, vertical: Array | null, } +export interface JsBordersSheet { horizontal: Array | null, vertical: Array | null, } +export interface JsBorderHorizontal { color: Rgba, line: CellBorderLine, x: bigint, y: bigint, width: bigint | null, unbounded: boolean, } +export interface JsBorderVertical { color: Rgba, line: CellBorderLine, x: bigint, y: bigint, height: bigint | null, unbounded: boolean, } +export interface JsCellsAccessed { sheetId: string, ranges: Array, } export interface JsCellValue { value: string, kind: string, } +export interface JsCellValuePos { value: string, kind: string, pos: string, } +export interface JsCellValuePosAIContext { sheet_name: string, rect_origin: string, rect_width: number, rect_height: number, starting_rect_values: Array>, } export interface JsClipboard { plainText: string, html: string, } +<<<<<<< HEAD export interface JsCodeCell { x: bigint, y: bigint, code_string: string, language: CodeCellLanguage, std_out: string | null, std_err: string | null, evaluation_result: string | null, spill_error: Array | null, return_info: JsReturnInfo | null, cells_accessed: Array | null, } export interface JsCodeResult { transaction_id: string, success: boolean, std_out: string | null, std_err: string | null, line_number: number | null, output_value: Array | null, output_array: Array>> | null, output_display_type: string | null, cancel_compute: boolean | null, chart_pixel_output: [number, number] | null, } export interface JsDataTableColumnHeader { name: string, display: boolean, valueIndex: number, } +======= +export interface JsCodeCell { x: bigint, y: bigint, code_string: string, language: CodeCellLanguage, std_out: string | null, std_err: string | null, evaluation_result: string | null, spill_error: Array | null, return_info: JsReturnInfo | null, cells_accessed: Array | null, } +export interface JsCodeResult { transaction_id: string, success: boolean, std_out: string | null, std_err: string | null, line_number: number | null, output_value: Array | null, output_array: Array>> | null, output_display_type: string | null, cancel_compute: boolean | null, } +export interface JsCoordinate { x: number, y: number, } +>>>>>>> origin/qa export interface JsGetCellResponse { x: bigint, y: bigint, value: string, type_name: string, } export interface JsHtmlOutput { sheet_id: string, x: bigint, y: bigint, html: string | null, w: number | null, h: number | null, } export interface JsNumber { decimals: number | null, commas: boolean | null, format: NumericFormat | null, } export interface JsOffset { column: number | null, row: number | null, size: number, } -export interface JsPos { x: bigint, y: bigint, } export interface JsRenderCell { x: bigint, y: bigint, value: string, language?: CodeCellLanguage, align?: CellAlign, verticalAlign?: CellVerticalAlign, wrap?: CellWrap, bold?: boolean, italic?: boolean, textColor?: string, special?: JsRenderCellSpecial, number?: JsNumber, underline?: boolean, strikeThrough?: boolean, } export type JsRenderCellSpecial = "Chart" | "SpillError" | "RunError" | "Logical" | "Checkbox" | "List" | "TableColumnHeader"; export interface JsRenderCodeCell { x: number, y: number, w: number, h: number, language: CodeCellLanguage, state: JsRenderCodeCellState, spill_error: Array | null, name: string, columns: Array, first_row_header: boolean, show_header: boolean, sort: Array | null, alternating_colors: boolean, } export type JsRenderCodeCellState = "NotYetRun" | "RunError" | "SpillError" | "Success" | "HTML" | "Image"; export interface JsRenderFill { x: bigint, y: bigint, w: number, h: number, color: string, } +export interface JsReturnInfo { line_number: number | null, output_type: string | null, } export interface JsRowHeight { row: bigint, height: number, } -export interface JsSheetFill { columns: Array<[bigint, [string, bigint]]>, rows: Array<[bigint, [string, bigint]]>, all: string | null, } +export interface JsSheetFill { x: number, y: number, w: number | null, h: number | null, color: string, } +export interface JsSummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export interface JsValidationWarning { x: bigint, y: bigint, validation: string | null, style: ValidationStyle | null, } export type JumpDirection = "Up" | "Down" | "Left" | "Right"; +<<<<<<< HEAD export interface MinMax { min: number, max: number, } +======= +>>>>>>> origin/qa export type NumberRange = { "Range": [number | null, number | null] } | { "Equal": Array } | { "NotEqual": Array }; export interface NumericFormat { type: NumericFormatKind, symbol: string | null, } export type NumericFormatKind = "NUMBER" | "CURRENCY" | "PERCENTAGE" | "EXPONENTIAL"; export type PasteSpecial = "None" | "Values" | "Formats"; -export interface Placement { index: number, position: number, size: number, } export interface Pos { x: bigint, y: bigint, } -export type RangeRef = { "type": "RowRange", start: CellRefCoord, end: CellRefCoord, sheet: string | null, } | { "type": "ColRange", start: CellRefCoord, end: CellRefCoord, sheet: string | null, } | { "type": "CellRange", start: CellRef, end: CellRef, } | { "type": "Cell", pos: CellRef, }; +export interface RefRangeBounds { start: CellRefRangeEnd, end: CellRefRangeEnd, } export interface Rect { min: Pos, max: Pos, } +export interface RenderSize { w: string, h: string, } export interface Rgba { red: number, green: number, blue: number, alpha: number, } export interface RunError { span: Span | null, msg: RunErrorMsg, } export type RunErrorMsg = { "CodeRunError": string } | "Spill" | { "Unimplemented": string } | "UnknownError" | { "InternalError": string } | { "Unterminated": string } | { "Expected": { expected: string, got: string | null, } } | { "Unexpected": string } | { "TooManyArguments": { func_name: string, max_arg_count: number, } } | { "MissingRequiredArgument": { func_name: string, arg_name: string, } } | "BadFunctionName" | "BadCellReference" | "BadNumber" | { "BadOp": { op: string, ty1: string, ty2: string | null, use_duration_instead: boolean, } } | "NaN" | { "ExactArraySizeMismatch": { expected: ArraySize, got: ArraySize, } } | { "ExactArrayAxisMismatch": { axis: Axis, expected: number, got: number, } } | { "ArrayAxisMismatch": { axis: Axis, expected: number, got: number, } } | "EmptyArray" | "NonRectangularArray" | "NonLinearArray" | "ArrayTooBig" | "CircularReference" | "Overflow" | "DivideByZero" | "NegativeExponent" | "NotANumber" | "Infinity" | "IndexOutOfBounds" | "NoMatch" | "InvalidArgument"; -export interface ScreenRect { x: number, y: number, w: number, h: number, } export interface SearchOptions { case_sensitive?: boolean, whole_cell?: boolean, search_code?: boolean, sheet_id?: string, } -export interface Selection { sheet_id: SheetId, x: bigint, y: bigint, rects: Array | null, rows: Array | null, columns: Array | null, all: boolean, } export interface SheetBounds { sheet_id: string, bounds: GridBounds, bounds_without_formatting: GridBounds, } export interface SheetId { id: string, } export interface SheetInfo { sheet_id: string, name: string, order: string, color: string | null, offsets: string, bounds: GridBounds, bounds_without_formatting: GridBounds, } export interface SheetPos { x: bigint, y: bigint, sheet_id: SheetId, } export interface SheetRect { min: Pos, max: Pos, sheet_id: SheetId, } +<<<<<<< HEAD export type SortDirection = "Ascending" | "Descending" | "None"; export interface DataTableSort { column_index: number, direction: SortDirection, } +======= +export type SmallTimestamp = number; +>>>>>>> origin/qa export interface Span { start: number, end: number, } -export interface SummarizeSelectionResult { count: bigint, sum: number | null, average: number | null, } export type TextCase = { "CaseInsensitive": Array } | { "CaseSensitive": Array }; export type TextMatch = { "Exactly": TextCase } | { "Contains": TextCase } | { "NotContains": TextCase } | { "TextLength": { min: number | null, max: number | null, } }; export type TransactionName = "Unknown" | "ResizeColumn" | "ResizeRow" | "ResizeRows" | "Autocomplete" | "SetBorders" | "SetCells" | "SetFormats" | "SetDataTableAt" | "CutClipboard" | "PasteClipboard" | "SetCode" | "RunCode" | "FlattenDataTable" | "SwitchDataTableKind" | "GridToDataTable" | "DataTableMeta" | "DataTableMutations" | "DataTableFirstRowAsHeader" | "Import" | "SetSheetMetadata" | "SheetAdd" | "SheetDelete" | "DuplicateSheet" | "MoveCells" | "Validation" | "ManipulateColumnRow"; export interface TransientResize { row: bigint | null, column: bigint | null, old_size: number, new_size: number, } -export interface Validation { id: string, selection: Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } +export interface Validation { id: string, selection: A1Selection, rule: ValidationRule, message: ValidationMessage, error: ValidationError, } export interface ValidationDateTime { ignore_blank: boolean, require_date: boolean, require_time: boolean, prohibit_date: boolean, prohibit_time: boolean, ranges: Array, } -export interface ValidationDisplay { checkbox: boolean, list: boolean, } -export interface ValidationDisplaySheet { columns: Array<[bigint, ValidationDisplay]> | null, rows: Array<[bigint, ValidationDisplay]> | null, all: ValidationDisplay | null, } export interface ValidationError { show: boolean, style: ValidationStyle, title: string | null, message: string | null, } export interface ValidationList { source: ValidationListSource, ignore_blank: boolean, drop_down: boolean, } -export type ValidationListSource = { "Selection": Selection } | { "List": Array }; +export type ValidationListSource = { "Selection": A1Selection } | { "List": Array }; export interface ValidationLogical { show_checkbox: boolean, ignore_blank: boolean, } export interface ValidationMessage { show: boolean, title: string | null, message: string | null, } export interface ValidationNumber { ignore_blank: boolean, ranges: Array, } diff --git a/quadratic-client/src/app/theme/colors.ts b/quadratic-client/src/app/theme/colors.ts index f2ad1d2a3b..161f519698 100644 --- a/quadratic-client/src/app/theme/colors.ts +++ b/quadratic-client/src/app/theme/colors.ts @@ -23,6 +23,7 @@ export const colors = { movingCells: 0x2463eb, gridBackground: 0xffffff, + gridBackgroundOutOfBounds: 0xfdfdfd, independence: 0x5d576b, headerBackgroundColor: 0xffffff, diff --git a/quadratic-client/src/app/ui/QuadraticSidebar.tsx b/quadratic-client/src/app/ui/QuadraticSidebar.tsx index ed2b277da5..40bf2e770d 100644 --- a/quadratic-client/src/app/ui/QuadraticSidebar.tsx +++ b/quadratic-client/src/app/ui/QuadraticSidebar.tsx @@ -1,6 +1,7 @@ import { isAvailableBecauseCanEditFile, isAvailableBecauseFileLocationIsAccessibleAndWriteable } from '@/app/actions'; import { Action } from '@/app/actions/actions'; import { defaultActionSpec } from '@/app/actions/defaultActionsSpec'; +import { incrementAiAnalystOpenCount, showAIAnalystAtom } from '@/app/atoms/aiAnalystAtom'; import { codeEditorShowCodeEditorAtom } from '@/app/atoms/codeEditorAtom'; import { editorInteractionStateShowCommandPaletteAtom, @@ -13,7 +14,9 @@ import { KeyboardSymbols } from '@/app/helpers/keyboardSymbols'; import { ThemePickerMenu } from '@/app/ui/components/ThemePickerMenu'; import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; import { KernelMenu } from '@/app/ui/menus/BottomBar/KernelMenu'; +import { useRootRouteLoaderData } from '@/routes/_root'; import { + AIIcon, CodeCellOutlineOff, CodeCellOutlineOn, DatabaseIcon, @@ -32,13 +35,18 @@ import { Link } from 'react-router-dom'; import { useRecoilState, useRecoilValue } from 'recoil'; const toggleCodeEditor = defaultActionSpec[Action.ShowCellTypeMenu]; +const toggleAIChat = defaultActionSpec[Action.ToggleAIAnalyst]; export const QuadraticSidebar = () => { const isRunningAsyncAction = useRecoilValue(editorInteractionStateShowIsRunningAsyncActionAtom); + const [showAIAnalyst, setShowAIAnalyst] = useRecoilState(showAIAnalystAtom); const showCodeEditor = useRecoilValue(codeEditorShowCodeEditorAtom); const [showCellTypeOutlines, setShowCellTypeOutlines] = useRecoilState(showCellTypeOutlinesAtom); const [showConnectionsMenu, setShowConnectionsMenu] = useRecoilState(editorInteractionStateShowConnectionsMenuAtom); const [showCommandPalette, setShowCommandPalette] = useRecoilState(editorInteractionStateShowCommandPaletteAtom); + + const { isAuthenticated } = useRootRouteLoaderData(); + const isAvailableArgs = useIsAvailableArgs(); const canEditFile = isAvailableBecauseCanEditFile(isAvailableArgs); const canDoTeamsStuff = isAvailableBecauseFileLocationIsAccessibleAndWriteable(isAvailableArgs); @@ -65,6 +73,26 @@ export const QuadraticSidebar = () => {
+ {canEditFile && isAuthenticated && ( + + { + setShowAIAnalyst((prevShowAiAnalyst) => { + // if it's hidden and therefore being opened by the user, count it! + if (prevShowAiAnalyst === false) { + incrementAiAnalystOpenCount(); + } + + return !prevShowAiAnalyst; + }); + }} + > + + + + )} + {canEditFile && ( hasPermissionToEditFile(permissions), [permissions]); + + // Show negative_offsets warning if present in URL (the result of an imported + // file) + useEffect(() => { + const url = new URLSearchParams(window.location.search); + if (url.has('negative_offsets')) { + setTimeout(() => + pixiAppSettings.snackbar('File automatically updated for A1 notation.', { + stayOpen: true, + button: { + title: 'Learn more', + callback: () => window.open(COMMUNITY_A1_FILE_UPDATE_URL, '_blank'), + }, + }) + ); + url.delete('negative_offsets'); + window.history.replaceState({}, '', `${window.location.pathname}${url.toString() ? `?${url}` : ''}`); + } + }, []); return (
+ {canEditFile && isAuthenticated && } {!presentationMode && } @@ -84,14 +105,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/AIUserMessageForm.tsx b/quadratic-client/src/app/ui/components/AIUserMessageForm.tsx new file mode 100644 index 0000000000..9df4541dac --- /dev/null +++ b/quadratic-client/src/app/ui/components/AIUserMessageForm.tsx @@ -0,0 +1,210 @@ +import { SelectAIModelMenu } from '@/app/ai/components/SelectAIModelMenu'; +import { KeyboardSymbols } from '@/app/helpers/keyboardSymbols'; +import ConditionalWrapper from '@/app/ui/components/ConditionalWrapper'; +import { AIAnalystContext } from '@/app/ui/menus/AIAnalyst/AIAnalystContext'; +import { ArrowUpwardIcon, BackspaceIcon, EditIcon } from '@/shared/components/Icons'; +import { AI_SECURITY } from '@/shared/constants/urls'; +import { Button } from '@/shared/shadcn/ui/button'; +import { Textarea } from '@/shared/shadcn/ui/textarea'; +import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; +import { cn } from '@/shared/shadcn/utils'; +import { Context } from 'quadratic-shared/typesAndSchemasAI'; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; +import { SetterOrUpdater } from 'recoil'; + +export type AIUserMessageFormWrapperProps = { + textareaRef: React.RefObject; + autoFocusRef?: React.RefObject; + initialPrompt?: string; + messageIndex?: number; +}; + +type Props = Omit & { + abortController: AbortController | undefined; + loading: boolean; + setLoading: SetterOrUpdater; + submitPrompt: (prompt: string) => void; + formOnKeyDown?: (event: React.KeyboardEvent) => void; + ctx?: { + context: Context; + setContext: React.Dispatch>; + initialContext?: Context; + }; +}; + +export const AIUserMessageForm = forwardRef((props: Props, ref) => { + const { + initialPrompt, + ctx, + autoFocusRef, + textareaRef: bottomTextareaRef, + abortController, + loading, + setLoading, + submitPrompt, + formOnKeyDown, + } = props; + + const [editing, setEditing] = useState(!initialPrompt); + const [prompt, setPrompt] = useState(initialPrompt ?? ''); + + const abortPrompt = useCallback(() => { + abortController?.abort(); + setLoading(false); + }, [abortController, setLoading]); + + const textareaRef = useRef(null); + useImperativeHandle(ref, () => textareaRef.current!); + + // Focus the input when relevant & the tab comes into focus + useEffect(() => { + if (autoFocusRef?.current) { + textareaRef.current?.focus(); + } + }, [autoFocusRef, textareaRef]); + + useEffect(() => { + if (loading && initialPrompt !== undefined) { + setEditing(false); + } + }, [loading, initialPrompt]); + + return ( +
e.preventDefault()} + onClick={() => { + if (editing) { + textareaRef.current?.focus(); + } + }} + > + {!editing && !loading && ( + + + + )} + + {ctx && } + + {editing ? ( +