731 lines
26 KiB
Vue
731 lines
26 KiB
Vue
<template>
|
|
<div>
|
|
<div class="columns is-multiline">
|
|
<div class="column is-12 is-clearfix is-unselectable">
|
|
<span class="title is-4">
|
|
<span class="icon"><i class="fas fa-server"></i> </span>
|
|
<NuxtLink to="/backends" v-text="'Backends'" />
|
|
-
|
|
<NuxtLink :to="'/backend/' + id" v-text="id" />
|
|
: Edit
|
|
</span>
|
|
|
|
<div class="is-pulled-right">
|
|
<div class="field is-grouped"></div>
|
|
</div>
|
|
|
|
<div class="is-hidden-mobile">
|
|
<span class="subtitle">Edit the backend settings.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="column is-12" v-if="isLimitedToken">
|
|
<Message title="For your information" message_class="has-background-warning-90 has-text-dark"
|
|
icon="fas fa-info-circle">
|
|
<p>
|
|
This backend is using accesstoken instead of API keys, And this method untested and may not work as
|
|
expected.
|
|
Please make sure you know what you are doing. Simple operations like <code>Import</code>,
|
|
<code>Export</code>
|
|
should work fine.
|
|
</p>
|
|
<p>
|
|
How the access token interact with the rest of the API is undefined and untested by us. Please use with
|
|
caution. If you notice any issue, please report it to us.
|
|
</p>
|
|
</Message>
|
|
</div>
|
|
|
|
<div class="column is-12" v-if="isLoading">
|
|
<Message message_class="is-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
|
|
message="Loading backend settings. Please wait..." />
|
|
</div>
|
|
|
|
<div v-else class="column is-12">
|
|
<form id="backend_edit_form" @submit.prevent="saveContent">
|
|
<div class="card">
|
|
<header class="card-header">
|
|
<p class="card-header-title">
|
|
Edit Backend: <u class="has-text-danger">{{ api_user }}</u>@{{ backend.name }}</p>
|
|
</header>
|
|
|
|
<div class="card-content">
|
|
<div class="field">
|
|
<label class="label">Local User</label>
|
|
<div class="control has-icons-left">
|
|
<div class="select is-fullwidth">
|
|
<select class="is-capitalized" disabled>
|
|
<option v-text="api_user" />
|
|
</select>
|
|
</div>
|
|
<div class="icon is-left">
|
|
<i class="fas fa-users"></i>
|
|
</div>
|
|
<p class="help">The local user which this backend is associated with.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label">Name</label>
|
|
<div class="control has-icons-left">
|
|
<input class="input" type="text" v-model="backend.name" required readonly disabled>
|
|
<div class="icon is-left">
|
|
<i class="fas fa-id-badge"></i>
|
|
</div>
|
|
<p class="help">
|
|
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
|
|
case a-z, 0-9 and _</code> only.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label">Type</label>
|
|
<div class="control has-icons-left">
|
|
<input class="input" type="text" v-model="backend.type" readonly disabled>
|
|
<div class="icon is-left">
|
|
<i class="fas fa-server"></i>
|
|
</div>
|
|
<p class="help">
|
|
The backend server type. The supported types are <code>{{
|
|
supported.map(v => ucFirst(v)).join(', ')
|
|
}}</code>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label">URL</label>
|
|
<div class="control has-icons-left">
|
|
<div class="select is-fullwidth" v-if="servers.length > 0">
|
|
<select v-model="backend.url" class="is-capital" @change="updateIdentifier" required>
|
|
<option value="" disabled>Select Server</option>
|
|
<option v-for="server in servers" :key="server.uuid" :value="server.uri">
|
|
{{ server.name }} - {{ server.uri }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<input class="input" type="text" v-model="backend.url" v-else required>
|
|
<div class="icon is-left">
|
|
<i class="fas fa-link"></i>
|
|
</div>
|
|
<p class="help">
|
|
<template v-if="servers.length < 1">
|
|
Enter the URL of the backend. For example
|
|
<code v-if="'plex' === backend.type">http://192.168.8.11:32400</code>
|
|
<code v-else>http://192.168.8.100:8096</code>
|
|
.
|
|
</template>
|
|
<template v-else>
|
|
Those are the servers associated with the Plex Token. Select the server you want to use.
|
|
</template>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label">
|
|
<template v-if="'plex' !== backend.type">API Key</template>
|
|
<template v-else>X-Plex-Token</template>
|
|
</label>
|
|
<div class="field-body">
|
|
<div class="field">
|
|
<div class="field has-addons">
|
|
<div class="control is-expanded has-icons-left">
|
|
<input class="input" v-model="backend.token" required
|
|
:type="false === exposeToken ? 'password' : 'text'">
|
|
<div class="icon is-left">
|
|
<i class="fas fa-key"></i>
|
|
</div>
|
|
</div>
|
|
<div class="control">
|
|
<button type="button" class="button is-primary" @click="exposeToken = !exposeToken"
|
|
v-tooltip="'Toggle token'">
|
|
<span class="icon" v-if="!exposeToken"><i class="fas fa-eye"></i></span>
|
|
<span class="icon" v-else><i class="fas fa-eye-slash"></i></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p class="help">
|
|
<template v-if="'plex' === backend.type">
|
|
Enter the <code>X-Plex-Token</code>.
|
|
<NuxtLink target="_blank"
|
|
to="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/"
|
|
v-text="'Visit This article for more information.'" />
|
|
</template>
|
|
<template v-else>
|
|
You can generate a new API key from <code>Dashboard > Settings > API Keys</code>.
|
|
</template>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label">Backend Unique ID</label>
|
|
<div class="control has-icons-left">
|
|
<input class="input" type="text" v-model="backend.uuid" required :disabled="isLimitedToken">
|
|
<div class="icon is-left">
|
|
<i class="fas fa-cloud" v-if="!uuidLoading"></i>
|
|
<i class="fas fa-spinner fa-pulse" v-else></i>
|
|
</div>
|
|
<p class="help">
|
|
<span v-if="'plex' === backend.type">
|
|
The backend unique ID is random string generated on server setup, In Plex case it used to inquiry
|
|
about the users associated with the server to generate limited <code>X-Plex-Token</code> for them.
|
|
It
|
|
used by webhooks as a filter to match the backend. in-case you are member of multiple servers.
|
|
</span>
|
|
<span v-else>
|
|
The backend unique ID is a random string generated on server setup. It is used to identify the
|
|
backend
|
|
uniquely. This is used for webhook matching and filtering.
|
|
</span>
|
|
<NuxtLink @click="getUUid" v-if="!isLimitedToken" v-text="'Get from the backend.'" />
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label">
|
|
<template v-if="users.length > 0">Associated User</template>
|
|
<template v-else>User ID</template>
|
|
</label>
|
|
<div class="control has-icons-left">
|
|
<div class="select is-fullwidth" v-if="users.length > 0">
|
|
<select v-model="backend.user" class="is-capitalized" :disabled="isLimitedToken">
|
|
<option v-for="user in users" :key="'uid-' + user.id" :value="user.id">
|
|
{{ user.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<input class="input" type="text" v-model="backend.user" v-else>
|
|
<div class="icon is-left">
|
|
<i class="fas fa-user-tie" v-if="!usersLoading"></i>
|
|
<i class="fas fa-spinner fa-pulse" v-else></i>
|
|
</div>
|
|
<p class="help">
|
|
<span v-if="'plex' === backend.type">
|
|
Plex doesn't use standard API practice for identifying users. They use <code>X-Plex-Token</code>
|
|
to identify the user. The list can only be populated if the user is admin or has
|
|
<code>ADMIN_TOKEN</code> set in Additional options.
|
|
</span>
|
|
<span v-else>
|
|
Which <code>{{ ucFirst(backend.type) }}</code> user should this backend use? The User ID will
|
|
determine the data we get from the backend. And for webhook matching and filtering.
|
|
</span>
|
|
<a href="javascript:void(0)" @click="getUsers" v-if="!isLimitedToken">
|
|
Get users.
|
|
</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field" v-if="backend.import">
|
|
<label class="label" for="backend_import">Import data from this backend?</label>
|
|
<div class="control">
|
|
<input id="backend_import" type="checkbox" class="switch is-success" v-model="backend.import.enabled">
|
|
<label for="backend_import">Enable</label>
|
|
<p class="help">
|
|
Import means to get the data from the backend and store it in the database.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field" v-if="backend.import && !backend.import.enabled">
|
|
<label class="label" for="backend_import_metadata">Import metadata only from this backend?</label>
|
|
<div class="control">
|
|
<input id="backend_import_metadata" type="checkbox" class="switch is-success"
|
|
v-model="backend.options.IMPORT_METADATA_ONLY">
|
|
<label for="backend_import_metadata">Enable</label>
|
|
<p class="help has-text-danger">
|
|
To efficiently push changes to the backend we need relation map and this require
|
|
us to get metadata from the backend. You have Importing disabled, as such this option
|
|
allow us to import this backend metadata without altering your play state.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field" v-if="backend.export">
|
|
<label class="label" for="backend_export">Export data to this backend?</label>
|
|
<div class="control">
|
|
<input id="backend_export" type="checkbox" class="switch is-success" v-model="backend.export.enabled">
|
|
<label for="backend_export">Enable</label>
|
|
<p class="help">
|
|
Export means to send the data from the database to this backend.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field" v-if="backend.webhook">
|
|
<label class="label" for="webhook_match_user">Webhook match user</label>
|
|
<div class="control">
|
|
<input id="webhook_match_user" type="checkbox" class="switch is-success"
|
|
v-model="backend.webhook.match.user">
|
|
<label for="webhook_match_user">Enable</label>
|
|
<p class="help">
|
|
Check webhook payload for user id match. if it does not match, the payload will be ignored.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field" v-if="backend.webhook">
|
|
<label class="label" for="webhook_match_uuid">Webhook match backend id</label>
|
|
<div class="control">
|
|
<input id="webhook_match_uuid" type="checkbox" class="switch is-success"
|
|
v-model="backend.webhook.match.uuid">
|
|
<label for="webhook_match_uuid">Enable</label>
|
|
<p class="help">
|
|
Check webhook payload for backend unique id. if it does not match, the payload will be ignored.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label is-clickable" @click="showOptions = !showOptions">
|
|
<span class="icon-text">
|
|
<span class="icon">
|
|
<i v-if="showOptions" class="fas fa-arrow-up"></i>
|
|
<i v-else class="fas fa-arrow-down"></i>
|
|
</span>
|
|
<span>Additional options...</span>
|
|
</span>
|
|
</label>
|
|
<template v-if="showOptions">
|
|
<div class="columns is-multiline is-mobile">
|
|
<template v-for="_option in flatOptionPaths" :key="'bo-'+_option">
|
|
<div class="column is-5">
|
|
<input type="text" class="input" :value="_option" readonly disabled>
|
|
<p class="help is-unselectable">
|
|
<span class="icon has-text-info">
|
|
<i class="fas fa-info-circle" :class="{ 'fa-bounce': newOptions[_option] }"></i>
|
|
</span>
|
|
{{ option_describe(_option) }}
|
|
</p>
|
|
</div>
|
|
<div class="column is-6">
|
|
<input type="text" class="input" :value="option_get(_option)"
|
|
@input="e => option_set(_option, e.target.value)" required>
|
|
</div>
|
|
<div class="column is-1">
|
|
<button class="button is-danger" @click.prevent="removeOption(_option)">
|
|
<span class="icon"><i class="fas fa-trash" /></span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div class="columns is-multiline is-mobile">
|
|
<div class="column is-12">
|
|
<span class="icon-text">
|
|
<span class="icon"><i class="fas fa-plus"></i></span>
|
|
<span>Add new option</span>
|
|
</span>
|
|
</div>
|
|
<div class="column is-5">
|
|
<div class="select is-fullwidth">
|
|
<select v-model="selectedOption">
|
|
<option value="">Select Option</option>
|
|
<option v-for="option in filteredOptions(optionsList)" :key="'opt-' + option.key"
|
|
:value="option.key">
|
|
{{ option.key }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="column is-6">
|
|
{{ selectedOptionHelp }}
|
|
</div>
|
|
<div class="column is-1">
|
|
<button class="button is-primary" @click.prevent="addOption">
|
|
<span class="icon">
|
|
<i class="fas fa-add"></i>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer">
|
|
<button class="button card-footer-item is-fullwidth is-primary" type="submit">
|
|
<span class="icon"><i class="fas fa-save"></i></span>
|
|
<span>Save Settings</span>
|
|
</button>
|
|
<NuxtLink class="card-footer-item button is-fullwidth is-danger" :to="`/backend/${id}`">
|
|
<span class="icon"><i class="fas fa-cancel"></i></span>
|
|
<span>Cancel changes</span>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import 'assets/css/bulma-switch.css'
|
|
import { notification, ucFirst } from '~/utils/index'
|
|
import Message from '~/components/Message'
|
|
import { useStorage } from "@vueuse/core";
|
|
import request from "~/utils/request.js";
|
|
|
|
const id = useRoute().params.backend
|
|
const redirect = useRoute().query?.redirect ?? `/backend/${id}`
|
|
|
|
const backend = ref({
|
|
name: '',
|
|
type: '',
|
|
url: '',
|
|
token: '',
|
|
uuid: '',
|
|
user: '',
|
|
import: { enabled: false },
|
|
export: { enabled: false },
|
|
webhook: { match: { user: false, uuid: false } },
|
|
options: {}
|
|
})
|
|
|
|
const showOptions = ref(false)
|
|
const isLoading = ref(true)
|
|
const users = ref([])
|
|
const supported = ref([])
|
|
const usersLoading = ref(false)
|
|
const uuidLoading = ref(false)
|
|
const optionsList = ref([])
|
|
const selectedOption = ref('')
|
|
const newOptions = ref({})
|
|
const exposeToken = ref(false)
|
|
const servers = ref([])
|
|
const serversLoading = ref(false)
|
|
const isLimitedToken = computed(() => Boolean(backend.value.options?.is_limited_token))
|
|
const api_user = useStorage('api_user', 'main')
|
|
|
|
const selectedOptionHelp = computed(() => {
|
|
const option = optionsList.value.find(v => v.key === selectedOption.value)
|
|
return option ? option.description : ''
|
|
});
|
|
|
|
useHead({ title: 'Backends - Edit: ' + id })
|
|
|
|
const loadContent = async () => {
|
|
supported.value = await (await request('/system/supported')).json()
|
|
|
|
const content = await request(`/backend/${id}`)
|
|
let json = await content.json()
|
|
|
|
if (!json?.options || typeof json.options !== 'object') {
|
|
json.options = {}
|
|
}
|
|
|
|
backend.value = json;
|
|
|
|
if ('plex' === backend.value.type) {
|
|
await getServers()
|
|
}
|
|
|
|
await getUsers()
|
|
|
|
isLoading.value = false
|
|
}
|
|
|
|
const saveContent = async () => {
|
|
const json_text = toRaw(backend.value)
|
|
|
|
const flat = {}
|
|
flatOptionPaths.value.forEach(path => flat[path] = option_get(path))
|
|
|
|
if (Object.keys(flat).length > 0) {
|
|
json_text.options = flat
|
|
}
|
|
|
|
try {
|
|
const response = await request(`/backend/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(json_text)
|
|
})
|
|
|
|
const json = await response.json()
|
|
if (200 !== response.status) {
|
|
notification('error', 'Error', `Failed to save backend settings. (${json.error.code}: ${json.error.message}).`)
|
|
return
|
|
}
|
|
|
|
notification('success', 'Success', `Successfully updated '${id}' settings.`)
|
|
const to = !redirect.startsWith('/') ? `/backend/${id}` : redirect
|
|
await navigateTo({ path: to })
|
|
} catch (e) {
|
|
notification('error', 'Error', `Request error. ${e.message}`)
|
|
}
|
|
}
|
|
|
|
const removeOption = async (key) => {
|
|
if (newOptions.value[key]) {
|
|
delete newOptions.value[key]
|
|
delete backend.value.options[key]
|
|
return
|
|
}
|
|
|
|
if (!confirm(`Are you sure you want to remove this option '${key}'?`)) {
|
|
return
|
|
}
|
|
|
|
const response = await request(`/backend/${id}/option/options.${key}`, { method: 'DELETE' })
|
|
|
|
if (!response.ok) {
|
|
const json = await response.json()
|
|
notification('error', 'Error', `Failed to remove the option. (${json.error.code}: ${json.error.message}).`)
|
|
return
|
|
}
|
|
|
|
notification('success', 'Information', `Option [${key}] removed successfully.`)
|
|
delete backend.value.options[key]
|
|
}
|
|
|
|
const addOption = async () => {
|
|
if (!selectedOption.value) {
|
|
notification('error', 'Error', 'Please select an option to add.')
|
|
return
|
|
}
|
|
|
|
backend.value.options = backend.value.options || {}
|
|
option_set(selectedOption.value, '')
|
|
newOptions.value[selectedOption.value] = true
|
|
selectedOption.value = ''
|
|
}
|
|
|
|
const getUUid = async () => {
|
|
const required_values = ['type', 'token', 'url'];
|
|
|
|
if (required_values.some(v => !backend.value[v])) {
|
|
notification('error', 'Error', `Please fill all the required fields. ${required_values.join(', ')}.`)
|
|
return
|
|
}
|
|
|
|
uuidLoading.value = true
|
|
|
|
const response = await request(`/backends/uuid/${backend.value.type}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
token: backend.value.token,
|
|
url: backend.value.url
|
|
})
|
|
})
|
|
|
|
const json = await response.json()
|
|
uuidLoading.value = false
|
|
|
|
if (!response.ok) {
|
|
notification('error', 'Error', 'Failed to get the UUID from the backend.')
|
|
return
|
|
}
|
|
|
|
backend.value.uuid = json.identifier
|
|
}
|
|
|
|
const getUsers = async (showAlert = true) => {
|
|
const required_values = ['type', 'token', 'url', 'uuid'];
|
|
|
|
if (required_values.some(v => !backend.value[v])) {
|
|
if (showAlert) {
|
|
notification('error', 'Error', `Please fill all the required fields. ${required_values.join(', ')}.`)
|
|
}
|
|
return
|
|
}
|
|
|
|
usersLoading.value = true
|
|
|
|
let data = {
|
|
token: backend.value.token,
|
|
url: backend.value.url,
|
|
uuid: backend.value.uuid,
|
|
user: backend.value.user
|
|
};
|
|
|
|
if (backend.value.options && backend.value.options?.ADMIN_TOKEN) {
|
|
data.options = {
|
|
ADMIN_TOKEN: backend.value.options.ADMIN_TOKEN
|
|
}
|
|
}
|
|
|
|
if (backend.value.options && backend.value.options?.is_limited_token) {
|
|
data.options = {
|
|
is_limited_token: Boolean(backend.value.options.is_limited_token)
|
|
}
|
|
}
|
|
|
|
const response = await request(`/backends/users/${backend.value.type}?tokens=1`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
})
|
|
|
|
const json = await response.json()
|
|
|
|
usersLoading.value = false
|
|
|
|
if (200 !== response.status) {
|
|
notification('error', 'Error', `${json.error.code}: ${json.error.message}`)
|
|
return
|
|
}
|
|
|
|
users.value = json
|
|
}
|
|
|
|
watch(showOptions, async value => {
|
|
if (!value) {
|
|
return
|
|
}
|
|
if (optionsList.value.length > 0) {
|
|
return
|
|
}
|
|
|
|
const response = await request(`/backends/spec`)
|
|
const json = await response.json()
|
|
json.forEach(v => {
|
|
if (false === v.key.startsWith('options.')) {
|
|
return
|
|
}
|
|
v['key'] = v.key.replace('options.', '')
|
|
optionsList.value.push(v)
|
|
})
|
|
});
|
|
|
|
const filteredOptions = options => {
|
|
if (!options) {
|
|
return []
|
|
}
|
|
return options.filter(v => !backend.value.options[v.key] && !newOptions.value[v.key])
|
|
}
|
|
|
|
const getServers = async () => {
|
|
if ('plex' !== backend.value.type || servers.value.length > 0) {
|
|
return
|
|
}
|
|
|
|
if (!backend.value.token) {
|
|
notification('error', 'Error', `Token is required to get list of servers.`)
|
|
return
|
|
}
|
|
|
|
serversLoading.value = true
|
|
|
|
let data = {
|
|
token: backend.value.token,
|
|
url: window.location.origin,
|
|
};
|
|
|
|
if (backend.value?.options && backend.value.options?.ADMIN_TOKEN) {
|
|
data.options = {
|
|
ADMIN_TOKEN: backend.value.options.ADMIN_TOKEN
|
|
}
|
|
}
|
|
|
|
const response = await request(`/backends/discover/${backend.value.type}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
})
|
|
|
|
serversLoading.value = false
|
|
|
|
const json = await response.json()
|
|
|
|
if (200 !== response.status) {
|
|
notification('error', 'Error', `${json.error.code}: ${json.error.message}`)
|
|
return
|
|
}
|
|
|
|
servers.value = json
|
|
}
|
|
|
|
const updateIdentifier = async () => {
|
|
backend.value.uuid = servers.value.find(s => s.uri === backend.value.url).identifier
|
|
await getUsers()
|
|
}
|
|
|
|
watch(() => backend.value.user, async () => {
|
|
if (users.value.length < 1 || 'plex' !== backend.value.type) {
|
|
return
|
|
}
|
|
|
|
// -- get token for the user
|
|
users.value.forEach(u => {
|
|
if (u.id !== backend.value.user) {
|
|
return
|
|
}
|
|
|
|
if (u?.guest) {
|
|
backend.value.options.plex_external_user = true
|
|
} else {
|
|
if (backend.value.options?.plex_external_user) {
|
|
delete backend.value.options.plex_external_user
|
|
}
|
|
}
|
|
|
|
backend.value.options.plex_user_name = u.name
|
|
backend.value.options.plex_user_uuid = u.uuid
|
|
|
|
|
|
if (!u?.token) {
|
|
notification('error', 'Error', `User token not found`)
|
|
return
|
|
}
|
|
|
|
backend.value.token = u.token
|
|
})
|
|
})
|
|
|
|
const flattenOptions = (obj, prefix = '') => {
|
|
const out = []
|
|
|
|
for (const [key, val] of Object.entries(obj)) {
|
|
const path = prefix ? `${prefix}.${key}` : key
|
|
|
|
if (Array.isArray(val)) {
|
|
if (val.length === 0) {
|
|
continue
|
|
}
|
|
out.push(path)
|
|
continue
|
|
}
|
|
|
|
if (val !== null && typeof val === 'object') {
|
|
if (Object.keys(val).length === 0) {
|
|
continue
|
|
}
|
|
out.push(...flattenOptions(val, path))
|
|
continue
|
|
}
|
|
|
|
out.push(path)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
const flatOptionPaths = computed(() => flattenOptions(backend.value.options))
|
|
|
|
const option_get = path => path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), backend.value.options)
|
|
const option_set = (path, value) => {
|
|
const keys = path.split('.')
|
|
const last = keys.pop()
|
|
let target = backend.value.options
|
|
for (const k of keys) {
|
|
if (target[k] == null || typeof target[k] !== 'object' || Array.isArray(target[k])) {
|
|
target[k] = {}
|
|
}
|
|
target = target[k]
|
|
}
|
|
|
|
target[last] = value
|
|
}
|
|
|
|
const option_describe = path => {
|
|
const item = optionsList.value.find((v) => v.key === path)
|
|
return item ? item.description : ''
|
|
}
|
|
|
|
onMounted(async () => await loadContent())
|
|
|
|
</script>
|