diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 831dcb5..ea104d7 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -50,6 +50,30 @@ api.interceptors.response.use( 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 export const authApi = { login(username, password) { @@ -388,19 +412,8 @@ export const modelsApi = { // Backend caps perpage at 100, so page through every model. Returns the // full array directly (not an axios response). Use in forms whose model // dropdown must include the editing record's model regardless of page. - async listAll() { - const first = await api.get('/models', { 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('/models', { params: { perpage: 100, page: i + 2 } }) - ) - ) - rest.forEach(r => { items = items.concat(r.data.data || []) }) - } - return items + listAll(params = {}) { + return fetchAllPages('/models', params) }, get(id) { return api.get(`/models/${id}`) @@ -468,6 +481,12 @@ export const applicationsApi = { list(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: }). + listAll(params = {}) { + return fetchAllPages('/applications', params) + }, get(id) { return api.get(`/applications/${id}`) }, diff --git a/frontend/src/views/reports/ReportsIndex.vue b/frontend/src/views/reports/ReportsIndex.vue index c2a5ccc..34457c3 100644 --- a/frontend/src/views/reports/ReportsIndex.vue +++ b/frontend/src/views/reports/ReportsIndex.vue @@ -270,8 +270,10 @@ async function loadFilterOptions(fields) { filterOptions.value.locations = response.data.data || [] } if (fields.includes('application') && !filterOptions.value.applications.length) { - const response = await applicationsApi.list({ perpage: 100 }) - filterOptions.value.applications = response.data.data || [] + // listAll: 100 is the server-side cap, not a generous limit, and the + // catalogue is past it - a report filtered by a late-alphabet + // application could not be built. + filterOptions.value.applications = await applicationsApi.listAll() } } catch (error) { console.error('Error loading filter options:', error) diff --git a/plugins/knowledgebase/frontend/views/KnowledgeBaseForm.vue b/plugins/knowledgebase/frontend/views/KnowledgeBaseForm.vue index 8e15586..942c5f2 100644 --- a/plugins/knowledgebase/frontend/views/KnowledgeBaseForm.vue +++ b/plugins/knowledgebase/frontend/views/KnowledgeBaseForm.vue @@ -106,10 +106,13 @@ const applications = ref([]) onMounted(async () => { try { // Load applications for topic dropdown - const appsRes = await applicationsApi.list({ perpage: 1000, showhidden: true }) // 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 = appsRes.data.data || [] + // listAll, not list: the backend clamps perpage to 100 without saying so, + // and the catalogue is past that, so a topic sorting late in the alphabet + // was silently missing from this dropdown. + // 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 if (isEdit.value) { diff --git a/plugins/knowledgebase/frontend/views/KnowledgeBaseList.vue b/plugins/knowledgebase/frontend/views/KnowledgeBaseList.vue index 700576a..01c048c 100644 --- a/plugins/knowledgebase/frontend/views/KnowledgeBaseList.vue +++ b/plugins/knowledgebase/frontend/views/KnowledgeBaseList.vue @@ -171,10 +171,13 @@ async function loadArticles() { async function loadTopics() { try { - const response = await applicationsApi.list({ perpage: 1000, showhidden: true }) // 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 = response.data.data || [] + // listAll, not list: the backend clamps perpage to 100 without saying so, + // and the catalogue is past that, so topics sorting late in the alphabet + // were silently missing from this filter. + // 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) { console.error('Error loading topics:', error) } diff --git a/plugins/notifications/frontend/views/NotificationForm.vue b/plugins/notifications/frontend/views/NotificationForm.vue index a48fff2..f80bd2d 100644 --- a/plugins/notifications/frontend/views/NotificationForm.vue +++ b/plugins/notifications/frontend/views/NotificationForm.vue @@ -318,7 +318,10 @@ onMounted(async () => { const [typesRes, buRes, appsRes, tzRes] = await Promise.all([ notificationsApi.types.list(), 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) ])