ADR-013 Phase 4: relocate 4 self-contained plugin frontends
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.
This commit is contained in:
279
plugins/applications/frontend/views/ApplicationForm.vue
Normal file
279
plugins/applications/frontend/views/ApplicationForm.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>{{ isEdit ? 'Edit Application' : 'New Application' }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<form v-else @submit.prevent="saveApplication">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="appname">Application Name *</label>
|
||||
<input
|
||||
id="appname"
|
||||
v-model="form.appname"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="supportteamid">Support Team</label>
|
||||
<select
|
||||
id="supportteamid"
|
||||
v-model="form.supportteamid"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="">Select team...</option>
|
||||
<option
|
||||
v-for="team in supportTeams"
|
||||
:key="team.supportteamid"
|
||||
:value="team.supportteamid"
|
||||
>
|
||||
{{ team.teamname }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="appdescription">Description</label>
|
||||
<textarea
|
||||
id="appdescription"
|
||||
v-model="form.appdescription"
|
||||
class="form-control"
|
||||
rows="2"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Application Flags</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" v-model="form.isinstallable" />
|
||||
Installable
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="form.islicenced" />
|
||||
Licensed
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="form.isprinter" />
|
||||
Printer App
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="form.isrequired" />
|
||||
Required on all PCs
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="form.ishidden" />
|
||||
Hidden
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Links & Paths</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="applicationlink">Application Link</label>
|
||||
<input
|
||||
id="applicationlink"
|
||||
v-model="form.applicationlink"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="documentationpath">Documentation Path</label>
|
||||
<input
|
||||
id="documentationpath"
|
||||
v-model="form.documentationpath"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="URL or file path"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="installpath">Install Path</label>
|
||||
<input
|
||||
id="installpath"
|
||||
v-model="form.installpath"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Network path or URL to install files"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="image">Image Filename</label>
|
||||
<input
|
||||
id="image"
|
||||
v-model="form.image"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="e.g., myapp.png"
|
||||
/>
|
||||
<small class="form-hint">Image should be placed in /images/applications/</small>
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Notes</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="applicationnotes">Application Notes (HTML supported)</label>
|
||||
<textarea
|
||||
id="applicationnotes"
|
||||
v-model="form.applicationnotes"
|
||||
class="form-control"
|
||||
rows="6"
|
||||
placeholder="Enter notes... HTML tags like <BR>, <a>, <strong> are supported"
|
||||
></textarea>
|
||||
</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...' : 'Save Application' }}
|
||||
</button>
|
||||
<router-link to="/applications" 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 { applicationsApi, supportteamsApi } 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({
|
||||
appname: '',
|
||||
appdescription: '',
|
||||
supportteamid: '',
|
||||
isinstallable: false,
|
||||
islicenced: false,
|
||||
isprinter: false,
|
||||
isrequired: false,
|
||||
ishidden: false,
|
||||
applicationlink: '',
|
||||
documentationpath: '',
|
||||
installpath: '',
|
||||
image: '',
|
||||
applicationnotes: ''
|
||||
})
|
||||
|
||||
const supportTeams = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load support teams
|
||||
const teamsRes = await supportteamsApi.list()
|
||||
supportTeams.value = teamsRes.data.data || []
|
||||
|
||||
// Load application if editing
|
||||
if (isEdit.value) {
|
||||
const response = await applicationsApi.get(route.params.id)
|
||||
const app = response.data.data
|
||||
|
||||
form.value = {
|
||||
appname: app.appname || '',
|
||||
appdescription: app.appdescription || '',
|
||||
supportteamid: app.supportteamid || '',
|
||||
isinstallable: app.isinstallable || false,
|
||||
islicenced: app.islicenced || false,
|
||||
isprinter: app.isprinter || false,
|
||||
isrequired: app.isrequired || false,
|
||||
ishidden: app.ishidden || false,
|
||||
applicationlink: app.applicationlink || '',
|
||||
documentationpath: app.documentationpath || '',
|
||||
installpath: app.installpath || '',
|
||||
image: app.image || '',
|
||||
applicationnotes: app.applicationnotes || ''
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err)
|
||||
error.value = 'Failed to load data'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function saveApplication() {
|
||||
error.value = ''
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const appData = {
|
||||
appname: form.value.appname,
|
||||
appdescription: form.value.appdescription || null,
|
||||
supportteamid: form.value.supportteamid || null,
|
||||
isinstallable: form.value.isinstallable,
|
||||
islicenced: form.value.islicenced,
|
||||
isprinter: form.value.isprinter,
|
||||
isrequired: form.value.isrequired,
|
||||
ishidden: form.value.ishidden,
|
||||
applicationlink: form.value.applicationlink || null,
|
||||
documentationpath: form.value.documentationpath || null,
|
||||
installpath: form.value.installpath || null,
|
||||
image: form.value.image || null,
|
||||
applicationnotes: form.value.applicationnotes || null
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await applicationsApi.update(route.params.id, appData)
|
||||
} else {
|
||||
await applicationsApi.create(appData)
|
||||
}
|
||||
|
||||
router.push('/applications')
|
||||
} catch (err) {
|
||||
console.error('Error saving application:', err)
|
||||
error.value = apiError(err, 'Failed to save application')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light, #666);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user