fix: an emptied selection is no shift anchor, and a deploy leaves no draft for the prompt to compare

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-15 17:15:29 +02:00
co-authored by Claude Opus 5
parent 0a6d49800e
commit 168ef0c6f5
4 changed files with 71 additions and 1 deletions
@@ -0,0 +1,59 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { HomeSelection, type BulkItem } from './homeSelection.svelte'
// A shift-click range resolves its ends through the rendered rows, so these need a
// document: the keys come back from `[data-row-selection-key]` in visual order.
function row(key: string): BulkItem {
return {
key,
kind: 'script',
path: key.slice('script/'.length),
displayPath: key.slice('script/'.length),
summary: '',
canWrite: true,
owner: true,
archived: false,
draftOnly: false,
isDraft: false,
rawApp: false
}
}
const a = row('script/f/a/one')
const b = row('script/f/a/two')
const c = row('script/f/a/three')
function rendered(...items: BulkItem[]): HomeSelection {
document.body.innerHTML = items
.map((i) => `<div data-row-selection-key="${i.key}"></div>`)
.join('')
const s = new HomeSelection()
s.available = true
for (const i of items) s.register(i)
return s
}
describe('HomeSelection shift-range', () => {
beforeEach(() => {
document.body.innerHTML = ''
})
it('selects the span between the anchor and the clicked row', () => {
const s = rendered(a, b, c)
s.toggle(a)
s.toggle(c, true)
expect(s.items.map((i) => i.key)).toEqual([a.key, b.key, c.key])
})
it('does not reach back into a selection the user already emptied', () => {
const s = rendered(a, b, c)
s.toggle(a)
s.toggle(a)
s.toggle(c, true)
expect(s.items.map((i) => i.key)).toEqual([c.key])
})
})
@@ -150,7 +150,10 @@ export class HomeSelection {
}
if (this.selected.has(item.key)) this.selected.delete(item.key)
else this.selected.set(item.key, item)
this.anchor = item.key
// Unticking the last row ends selection mode, so the row it was on is not an
// anchor any more: the next shift-click would otherwise reach back and select
// everything between it and the row the user just started from.
this.anchor = this.selected.size === 0 ? undefined : item.key
}
/** Visual order is read back from the DOM: the tree nests rows and pages them
@@ -501,6 +501,10 @@
// (unknown when another landed beside it), and the head is what is deployed,
// named by whoever deployed it rather than by the page load's author.
draftBaseVersion = version != null ? String(version) : undefined
// The deploy consumed the draft, so the prompt has nothing to compare: without
// this the timestamp fallback reads a load-time draft save against a deploy
// that just happened and opens on a row that no longer exists.
draftSavedAt = undefined
if (head != null) {
deployedHeadVersion = String(head)
deployedBy = headBy
@@ -663,6 +663,10 @@
// than keeping the one it just superseded.
parentVersion = version
draftBaseVersion = version != null ? String(version) : undefined
// The deploy consumed the draft, so the prompt has nothing to compare: without
// this the timestamp fallback reads a load-time draft save against a deploy
// that just happened and opens on a row that no longer exists.
draftSavedAt = undefined
if (head != null) {
deployedHeadVersion = String(head)
// Named by whoever deployed the head, not by the page load's author.