mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 00:41:56 -06:00
Model: sheets carry an identity while the app runs
Each sheet gets an identity (gridcraft_model::SheetUid, Sheet::uid) for what must follow one sheet while sheets are inserted, deleted, moved or renamed, which a sheet index or a name can't do. It lives only while the app runs: it isn't saved to JSON or a file, and a new, inserted, copied or opened sheet gets a fresh one. A rename, a move and the copies undo keeps don't change it, so undo of a deletion brings back the same sheet. Sheets compare by content, whatever their identity: any two identities compare equal, and SheetUid::get compares them. sheet.move with copy gives the copy a fresh identity. Tests cover new sheets, clones, JSON, renames, moves, deletion and undo, copies and a workbook read from a file.
This commit is contained in:
@@ -159,6 +159,8 @@ pub(super) fn move_sheet(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let Some(src) = cx.wb.sheets.get(i).cloned() else { return Err(bad("sheet.move", "no such sheet")) };
|
||||
if copy {
|
||||
let mut sh = (*src).clone();
|
||||
// The copy is a sheet of its own, not the one it was copied from.
|
||||
sh.uid = gridcraft_model::SheetUid::fresh();
|
||||
let base = sh.name.clone();
|
||||
// `Name (2)`: the name is shortened so the suffix fits in Excel's 31 characters.
|
||||
let name = (2..)
|
||||
|
||||
@@ -1135,3 +1135,29 @@ fn a_linked_paste_carries_the_files_trust_whatever_it_pastes() {
|
||||
assert!(sheet(&s).cell(b1).is_some_and(|c| c.formula.is_some()), "a linked paste of formats writes a formula");
|
||||
assert_eq!(trust(&s), Trust::FromFile);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sheet_keeps_its_identity_until_it_is_copied_or_read_from_a_file() {
|
||||
let mut s = s();
|
||||
let uids = |s: &Session| s.doc().unwrap().wb.sheets.iter().map(|sh| sh.uid.get()).collect::<Vec<_>>();
|
||||
let first = uids(&s)[0];
|
||||
// Renamed, moved, deleted and brought back by undo: the same sheet.
|
||||
s.execute("home.insertSheet", json!({"name": "Other"})).unwrap();
|
||||
s.execute("sheet.rename", json!({"sheet": "Sheet1", "name": "Data"})).unwrap();
|
||||
s.execute("sheet.move", json!({"sheet": "Data", "to": 1})).unwrap();
|
||||
let data = s.doc().unwrap().wb.sheet_index("Data").unwrap();
|
||||
assert_eq!(uids(&s)[data], first);
|
||||
s.execute("home.deleteSheet", json!({"sheet": "Data"})).unwrap();
|
||||
assert!(!uids(&s).contains(&first));
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(uids(&s).contains(&first), "undo brings the same sheet back");
|
||||
// A copy, or a new sheet of the same name, is another sheet.
|
||||
s.execute("sheet.move", json!({"sheet": "Data", "to": 0, "copy": true})).unwrap();
|
||||
let all = uids(&s);
|
||||
assert_eq!(all.len(), 3);
|
||||
assert_eq!(all.iter().collect::<std::collections::BTreeSet<_>>().len(), 3, "{all:?}");
|
||||
// Read from a file: new sheets.
|
||||
let bytes = crate::io::save_bytes(&s.doc().unwrap().wb, "book.xlsx").unwrap();
|
||||
let (read, _) = crate::io::open_bytes("book.xlsx", &bytes).unwrap();
|
||||
assert!(read.sheets.iter().all(|sh| !all.contains(&sh.uid.get())));
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ pub use macros::*;
|
||||
pub use password::PasswordHash;
|
||||
pub use preserved::{Preserved, PreservedPart, PreservedRel, PreservedSheet, PreservedXml};
|
||||
pub use python::{PyCode, PyOutput};
|
||||
pub use sheet::{DEFAULT_COL_WIDTH, DEFAULT_ROW_HEIGHT, LineIndex, Sheet, SheetViewMode, Visibility, points_to_chars};
|
||||
pub use sheet::{DEFAULT_COL_WIDTH, DEFAULT_ROW_HEIGHT, LineIndex, Sheet, SheetUid, SheetViewMode, Visibility, points_to_chars};
|
||||
pub use store::CellStore;
|
||||
pub use style::{
|
||||
Alignment, BorderLine, BorderStyle, Borders, Color, Fill, Font, HAlign, NumFmt, PatternType, Style, StyleId, StyleTable, Theme, Underline,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use gridcraft_core::{CellRef, RangeRef, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -52,9 +53,44 @@ impl SheetViewMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// A sheet's identity while the app runs, for what must follow one sheet (an open cell draft): a
|
||||
/// sheet made new (added, inserted, copied, read from a file or from JSON) gets a fresh one, and it
|
||||
/// stays with the sheet through renames, moves and the copies undo keeps. Never saved.
|
||||
/// It is not content: any two compare equal, so comparing sheets compares what they hold; compare
|
||||
/// identities with [`SheetUid::get`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SheetUid(u64);
|
||||
|
||||
/// The next [`SheetUid`]; 0 is never given out.
|
||||
static NEXT_SHEET_UID: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
impl SheetUid {
|
||||
pub fn fresh() -> SheetUid {
|
||||
SheetUid(NEXT_SHEET_UID.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
pub fn get(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SheetUid {
|
||||
fn default() -> SheetUid {
|
||||
SheetUid::fresh()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for SheetUid {
|
||||
fn eq(&self, _: &SheetUid) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Sheet {
|
||||
pub name: String,
|
||||
/// The sheet's identity while the app runs ([`SheetUid`]). Not in JSON or the file.
|
||||
#[serde(skip)]
|
||||
pub uid: SheetUid,
|
||||
/// The `sheetId` the file gave the sheet, written back on save so that what refers to the
|
||||
/// sheet by its id keeps finding it; 0 for a sheet the file did not have. Not in JSON.
|
||||
#[serde(skip)]
|
||||
@@ -149,6 +185,7 @@ impl Sheet {
|
||||
pub fn new(name: impl Into<String>) -> Sheet {
|
||||
Sheet {
|
||||
name: name.into(),
|
||||
uid: SheetUid::fresh(),
|
||||
file_id: 0,
|
||||
cells: CellStore::new(),
|
||||
cell_pictures: BTreeMap::new(),
|
||||
@@ -514,6 +551,26 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::cell::Formula;
|
||||
|
||||
#[test]
|
||||
fn a_sheet_keeps_its_identity_through_clones_and_a_new_sheet_gets_its_own() {
|
||||
let a = Sheet::new("A");
|
||||
let b = Sheet::new("A");
|
||||
assert_ne!(a.uid.get(), b.uid.get(), "two new sheets");
|
||||
assert_ne!(a.uid.get(), 0);
|
||||
// A clone (an undo snapshot, a rename through Arc::make_mut) is the same sheet.
|
||||
let mut renamed = a.clone();
|
||||
renamed.name = "B".into();
|
||||
assert_eq!(renamed.uid.get(), a.uid.get());
|
||||
// Not content: equal sheets compare equal whatever their identity.
|
||||
assert_eq!(a, b);
|
||||
// Not in JSON: a sheet read back is a new one.
|
||||
let json = serde_json::to_string(&a).unwrap();
|
||||
assert!(!json.contains("uid"), "{json}");
|
||||
let read: Sheet = serde_json::from_str(&json).unwrap();
|
||||
assert_ne!(read.uid.get(), a.uid.get());
|
||||
assert_eq!(read, a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_index_matches_linear() {
|
||||
let mut s = Sheet::new("S");
|
||||
|
||||
Reference in New Issue
Block a user