feat(series): operator-set sparse page numbers + gap blocks (#789 tweak)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Failing after 3m17s

Replaces the auto-renumbered 1..N position key with operator-OWNED page
numbers: sparse, gaps allowed, editable, never auto-renumbered. Order follows
the numbers; unnumbered pages sort to the tail. This is the fix for the model
that clobbered hand-set numbers on the flatten — numbers are now data, not a
derived sequence.

- series_service: drop the renumber-on-reorder/remove; order by page_number
  NULLS LAST; new set_page_number(image_id, n|None); list_pages returns `gaps`
  (one entry per missing-number run) + each pending group's parsed `start_page`;
  set_cover renumbers below the current min; place_pending(image_ids, start_page)
  numbers placed pages sequentially from the start (drop junk first → numbers
  line up); add_post stamps the parsed start on staged pages.
- api/tags: POST /series/<id>/pages/number (set one page's number); /pending/
  place takes start_page; removed /reorder.
- frontend: per-card editable number input; one gap block per gap with
  drop-on-edge to assign the adjacent number (middle → type); append drop zone;
  pending tray gets a "from page N" field + "Place from page N".
- tests reworked: sparse numbers + gaps, place-from-start, set-page-number route.

No migration; nothing destructive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 23:10:30 -04:00
parent 7bb765b6ed
commit 013b9d7f06
8 changed files with 401 additions and 228 deletions
+17 -6
View File
@@ -42,19 +42,30 @@ describe('seriesManage', () => {
expect(s.pageCount).toBe(1)
})
it('reorder posts ordered ids to the series reorder route', async () => {
it('setPageNumber posts the image + number to /pages/number', async () => {
const s = useSeriesManageStore()
s.tagId = 7
const calls = []
stubFetch((url, init) => {
calls.push({ url, body: init.body ? JSON.parse(init.body) : null })
if (url.includes('/reorder')) return { status: 200, body: { ok: true } }
if (url.includes('/pages/number')) return { status: 200, body: { ok: true } }
return { status: 200, body: SERIES_BODY }
})
await s.reorder([30, 10, 20])
const r = calls.find(c => c.url.includes('/reorder'))
expect(r.url).toContain('/api/series/7/reorder')
expect(r.body).toEqual({ image_ids: [30, 10, 20] })
await s.setPageNumber(42, 5)
const r = calls.find(c => c.url.includes('/pages/number'))
expect(r.url).toContain('/api/series/7/pages/number')
expect(r.body).toEqual({ image_id: 42, page_number: 5 })
})
it('gapAfter looks up a gap by its after_image_id', async () => {
const s = useSeriesManageStore()
stubFetch(() => ({
status: 200,
body: { ...SERIES_BODY, gaps: [{ after_image_id: 1, before_image_id: 9, from: 2, to: 3 }] }
}))
await s.load(7)
expect(s.gapAfter(1)).toEqual({ after_image_id: 1, before_image_id: 9, from: 2, to: 3 })
expect(s.gapAfter(99)).toBeNull()
})
it('createDivider posts the anchor image + labels', async () => {