Files
shopdb-flask/plugins/knowledgebase/frontend/views/KnowledgeBaseForm.vue
cproudlock 9c1c6c5729
Some checks failed
CI / backend (push) Failing after 7s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 8s
fix: page past the 100-row cap in application pickers
get_pagination_params clamps perpage to MAX_PAGE_SIZE (100) and reports
nothing about having done so, so a caller asking for perpage: 1000 gets the
first 100 rows and a success response. Every picker built that way looked
complete and was not.

Found on a live site with 126 active applications: the 26 sorting last were
absent from the knowledge-base topic dropdown, so an article could not be
filed against them. Nothing was wrong with those application records, and
editing them could never have helped.

Adds fetchAllPages() to the api module, generalizing the one call site that
already handled this correctly (modelsApi.listAll), and points the four
application pickers at a new applicationsApi.listAll(): the KB article form,
the KB list's topic filter, the notification form, and the report filter
builder.

Lists that render a page at a time are untouched - they page for a reason.
Other callers still asking for more than 100 rows of vendors, locations,
models, subnets and the rest are latent: correct only while those tables stay
under 100, and silent on the day they do not.
2026-08-17 14:06:35 -04:00

173 lines
4.8 KiB
Vue

<template>
<div>
<div class="page-header">
<h2>{{ isEdit ? 'Edit Article' : 'Add Knowledge Base Article' }}</h2>
</div>
<div class="card">
<div v-if="loading" class="loading">Loading...</div>
<form v-else @submit.prevent="saveArticle">
<div class="form-group">
<label for="shortdescription">Description *</label>
<input
id="shortdescription"
v-model="form.shortdescription"
type="text"
class="form-control"
required
maxlength="500"
placeholder="Brief description of the article"
/>
</div>
<div class="form-group">
<label for="linkurl">URL *</label>
<input
id="linkurl"
v-model="form.linkurl"
type="url"
class="form-control"
required
maxlength="2000"
placeholder="https://..."
/>
</div>
<div class="form-group">
<label for="keywords">Keywords</label>
<input
id="keywords"
v-model="form.keywords"
type="text"
class="form-control"
maxlength="500"
placeholder="Space-separated keywords"
/>
<small class="form-hint">Keywords help with search - separate with spaces</small>
</div>
<div class="form-group">
<label for="appid">Topic (Application)</label>
<select
id="appid"
v-model="form.appid"
class="form-control"
>
<option value="">-- Select Topic (Optional) --</option>
<option
v-for="app in applications"
:key="app.appid"
:value="app.appid"
>
{{ app.appname }}
</option>
</select>
<small class="form-hint">Select the application/topic this article relates to</small>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
<button type="submit" class="btn btn-primary" :disabled="saving">
{{ saving ? 'Saving...' : (isEdit ? 'Update Article' : 'Add Article') }}
</button>
<router-link to="/knowledgebase" class="btn btn-secondary">Cancel</router-link>
</div>
</form>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { knowledgebaseApi, applicationsApi } from '@/api'
import { apiError } from '@/utils/apiError'
const route = useRoute()
const router = useRouter()
const isEdit = computed(() => !!route.params.id)
const loading = ref(true)
const saving = ref(false)
const error = ref('')
const form = ref({
shortdescription: '',
linkurl: '',
keywords: '',
appid: ''
})
const applications = ref([])
onMounted(async () => {
try {
// Load applications for topic dropdown
// 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) {
const response = await knowledgebaseApi.get(route.params.id)
const article = response.data.data
form.value = {
shortdescription: article.shortdescription || '',
linkurl: article.linkurl || '',
keywords: article.keywords || '',
appid: article.application?.appid || ''
}
}
} catch (err) {
console.error('Error loading data:', err)
error.value = 'Failed to load data'
} finally {
loading.value = false
}
})
async function saveArticle() {
error.value = ''
saving.value = true
try {
const articleData = {
shortdescription: form.value.shortdescription,
linkurl: form.value.linkurl,
keywords: form.value.keywords || null,
appid: form.value.appid || null
}
if (isEdit.value) {
await knowledgebaseApi.update(route.params.id, articleData)
} else {
await knowledgebaseApi.create(articleData)
}
router.push('/knowledgebase')
} catch (err) {
console.error('Error saving article:', err)
error.value = apiError(err, 'Failed to save article')
} finally {
saving.value = false
}
}
</script>
<style scoped>
.form-hint {
display: block;
margin-top: 0.25rem;
font-size: 0.8rem;
color: var(--text-light, #666);
}
</style>