Relocate applications, geenforce, knowledgebase, and machines - each owns only its own views dir, so a clean move to plugins/<name>/frontend/ (views/ + routes.js, core imports rewritten to @/). geenforce's entryForm.js helper + its vitest spec move with it (ManifestEditor imports it as a sibling). Machinery fixes this batch surfaced: - routes.gen.js codegen uses namespace imports (import * as p_x). A route file without a `toplevel` export is undefined on the namespace instead of a strict- ESM missing-binding build error. - vitest gains a `pretest` stage so plugin-frontend specs (now under plugins/<name>/frontend/) run from their staged copy in src/.plugins-staged/. Verified live: GE-Enforce (the most complex, uses the entryForm sibling helper) renders fully from its staged frontend. Build + 58 vitest + naming green.
213 lines
4.9 KiB
Vue
213 lines
4.9 KiB
Vue
<template>
|
|
<div class="detail-page">
|
|
<div class="page-header">
|
|
<h2>Knowledge Base Article</h2>
|
|
<div class="header-actions">
|
|
<router-link :to="`/knowledgebase/${$route.params.id}/edit`" class="btn btn-primary">Edit</router-link>
|
|
<router-link to="/knowledgebase" class="btn btn-secondary">Back to List</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
|
|
<template v-else-if="article">
|
|
<div class="card article-card">
|
|
<table class="info-table">
|
|
<tbody>
|
|
<tr>
|
|
<th>Description:</th>
|
|
<td>{{ article.shortdescription }}</td>
|
|
</tr>
|
|
<tr v-if="article.application">
|
|
<th>Topic:</th>
|
|
<td>
|
|
<router-link :to="`/applications/${article.application.appid}`">
|
|
{{ article.application.appname }}
|
|
</router-link>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="article.linkurl">
|
|
<th>URL:</th>
|
|
<td>
|
|
<a href="#" @click.prevent="openArticle">
|
|
{{ article.linkurl }}
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="article.keywords">
|
|
<th>Keywords:</th>
|
|
<td>{{ article.keywords }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Clicks:</th>
|
|
<td>{{ article.clicks }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Last Updated:</th>
|
|
<td>{{ formatDate(article.lastupdated) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<hr />
|
|
|
|
<div class="actions-center">
|
|
<a
|
|
v-if="article.linkurl"
|
|
href="#"
|
|
class="btn btn-primary btn-lg"
|
|
@click.prevent="openArticle"
|
|
>
|
|
<span class="btn-icon">↗</span> Open Article
|
|
</a>
|
|
<div v-else class="alert alert-warning">
|
|
This article does not have a URL link defined. Please edit the article to add one.
|
|
</div>
|
|
<router-link :to="`/knowledgebase/${article.linkid}/edit`" class="btn btn-secondary btn-lg">
|
|
<span class="btn-icon">✎</span> Edit
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-else class="card">
|
|
<p style="text-align: center; color: var(--text-light);">Article not found</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { knowledgebaseApi } from '@/api'
|
|
|
|
const route = useRoute()
|
|
|
|
const loading = ref(true)
|
|
const article = ref(null)
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const response = await knowledgebaseApi.get(route.params.id)
|
|
article.value = response.data.data
|
|
} catch (error) {
|
|
console.error('Error loading article:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '-'
|
|
return new Date(dateStr).toLocaleString()
|
|
}
|
|
|
|
async function openArticle() {
|
|
try {
|
|
await knowledgebaseApi.trackClick(article.value.linkid)
|
|
article.value.clicks = (article.value.clicks || 0) + 1
|
|
// Open the URL after tracking
|
|
if (article.value.linkurl) {
|
|
window.open(article.value.linkurl, '_blank')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error tracking click:', error)
|
|
// Still open even if tracking fails
|
|
if (article.value.linkurl) {
|
|
window.open(article.value.linkurl, '_blank')
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.detail-page {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.article-card {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.info-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.info-table th,
|
|
.info-table td {
|
|
padding: 0.75rem;
|
|
text-align: left;
|
|
border: none;
|
|
}
|
|
|
|
.info-table th {
|
|
width: 150px;
|
|
font-weight: 600;
|
|
color: var(--text-light, #666);
|
|
vertical-align: top;
|
|
}
|
|
|
|
.info-table td a {
|
|
color: var(--primary, #1976d2);
|
|
text-decoration: none;
|
|
word-break: break-all;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.info-table td a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.info-table td {
|
|
word-break: break-word;
|
|
overflow-wrap: break-word;
|
|
max-width: 500px;
|
|
}
|
|
|
|
hr {
|
|
margin: 1.5rem 0;
|
|
border: none;
|
|
border-top: 1px solid var(--border-color, #e5e5e5);
|
|
}
|
|
|
|
.actions-center {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 1rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.btn-lg {
|
|
padding: 0.75rem 1.5rem;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.btn-icon {
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.alert {
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.alert-warning {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.alert-warning {
|
|
background: #3d3200;
|
|
color: #ffc107;
|
|
}
|
|
}
|
|
</style>
|