authorclover caruso <git@paperclover.net> 2026-10-11 17:47:37-07:00
committerclover caruso <git@paperclover.net> 2026-10-11 17:59:58-07:00
log970fdd7440162d30295c9e624ff2d295da7869f5
tree34909c706e51b18273527a40c4b9d811c06de87f
parent7ca09704a666b9adcf7406210f5707fccce92614
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

fix(ios): offer only valid directional page moves

Keep expanded page moves scoped to one page, matching the OneNote movement corpus. Omit Move Down when the first page would leave a subpage leading the section. Resolve directional actions against the current section and stable page ID, and return the actual arrangement acceptance result. Fixes: #134 Assisted-by: gpt-6.1-sol

1 files changed, 25 insertions(+), 9 deletions(-)

apps/ios/Snowbound/Arranging.swift+25-9
...@@ -91,14 +91,16 @@ enum Arranging {...@@ -91,14 +91,16 @@ enum Arranging {
91 }91 }
9292
93 /// Puts the pages of `section` in `order`, page `id` the one moved, as one edit.93 /// Puts the pages of `section` in `order`, page `id` the one moved, as one edit.
94 static func arrange(_ section: Section, _ order: [Placed], moving id: String) {94 @discardableResult
95 guard available() else { return }95 static func arrange(_ section: Section, _ order: [Placed], moving id: String) -> Bool {
96 guard available() else { return false }
96 let before = section.rows.map { Placed(id: $0.id, level: $0.level) }97 let before = section.rows.map { Placed(id: $0.id, level: $0.level) }
97 guard order != before, section.arrange(order, moving: id) else { return }98 guard order != before, section.arrange(order, moving: id) else { return false }
98 let (notebook, path) = (section.notebook, section.tab.path)99 let (notebook, path) = (section.notebook, section.tab.path)
99 done("Move Page") {100 done("Move Page") {
100 opening(path, of: notebook) { arrange($0, before, moving: id) }101 opening(path, of: notebook) { arrange($0, before, moving: id) }
101 }102 }
103 return true
102 }104 }
103105
104 /// Gives page `id` of `section` `level` where it stands, as Make Subpage and Promote106 /// Gives page `id` of `section` `level` where it stands, as Make Subpage and Promote
...@@ -393,10 +395,13 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate...@@ -393,10 +395,13 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate
393 }395 }
394396
395 /// Page `id` moved to `index` of the section's pages.397 /// Page `id` moved to `index` of the section's pages.
396 private func move(_ id: String, to index: Int) {398 @discardableResult
397 guard let section else { return }399 private func move(_ id: String, to index: Int) -> Bool {
398 Arranging.arrange(section, Arranging.order(section.rows, moving: id, to: index), moving: id)400 guard let section else { return false }
401 let order = Arranging.order(section.rows, moving: id, to: index)
402 guard order.first?.level == 1, Arranging.arrange(section, order, moving: id) else { return false }
399 becomeFirstResponder()403 becomeFirstResponder()
404 return true
400 }405 }
401406
402 /// Page `id` a level deeper or shallower, where it stays.407 /// Page `id` a level deeper or shallower, where it stays.
...@@ -449,15 +454,26 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate...@@ -449,15 +454,26 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate
449454
450 /// VoiceOver's ways to move a page without dragging it.455 /// VoiceOver's ways to move a page without dragging it.
451 func arrangeActions(_ item: Item) -> [UIAccessibilityCustomAction]? {456 func arrangeActions(_ item: Item) -> [UIAccessibilityCustomAction]? {
452 guard !Arranging.busy, item.version == nil, let rows = section?.rows, let at = rows.firstIndex(where: { $0.id == item.row.id })457 guard !Arranging.busy, item.version == nil, let section,
458 let at = section.rows.firstIndex(where: { $0.id == item.row.id })
453 else { return nil }459 else { return nil }
460 let rows = section.rows
454 let id = item.row.id461 let id = item.row.id
455 var actions: [UIAccessibilityCustomAction] = []462 var actions: [UIAccessibilityCustomAction] = []
463 for (name, direction) in [("Move Up", -1), ("Move Down", 1)] {
464 guard rows.indices.contains(at + direction),
465 at > 0 || direction < 0 || rows[1].level == 1
466 else { continue }
467 actions.append(UIAccessibilityCustomAction(name: name) { [weak self] _ in
468 guard let self, self.section === section,
469 let at = section.rows.firstIndex(where: { $0.id == id }), section.rows.indices.contains(at + direction)
470 else { return false }
471 return move(id, to: at + direction)
472 })
473 }
456 let add = { (name: String, act: @escaping () -> Void) in474 let add = { (name: String, act: @escaping () -> Void) in
457 actions.append(UIAccessibilityCustomAction(name: name) { _ in act(); return true })475 actions.append(UIAccessibilityCustomAction(name: name) { _ in act(); return true })
458 }476 }
459 if at > 0 { add("Move Up") { [weak self] in self?.move(id, to: at - 1) } }
460 if at < rows.count - 1 { add("Move Down") { [weak self] in self?.move(id, to: at + 1) } }
461 if indentable(rows, at) { add("Make Subpage") { [weak self] in self?.indent(id, by: 1) } }477 if indentable(rows, at) { add("Make Subpage") { [weak self] in self?.indent(id, by: 1) } }
462 if rows[at].level > 1 { add("Promote Subpage") { [weak self] in self?.indent(id, by: -1) } }478 if rows[at].level > 1 { add("Promote Subpage") { [weak self] in self?.indent(id, by: -1) } }
463 if !moveActions(item).isEmpty {479 if !moveActions(item).isEmpty {