Page past the 100-row cap in pickers and batch label sheets
This commit is contained in:
15
CHANGELOG.md
15
CHANGELOG.md
@@ -10,6 +10,21 @@ ADR-007 and ADR-002.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Pickers and batch label sheets no longer stop at 100 rows. `perpage` is
|
||||||
|
clamped to `MAX_PAGE_SIZE` (100) server-side and the response says nothing
|
||||||
|
about it, so a control asking for 500 or 1000 received the first 100 rows and
|
||||||
|
a success. On a site with 126 applications the 26 sorting last could not be
|
||||||
|
chosen as a knowledge-base topic; batch label printing rendered a sheet for
|
||||||
|
the first 100 of 262 machines that looked complete. New `fetchAllPages()`
|
||||||
|
helper plus `listAll()` on the applications, models, machines, computers,
|
||||||
|
printers, network, measuring-tools, USB and printed-parts APIs, used by the
|
||||||
|
KB article form and topic filter, the notification form, the report filter
|
||||||
|
builder, `AssetLabelBatch`, `PrinterQRBatch`, `USBLabelBatch`,
|
||||||
|
`PrintedPartsLabels`, `MachineForm` and `NetworkDeviceForm`. Lists that render
|
||||||
|
a page at a time are unchanged - they page deliberately.
|
||||||
|
|
||||||
## [0.11.0] - 2026-08-17
|
## [0.11.0] - 2026-08-17
|
||||||
|
|
||||||
The floor map became a set of drawings instead of one picture. A site can hold
|
The floor map became a set of drawings instead of one picture. A site can hold
|
||||||
|
|||||||
@@ -50,6 +50,30 @@ api.interceptors.response.use(
|
|||||||
|
|
||||||
export default api
|
export default api
|
||||||
|
|
||||||
|
// The backend clamps perpage to MAX_PAGE_SIZE (100) and says nothing about it,
|
||||||
|
// so asking for `perpage: 1000` silently returns the first 100 rows and drops
|
||||||
|
// the rest. A picker built that way looks complete and is not: with 126
|
||||||
|
// applications on a live site, the 26 sorting last were simply unselectable.
|
||||||
|
//
|
||||||
|
// Use this wherever a control needs the WHOLE list (dropdowns, pickers, label
|
||||||
|
// batches) rather than a page of it. Returns the full array directly, not an
|
||||||
|
// axios response. Anything that renders a paged table should keep calling
|
||||||
|
// list() with a real page number instead.
|
||||||
|
export async function fetchAllPages(path, params = {}) {
|
||||||
|
const first = await api.get(path, { params: { ...params, perpage: 100, page: 1 } })
|
||||||
|
let items = first.data.data || []
|
||||||
|
const totalpages = first.data.meta?.pagination?.totalpages || 1
|
||||||
|
if (totalpages > 1) {
|
||||||
|
const rest = await Promise.all(
|
||||||
|
Array.from({ length: totalpages - 1 }, (_, i) =>
|
||||||
|
api.get(path, { params: { ...params, perpage: 100, page: i + 2 } })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rest.forEach(response => { items = items.concat(response.data.data || []) })
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
// Auth API
|
// Auth API
|
||||||
export const authApi = {
|
export const authApi = {
|
||||||
login(username, password) {
|
login(username, password) {
|
||||||
@@ -77,6 +101,13 @@ export const machinesApi = {
|
|||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/machines', { params })
|
return api.get('/machines', { params })
|
||||||
},
|
},
|
||||||
|
// Every machine, paged past the backend's 100-row cap. Batch label printing
|
||||||
|
// and "pick any record" dropdowns must use this: list() with a large
|
||||||
|
// perpage is clamped to 100 and still returns a success response, so
|
||||||
|
// the tail simply goes missing.
|
||||||
|
listAll(params = {}) {
|
||||||
|
return fetchAllPages('/machines', params)
|
||||||
|
},
|
||||||
get(id) {
|
get(id) {
|
||||||
return api.get(`/machines/${id}`)
|
return api.get(`/machines/${id}`)
|
||||||
},
|
},
|
||||||
@@ -120,6 +151,13 @@ export const computersApi = {
|
|||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/computers', { params })
|
return api.get('/computers', { params })
|
||||||
},
|
},
|
||||||
|
// Every PC, paged past the backend's 100-row cap. Batch label printing
|
||||||
|
// and "pick any record" dropdowns must use this: list() with a large
|
||||||
|
// perpage is clamped to 100 and still returns a success response, so
|
||||||
|
// the tail simply goes missing.
|
||||||
|
listAll(params = {}) {
|
||||||
|
return fetchAllPages('/computers', params)
|
||||||
|
},
|
||||||
displayKiosks() {
|
displayKiosks() {
|
||||||
return api.get('/computers/display-kiosks')
|
return api.get('/computers/display-kiosks')
|
||||||
},
|
},
|
||||||
@@ -268,6 +306,13 @@ export const printersApi = {
|
|||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/printers', { params })
|
return api.get('/printers', { params })
|
||||||
},
|
},
|
||||||
|
// Every printer, paged past the backend's 100-row cap. Batch label printing
|
||||||
|
// and "pick any record" dropdowns must use this: list() with a large
|
||||||
|
// perpage is clamped to 100 and still returns a success response, so
|
||||||
|
// the tail simply goes missing.
|
||||||
|
listAll(params = {}) {
|
||||||
|
return fetchAllPages('/printers', params)
|
||||||
|
},
|
||||||
get(id) {
|
get(id) {
|
||||||
return api.get(`/printers/${id}`)
|
return api.get(`/printers/${id}`)
|
||||||
},
|
},
|
||||||
@@ -388,19 +433,8 @@ export const modelsApi = {
|
|||||||
// Backend caps perpage at 100, so page through every model. Returns the
|
// Backend caps perpage at 100, so page through every model. Returns the
|
||||||
// full array directly (not an axios response). Use in forms whose model
|
// full array directly (not an axios response). Use in forms whose model
|
||||||
// dropdown must include the editing record's model regardless of page.
|
// dropdown must include the editing record's model regardless of page.
|
||||||
async listAll() {
|
listAll(params = {}) {
|
||||||
const first = await api.get('/models', { params: { perpage: 100, page: 1 } })
|
return fetchAllPages('/models', params)
|
||||||
let items = first.data.data || []
|
|
||||||
const totalpages = first.data.meta?.pagination?.totalpages || 1
|
|
||||||
if (totalpages > 1) {
|
|
||||||
const rest = await Promise.all(
|
|
||||||
Array.from({ length: totalpages - 1 }, (_, i) =>
|
|
||||||
api.get('/models', { params: { perpage: 100, page: i + 2 } })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
rest.forEach(r => { items = items.concat(r.data.data || []) })
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
},
|
},
|
||||||
get(id) {
|
get(id) {
|
||||||
return api.get(`/models/${id}`)
|
return api.get(`/models/${id}`)
|
||||||
@@ -468,6 +502,12 @@ export const applicationsApi = {
|
|||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/applications', { params })
|
return api.get('/applications', { params })
|
||||||
},
|
},
|
||||||
|
// Every application, paged past the backend's 100-row cap. The catalogue is
|
||||||
|
// already over 100 entries on a live site, so any picker offering "all
|
||||||
|
// applications" must use this and not list({ perpage: <big number> }).
|
||||||
|
listAll(params = {}) {
|
||||||
|
return fetchAllPages('/applications', params)
|
||||||
|
},
|
||||||
get(id) {
|
get(id) {
|
||||||
return api.get(`/applications/${id}`)
|
return api.get(`/applications/${id}`)
|
||||||
},
|
},
|
||||||
@@ -713,6 +753,13 @@ export const usbApi = {
|
|||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/usb', { params })
|
return api.get('/usb', { params })
|
||||||
},
|
},
|
||||||
|
// Every USB device, paged past the backend's 100-row cap. Batch label printing
|
||||||
|
// and "pick any record" dropdowns must use this: list() with a large
|
||||||
|
// perpage is clamped to 100 and still returns a success response, so
|
||||||
|
// the tail simply goes missing.
|
||||||
|
listAll(params = {}) {
|
||||||
|
return fetchAllPages('/usb', params)
|
||||||
|
},
|
||||||
get(id) {
|
get(id) {
|
||||||
return api.get(`/usb/${id}`)
|
return api.get(`/usb/${id}`)
|
||||||
},
|
},
|
||||||
@@ -1046,6 +1093,13 @@ export const networkApi = {
|
|||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/network', { params })
|
return api.get('/network', { params })
|
||||||
},
|
},
|
||||||
|
// Every network device, paged past the backend's 100-row cap. Batch label
|
||||||
|
// printing and "pick any record" dropdowns must use this: list() with a
|
||||||
|
// large perpage is clamped to 100 and still returns a success response, so
|
||||||
|
// the tail simply goes missing.
|
||||||
|
listAll(params = {}) {
|
||||||
|
return fetchAllPages('/network', params)
|
||||||
|
},
|
||||||
get(id) {
|
get(id) {
|
||||||
return api.get(`/network/${id}`)
|
return api.get(`/network/${id}`)
|
||||||
},
|
},
|
||||||
@@ -1192,6 +1246,13 @@ export const measuringtoolsApi = {
|
|||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/measuringtools', { params })
|
return api.get('/measuringtools', { params })
|
||||||
},
|
},
|
||||||
|
// Every measuring tool, paged past the backend's 100-row cap. Batch label printing
|
||||||
|
// and "pick any record" dropdowns must use this: list() with a large
|
||||||
|
// perpage is clamped to 100 and still returns a success response, so
|
||||||
|
// the tail simply goes missing.
|
||||||
|
listAll(params = {}) {
|
||||||
|
return fetchAllPages('/measuringtools', params)
|
||||||
|
},
|
||||||
get(id) {
|
get(id) {
|
||||||
return api.get(`/measuringtools/${id}`)
|
return api.get(`/measuringtools/${id}`)
|
||||||
},
|
},
|
||||||
@@ -1235,6 +1296,11 @@ export const printedpartsApi = {
|
|||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/printedparts/items', { params })
|
return api.get('/printedparts/items', { params })
|
||||||
},
|
},
|
||||||
|
// Every printed item, paged past the backend's 100-row cap. The label sheet
|
||||||
|
// must print the whole selection, not the first page of it.
|
||||||
|
listAll(params = {}) {
|
||||||
|
return fetchAllPages('/printedparts/items', params)
|
||||||
|
},
|
||||||
get(printeditemid) {
|
get(printeditemid) {
|
||||||
return api.get(`/printedparts/items/${printeditemid}`)
|
return api.get(`/printedparts/items/${printeditemid}`)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -295,8 +295,10 @@ onMounted(async () => {
|
|||||||
codetype.value = (await getSetting('label_default_codetype', 'qr')) === 'barcode' ? 'barcode' : 'qr'
|
codetype.value = (await getSetting('label_default_codetype', 'qr')) === 'barcode' ? 'barcode' : 'qr'
|
||||||
encodes.value = await resolveDefaultEncodes(assettype)
|
encodes.value = await resolveDefaultEncodes(assettype)
|
||||||
try {
|
try {
|
||||||
const response = await config.api.list({ perpage: 500 })
|
// listAll, not list: perpage is clamped to 100 server-side, so a batch
|
||||||
assets.value = response.data.data || []
|
// sheet built from list() silently omitted every asset past the first
|
||||||
|
// 100 and printed a short run that looked complete.
|
||||||
|
assets.value = await config.api.listAll()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading assets:', err)
|
console.error('Error loading assets:', err)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -270,8 +270,10 @@ async function loadFilterOptions(fields) {
|
|||||||
filterOptions.value.locations = response.data.data || []
|
filterOptions.value.locations = response.data.data || []
|
||||||
}
|
}
|
||||||
if (fields.includes('application') && !filterOptions.value.applications.length) {
|
if (fields.includes('application') && !filterOptions.value.applications.length) {
|
||||||
const response = await applicationsApi.list({ perpage: 100 })
|
// listAll: 100 is the server-side cap, not a generous limit, and the
|
||||||
filterOptions.value.applications = response.data.data || []
|
// catalogue is past it - a report filtered by a late-alphabet
|
||||||
|
// application could not be built.
|
||||||
|
filterOptions.value.applications = await applicationsApi.listAll()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading filter options:', error)
|
console.error('Error loading filter options:', error)
|
||||||
|
|||||||
@@ -106,10 +106,13 @@ const applications = ref([])
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
// Load applications for topic dropdown
|
// Load applications for topic dropdown
|
||||||
const appsRes = await applicationsApi.list({ perpage: 1000, showhidden: true }) // isactive is the only filter that applies to a topic:
|
// listAll, not list: the backend clamps perpage to 100 without saying so,
|
||||||
// ishidden governs whether an application shows on the tiles page, which
|
// and the catalogue is past that, so a topic sorting late in the alphabet
|
||||||
// says nothing about whether it can be the subject of an article.
|
// was silently missing from this dropdown.
|
||||||
applications.value = appsRes.data.data || []
|
// isactive is the only filter that applies to a topic: ishidden governs
|
||||||
|
// whether an application shows on the tiles page, which says nothing about
|
||||||
|
// whether it can be the subject of an article.
|
||||||
|
applications.value = await applicationsApi.listAll({ showhidden: true })
|
||||||
|
|
||||||
// Load article if editing
|
// Load article if editing
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
|
|||||||
@@ -171,10 +171,13 @@ async function loadArticles() {
|
|||||||
|
|
||||||
async function loadTopics() {
|
async function loadTopics() {
|
||||||
try {
|
try {
|
||||||
const response = await applicationsApi.list({ perpage: 1000, showhidden: true }) // isactive is the only filter that applies to a topic:
|
// listAll, not list: the backend clamps perpage to 100 without saying so,
|
||||||
// ishidden governs whether an application shows on the tiles page, which
|
// and the catalogue is past that, so topics sorting late in the alphabet
|
||||||
// says nothing about whether it can be the subject of an article.
|
// were silently missing from this filter.
|
||||||
topics.value = response.data.data || []
|
// isactive is the only filter that applies to a topic: ishidden governs
|
||||||
|
// whether an application shows on the tiles page, which says nothing about
|
||||||
|
// whether it can be the subject of an article.
|
||||||
|
topics.value = await applicationsApi.listAll({ showhidden: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading topics:', error)
|
console.error('Error loading topics:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -469,7 +469,7 @@ onMounted(async () => {
|
|||||||
locationsApi.list({ perpage: 500 }),
|
locationsApi.list({ perpage: 500 }),
|
||||||
modelsApi.listAll(), // backend caps perpage at 100; page through all
|
modelsApi.listAll(), // backend caps perpage at 100; page through all
|
||||||
businessunitsApi.list({ perpage: 500 }),
|
businessunitsApi.list({ perpage: 500 }),
|
||||||
computersApi.list({ perpage: 500 }),
|
computersApi.listAll(), // backend caps perpage at 100; page through all
|
||||||
assetsApi.types.list() // Used for relationship types, will fix below
|
assetsApi.types.list() // Used for relationship types, will fix below
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -428,8 +428,9 @@ async function loadVendors() {
|
|||||||
try {
|
try {
|
||||||
const response = await vendorsApi.list({ perpage: 100 })
|
const response = await vendorsApi.list({ perpage: 100 })
|
||||||
vendors.value = response.data.data || []
|
vendors.value = response.data.data || []
|
||||||
const modelResponse = await modelsApi.list({ perpage: 500 })
|
// listAll: the models catalogue is already past the backend's 100-row
|
||||||
models.value = modelResponse.data.data || []
|
// cap, so list() left the tail of it unselectable.
|
||||||
|
models.value = await modelsApi.listAll()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading vendors:', err)
|
console.error('Error loading vendors:', err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,7 +318,10 @@ onMounted(async () => {
|
|||||||
const [typesRes, buRes, appsRes, tzRes] = await Promise.all([
|
const [typesRes, buRes, appsRes, tzRes] = await Promise.all([
|
||||||
notificationsApi.types.list(),
|
notificationsApi.types.list(),
|
||||||
businessUnitsApi.list().catch(() => ({ data: { data: [] } })),
|
businessUnitsApi.list().catch(() => ({ data: { data: [] } })),
|
||||||
applicationsApi.list({ perpage: 500 }).catch(() => ({ data: { data: [] } })),
|
// listAll: perpage is clamped to 100 server-side, and the catalogue is
|
||||||
|
// already past that, so this dropdown was missing its tail.
|
||||||
|
applicationsApi.listAll().then(items => ({ data: { data: items } }))
|
||||||
|
.catch(() => ({ data: { data: [] } })),
|
||||||
settingsApi.get('site_timezone').catch(() => null)
|
settingsApi.get('site_timezone').catch(() => null)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,9 @@ function labelText(item) {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await printedpartsApi.list({ perpage: 500 })
|
// listAll: perpage is clamped to 100, and a label sheet must cover every
|
||||||
items.value = response.data.data || []
|
// item, not the first page of them.
|
||||||
|
items.value = await printedpartsApi.listAll()
|
||||||
// ?item=<id> preselects one part (the Detail-page print button)
|
// ?item=<id> preselects one part (the Detail-page print button)
|
||||||
const preselect = new URLSearchParams(window.location.search).get('item')
|
const preselect = new URLSearchParams(window.location.search).get('item')
|
||||||
if (preselect) {
|
if (preselect) {
|
||||||
|
|||||||
@@ -91,8 +91,9 @@ const pages = computed(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await printersApi.list({ perpage: 500 })
|
// listAll: perpage is clamped to 100, and a batch sheet must cover every
|
||||||
printers.value = response.data.data || []
|
// printer, not the first page of them.
|
||||||
|
printers.value = await printersApi.listAll()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading printers:', error)
|
console.error('Error loading printers:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -145,8 +145,9 @@ const sheetPages = computed(() => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
labelStyle.value = (await getSetting('usb_label_style', 'barcode')) === 'qr' ? 'qr' : 'barcode'
|
labelStyle.value = (await getSetting('usb_label_style', 'barcode')) === 'qr' ? 'qr' : 'barcode'
|
||||||
try {
|
try {
|
||||||
const response = await usbApi.list({ perpage: 500 })
|
// listAll: perpage is clamped to 100, and a batch sheet must cover every
|
||||||
devices.value = response.data.data || []
|
// device, not the first page of them.
|
||||||
|
devices.value = await usbApi.listAll()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading USB devices:', error)
|
console.error('Error loading USB devices:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user