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
+18 -10
View File
@@ -20,7 +20,8 @@ export const useSeriesManageStore = defineStore('seriesManage', () => {
const series = ref(null)
const pages = ref([]) // flat, ordered: [{image_id, page_number, stated_page, thumbnail_url, image_url, source_post}]
const dividers = ref([]) // [{id, anchor_image_id, page_number, title, stated_part}]
const pending = ref([]) // staged-from-post: [{post:{id,title}|null, pages:[{image_id, stated_page, thumbnail_url, image_url}]}]
const gaps = ref([]) // missing-number gaps: [{after_image_id, before_image_id, from, to}]
const pending = ref([]) // staged-from-post: [{post:{id,title}|null, start_page, pages:[{image_id, stated_page, thumbnail_url, image_url}]}]
const partGaps = ref([]) // [{after_divider_id, start, end}]
const pageCount = ref(0)
const picker = ref([]) // gallery scroll results
@@ -36,6 +37,7 @@ export const useSeriesManageStore = defineStore('seriesManage', () => {
series.value = body.series
pages.value = body.pages || []
dividers.value = body.dividers || []
gaps.value = body.gaps || []
pending.value = body.pending || []
partGaps.value = body.part_gaps || []
pageCount.value = pages.value.length
@@ -57,14 +59,19 @@ export const useSeriesManageStore = defineStore('seriesManage', () => {
return partGaps.value.find(g => g.after_divider_id === dividerId) || null
}
// ---- pages (series-wide) ----
async function reorder(orderedImageIds) {
await api.post(`/api/series/${tagId.value}/reorder`, {
body: { image_ids: orderedImageIds }
// ---- pages (number-driven) ----
// Set one page's number (sparse, gaps allowed; pass null to unnumber).
async function setPageNumber(imageId, pageNumber) {
await api.post(`/api/series/${tagId.value}/pages/number`, {
body: { image_id: imageId, page_number: pageNumber }
})
await refresh()
}
function gapAfter(imageId) {
return gaps.value.find(g => g.after_image_id === imageId) || null
}
async function remove(imageId) {
await api.post(`/api/series/${tagId.value}/pages/remove`, {
body: { image_ids: [imageId] }
@@ -108,9 +115,10 @@ export const useSeriesManageStore = defineStore('seriesManage', () => {
}
// ---- pending staging (add-from-post) ----
async function placePending(imageIds, beforeImageId = null) {
// Place pending pages, numbered sequentially from startPage in the given order.
async function placePending(imageIds, startPage = null) {
await api.post(`/api/series/${tagId.value}/pending/place`, {
body: { image_ids: imageIds, before_image_id: beforeImageId }
body: { image_ids: imageIds, start_page: startPage }
})
await refresh()
toast({ text: 'Pages placed into the series', type: 'success' })
@@ -143,10 +151,10 @@ export const useSeriesManageStore = defineStore('seriesManage', () => {
}
return {
tagId, series, pages, dividers, pending, partGaps, pageCount,
tagId, series, pages, dividers, gaps, pending, partGaps, pageCount,
picker, pickerCursor, pickerSelection, loading,
load, refresh, dividerAt, partGapAfter,
reorder, remove, setCover, placePending,
load, refresh, dividerAt, partGapAfter, gapAfter,
setPageNumber, remove, setCover, placePending,
createDivider, renameDivider, setDividerPart, deleteDivider,
loadPicker, togglePick, addSelected
}