|
|
|
|
@@ -42,33 +42,43 @@
|
|
|
|
|
<span class="plugin-name">{{ p.name }}</span>
|
|
|
|
|
<span class="plugin-desc">{{ p.description }}</span>
|
|
|
|
|
</label>
|
|
|
|
|
<!-- Transparency note for plugins that provision extra tables -->
|
|
|
|
|
<div v-if="p.enabled && p.provisioning_note" class="provision-note">
|
|
|
|
|
<p>{{ p.provisioning_note.note }}</p>
|
|
|
|
|
<p class="provision-tables">
|
|
|
|
|
Creates in shopdb:
|
|
|
|
|
<code v-for="t in p.provisioning_note.tables" :key="t">{{ t }}</code>
|
|
|
|
|
</p>
|
|
|
|
|
<p v-if="p.provisioning_note.docs" class="provision-docs">Schema: <code>{{ p.provisioning_note.docs }}</code></p>
|
|
|
|
|
|
|
|
|
|
<!-- Plugins that provision tables: choose to create them here or
|
|
|
|
|
connect an existing database -->
|
|
|
|
|
<div v-if="p.enabled && p.provisioning_note && p.provisioning_note.mode_setting" class="plugin-mode">
|
|
|
|
|
<label class="mode-opt">
|
|
|
|
|
<input type="radio" value="selfhosted" v-model="pluginModes[p.provisioning_note.mode_setting]" />
|
|
|
|
|
Create the tables here <span class="hint">(recommended)</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label class="mode-opt">
|
|
|
|
|
<input type="radio" value="external" v-model="pluginModes[p.provisioning_note.mode_setting]" />
|
|
|
|
|
Connect to your own database
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<!-- Self-hosted: transparency note -->
|
|
|
|
|
<div v-if="pluginModes[p.provisioning_note.mode_setting] !== 'external'" class="provision-note">
|
|
|
|
|
<p>{{ p.provisioning_note.note }}</p>
|
|
|
|
|
<p class="provision-tables">Creates in shopdb: <code v-for="t in p.provisioning_note.tables" :key="t">{{ t }}</code></p>
|
|
|
|
|
<p v-if="p.provisioning_note.docs" class="provision-docs">Schema: <code>{{ p.provisioning_note.docs }}</code></p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- External: connection config -->
|
|
|
|
|
<div v-else class="config-block">
|
|
|
|
|
<div v-for="field in p.config_schema" :key="field.key" class="form-group">
|
|
|
|
|
<label>{{ field.label }}<span v-if="field.secret" class="secret-tag">secret</span></label>
|
|
|
|
|
<input
|
|
|
|
|
:type="field.type === 'password' ? 'password' : (field.type === 'number' ? 'number' : 'text')"
|
|
|
|
|
:placeholder="field.default || ''"
|
|
|
|
|
v-model="pluginConfig[field.key]"
|
|
|
|
|
class="form-control" />
|
|
|
|
|
<small v-if="field.help" class="hint">{{ field.help }}</small>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<p v-else class="hint">No optional plugins found.</p>
|
|
|
|
|
|
|
|
|
|
<!-- Per-plugin config for enabled plugins that declare a schema -->
|
|
|
|
|
<div v-for="p in configurablePlugins" :key="`config-${p.name}`" class="config-block">
|
|
|
|
|
<h3 class="config-title">{{ p.name }} configuration</h3>
|
|
|
|
|
<div v-for="field in p.config_schema" :key="field.key" class="form-group">
|
|
|
|
|
<label>{{ field.label }}<span v-if="field.secret" class="secret-tag">secret</span></label>
|
|
|
|
|
<input
|
|
|
|
|
:type="field.type === 'password' ? 'password' : (field.type === 'number' ? 'number' : 'text')"
|
|
|
|
|
:placeholder="field.default || ''"
|
|
|
|
|
v-model="pluginConfig[field.key]"
|
|
|
|
|
class="form-control" />
|
|
|
|
|
<small v-if="field.help" class="hint">{{ field.help }}</small>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Secrets are never stored in the DB; emit .env lines to paste -->
|
|
|
|
|
<div v-if="envLines" class="env-block">
|
|
|
|
|
<p class="hint">Paste these into <code>.env</code> and restart the app (secrets are not stored in the database):</p>
|
|
|
|
|
@@ -153,13 +163,21 @@ const form = ref({
|
|
|
|
|
})
|
|
|
|
|
const plugins = ref([])
|
|
|
|
|
const pluginConfig = ref({}) // flat map: setting/field key -> value
|
|
|
|
|
const pluginModes = ref({}) // mode_setting key -> 'selfhosted' | 'external'
|
|
|
|
|
const saving = ref(false)
|
|
|
|
|
const seeding = ref(false)
|
|
|
|
|
const seedResult = ref('')
|
|
|
|
|
|
|
|
|
|
// Enabled plugins that declare config fields.
|
|
|
|
|
function modeOf(plugin) {
|
|
|
|
|
const key = plugin.provisioning_note?.mode_setting
|
|
|
|
|
return key ? (pluginModes.value[key] || 'selfhosted') : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enabled plugins whose external connection config should be collected right
|
|
|
|
|
// now: those in external mode (or with config but no mode concept).
|
|
|
|
|
const configurablePlugins = computed(() =>
|
|
|
|
|
plugins.value.filter(p => p.enabled && (p.config_schema || []).length))
|
|
|
|
|
plugins.value.filter(p => p.enabled && (p.config_schema || []).length &&
|
|
|
|
|
(!p.provisioning_note?.mode_setting || modeOf(p) === 'external')))
|
|
|
|
|
|
|
|
|
|
// .env lines for secret fields that have a value entered.
|
|
|
|
|
const envLines = computed(() => {
|
|
|
|
|
@@ -196,8 +214,15 @@ onMounted(async () => {
|
|
|
|
|
plugins.value = response.data.data?.plugins || response.data.data || []
|
|
|
|
|
} catch (err) { /* ignore */ }
|
|
|
|
|
|
|
|
|
|
// Preload current values for non-secret plugin config fields.
|
|
|
|
|
// Preload directory mode (defaults self-hosted) + non-secret config fields.
|
|
|
|
|
for (const plugin of plugins.value) {
|
|
|
|
|
const modeKey = plugin.provisioning_note?.mode_setting
|
|
|
|
|
if (modeKey && pluginModes.value[modeKey] === undefined) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await settingsApi.get(modeKey)
|
|
|
|
|
pluginModes.value[modeKey] = response.data?.data?.value || 'selfhosted'
|
|
|
|
|
} catch (err) { pluginModes.value[modeKey] = 'selfhosted' }
|
|
|
|
|
}
|
|
|
|
|
for (const field of (plugin.config_schema || [])) {
|
|
|
|
|
if (field.secret) continue
|
|
|
|
|
try {
|
|
|
|
|
@@ -210,8 +235,12 @@ onMounted(async () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
async function saveStep() {
|
|
|
|
|
// Plugins step: persist non-secret config fields (secrets stay in .env).
|
|
|
|
|
// Plugins step: save each plugin's directory mode, plus non-secret external
|
|
|
|
|
// connection config (secrets stay in .env).
|
|
|
|
|
if (current.value.key === 'plugins') {
|
|
|
|
|
for (const [modeKey, modeValue] of Object.entries(pluginModes.value)) {
|
|
|
|
|
await settingsApi.update(modeKey, modeValue)
|
|
|
|
|
}
|
|
|
|
|
for (const plugin of configurablePlugins.value) {
|
|
|
|
|
for (const field of plugin.config_schema) {
|
|
|
|
|
if (field.secret) continue
|
|
|
|
|
@@ -319,6 +348,10 @@ async function finish() {
|
|
|
|
|
.plugin-row { display: grid; grid-template-columns: auto auto 1fr; gap: 0.6rem; align-items: baseline; padding: 0.5rem 0.6rem; background: var(--bg); border-radius: 6px; }
|
|
|
|
|
.plugin-name { font-weight: 600; text-transform: capitalize; }
|
|
|
|
|
.plugin-desc { color: var(--text-light); font-size: 0.85rem; }
|
|
|
|
|
.plugin-mode { margin: 0.4rem 0 0.2rem 1.9rem; }
|
|
|
|
|
.mode-opt { display: block; font-size: 0.86rem; margin-bottom: 0.3rem; cursor: pointer; }
|
|
|
|
|
.mode-opt input { margin-right: 0.4rem; }
|
|
|
|
|
.plugin-mode .provision-note, .plugin-mode .config-block { margin-left: 0; }
|
|
|
|
|
.provision-note { margin: 0.3rem 0 0.2rem 1.9rem; padding: 0.6rem 0.75rem; background: var(--bg); border-left: 3px solid var(--warning); border-radius: 4px; font-size: 0.82rem; }
|
|
|
|
|
.provision-note p { margin: 0 0 0.35rem; }
|
|
|
|
|
.provision-note p:last-child { margin-bottom: 0; }
|
|
|
|
|
|