oficina / sheets Public

Name exported workbooks and record native editing checks

Paul Campbell pushed by codex agent, operated by paul@keymail.dev ecd950313e8567e39e4ee76884ae88e23681bab4 parent e598177
3 files changed, +29 −10
  • native/PARITY.md +9 −8
  • native/apple/Sources/SheetsKit/SheetFiles.swift +12 −2
  • native/apple/Tests/SheetsKitTests/EditingTests.swift +8 −0
diff --git a/native/PARITY.md b/native/PARITY.md
index 6742b71..2a5f6bb 100644
--- a/native/PARITY.md
+++ b/native/PARITY.md
@@ -9,24 +9,25 @@ coverage does not establish that every native control has been exercised.
| Sign in, list and create sheets | Shared native sign-in; workspace and model | Real isolated native approval and iPhone/iPad create flow; JSON list/create tests |
| Rename | Native panel, canonical rename command | Server JSON mutation test; native control compiles |
| Edit values/formulas; select cell and display raw formula | Native cell, formula field, serialized writes | iPhone/iPad create, edit, reselect, replace; independent fresh persistence check; Swift draft tests |
-| Range address selection and select all | Coordinate parser, range field and Edit menu | Swift coordinate/reversed range tests; select-all control compiles |
-| Clear, copy, cut, paste and fill down/right | Native clipboard, canonical translated batch writes | Swift quoted/CRLF TSV round trip; server batch/formula suites; native controls compile |
+| Range address selection and select all | Coordinate parser, range field and Edit menu | Actual iPad range-address selection; Swift coordinate/reversed range tests; select-all control compiles |
+| Clear, copy, cut, paste and fill down/right | Native clipboard, canonical translated batch writes | Actual iPad single-cell fill down, range copy/paste to B1:B2 and fresh server persistence; Swift TSV/formula-origin/bounds tests; clear/cut/fill-right controls compile |
| SUM, AVERAGE, COUNT, MIN, MAX insertion | Model insertion method and native menu | Web argument-selection implementation compared; native controls compile |
| Enter/Tab, Escape, arrows/type-to-edit and shortcuts | Formula field, grid and menu shortcuts | Save/tap exercised on simulators; hardware keyboard sequences not independently exercised |
-| Bold, italic, alignment, number formats, fill/ink and clear formatting | Native format menu, canonical format endpoint | Server format suite; native controls compile |
+| Bold, italic, alignment, number formats, fill/ink and clear formatting | Native format menu, canonical format endpoint | Actual iPad range Number formatting and fresh server format/display verification; server format suite; other native format controls compile |
| Row height and column width | Native sizes panel, canonical size endpoint | Server size suite; native controls compile |
-| Input undo, history and restore | Edit menu and history panel | Native JSON history/undo tests; server history suite; native panel compiles |
+| Input undo, history and restore | Edit menu and history panel | Actual iPad undo and first-open History Restore, with fresh authenticated raw/display/history checks; native JSON and server history suites |
| Non-destructive table sorting/filtering | Native table and view panel, canonical view endpoint | Server suites and source-coordinate snapshot test; native controls compile |
| Comment thread add/resolve | Native comment panel, canonical routes | JSON access/permission tests and server comment suite; native controls compile |
| Grants, revoke, viewer/commenter links | Native sharing panel, existing owner-only routes | Native mutation denial tests and server sharing suite; native controls compile |
| XLSX/XLSM import and job progress | Native picker, original import route and owner-only JSON job polling | Workbook import suite; native owner/other-session polling tests; native picker compiles |
-| XLSX export | Streaming download to explicit private temporary export, system file exporter | Shared transport tests; native Transferable exporter compiles; picker transfer not independently exercised |
+| XLSX export | Streaming download to explicit private temporary export, system file exporter | Actual iPad destination picker and cancellation; fixture verifies authenticated export request and temporary XLSX removal; final destination write not independently exercised |
| Live edits, reconnect, resync and revoked access | Stream, tiled snapshots, version reconciliation, server delivery revalidation | SSE framing/version/draft tests; live revoked-session test; native background task compiles |
| Appearance and imported colour warnings | Appearance selector, native colours and warning text | Actual light iPhone/iPad screenshots inspected; dark mode not independently exercised |
-| Unsaved edit protection | Draft state, retry/discard, navigation confirmation and Mac close/quit guard | Swift synchronous draft and peer-update tests; native close/quit prompts compile |
+| Unsaved edit protection | Draft state, retry/discard, navigation confirmation and Mac close/quit guard | Actual iPad forced HTTP503 retains draft, disables new formula edits/selection, and Retry succeeds; Swift pending-batch/draft tests; Mac close/quit prompts compile |
Both simulator UI runs passed the explicitly documented flow in `REVIEW.md`.
Mac executable builds passed, but the environment did not grant screen capture
-access, so no Mac screenshot is accepted as verification. Physical-device
-signing and exhaustive native interaction coverage remain separate validation.
+access, so no Mac screenshot is accepted as verification. A signed iOS device
+build was produced; physical-device execution and exhaustive native interaction
+coverage remain separate validation.
The implementation contains no embedded web editor.
diff --git a/native/apple/Sources/SheetsKit/SheetFiles.swift b/native/apple/Sources/SheetsKit/SheetFiles.swift
index 4486524..7196fbb 100644
--- a/native/apple/Sources/SheetsKit/SheetFiles.swift
+++ b/native/apple/Sources/SheetsKit/SheetFiles.swift
@@ -4,11 +4,21 @@ import UniformTypeIdentifiers
struct WorkbookExport: Transferable {
let url: URL
+ let filename: String
+ init(url: URL, title: String) {
+ self.url = url
+ var name = title.components(separatedBy: .controlCharacters).joined()
+ .replacingOccurrences(of: "/", with: "-").replacingOccurrences(of: ":", with: "-")
+ .trimmingCharacters(in: .whitespacesAndNewlines.union(CharacterSet(charactersIn: ".")))
+ while name.utf8.count > 200 { name.removeLast() }
+ if name.isEmpty { name = "Workbook" }
+ filename = name.lowercased().hasSuffix(".xlsx") ? name : name + ".xlsx"
+ }
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(exportedContentType: UTType(filenameExtension: "xlsx") ?? .data) {
workbook in
SentTransferredFile(workbook.url, allowAccessingOriginalFile: false)
- }.suggestedFileName { $0.url.lastPathComponent }
+ }.suggestedFileName { $0.filename }
}
}
struct ImportStarted: Decodable { let job: String }
@@ -83,7 +93,7 @@ struct SheetFiles: View {
try? FileManager.default.removeItem(at: destination)
return
}
- workbook = WorkbookExport(url: destination)
+ workbook = WorkbookExport(url: destination, title: model.page?.Sheet.Title ?? "Workbook")
exporter = true
} catch {
try? FileManager.default.removeItem(at: destination)
diff --git a/native/apple/Tests/SheetsKitTests/EditingTests.swift b/native/apple/Tests/SheetsKitTests/EditingTests.swift
index 4bdb8a1..60116b6 100644
--- a/native/apple/Tests/SheetsKitTests/EditingTests.swift
+++ b/native/apple/Tests/SheetsKitTests/EditingTests.swift
@@ -108,3 +108,11 @@ import Testing
#expect(model.selection.start == Coordinate(r: 0, c: 0))
#expect(model.failedWrites.count == 2)
}
+
+@Test func workbookExportNamesUseSafeDocumentTitles() {
+ let url = URL(fileURLWithPath: "/tmp/private-staging.xlsx")
+ #expect(WorkbookExport(url: url, title: "Budget 2026").filename == "Budget 2026.xlsx")
+ #expect(WorkbookExport(url: url, title: "../Budget:2026\n").filename == "-Budget-2026.xlsx")
+ #expect(WorkbookExport(url: url, title: "...").filename == "Workbook.xlsx")
+ #expect(WorkbookExport(url: url, title: String(repeating: "🧪", count: 100)).filename.utf8.count <= 205)
+}