feat: dynamic extension loading via routes.json (#3605)

This commit is contained in:
dni ⚡ 2025-12-08 11:28:09 +01:00 committed by GitHub
parent 5f86627eae
commit fd765e2060
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 91 additions and 31 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -9,27 +9,18 @@ window.app.component('lnbits-manage-extension-list', {
}
},
watch: {
'g.user.extensions': {
async handler() {
await this.loadExtensions()
}
'g.user.extensions'() {
this.loadExtensions()
},
searchTerm() {
this.filterUserExtensionsByTerm()
}
},
methods: {
map(data) {
const obj = {...data}
obj.url = ['/', obj.code, '/'].join('')
return obj
},
async loadExtensions() {
try {
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
this.extensions = data
.map(extension => this.map(extension))
.sort((a, b) => a.name.localeCompare(b.name))
res = await LNbits.api.request('GET', '/api/v1/extension')
this.extensions = res.data.sort((a, b) => a.name.localeCompare(b.name))
this.filterUserExtensionsByTerm()
} catch (error) {
LNbits.utils.notifyApiError(error)

View file

@ -9,6 +9,46 @@ const quasarConfig = {
}
}
const DynamicComponent = {
async created() {
const name = this.$route.path.split('/')[1]
const path = `/${name}/`
const routesPath = `/${name}/static/routes.json`
const hasPath = this.$router.getRoutes().some(r => r.path === path)
if (hasPath) {
console.log('Dynamic route already exists for extension:', name)
return
}
fetch(routesPath)
.then(async res => {
if (!res.ok) {
throw new Error('No dynamic routes found')
}
const routes = await res.json()
routes.forEach(r => {
console.log('Adding dynamic route:', r.path)
window.router.addRoute({
path: r.path,
name: r.name,
component: async () => {
await LNbits.utils.loadTemplate(r.template)
await LNbits.utils.loadScript(r.component)
return window[r.name]
}
})
window.router.push(this.$route.fullPath)
})
})
.catch(() => {
if (RENDERED_ROUTE !== path) {
console.log('Redirecting to non-vue route:', path)
window.location = path
return
}
})
}
}
const routes = [
{
path: '/node',
@ -87,6 +127,11 @@ const routes = [
path: '/error',
name: 'PageError',
component: PageError
},
{
path: '/:pathMatch(.*)*',
name: 'DynamicComponent',
component: DynamicComponent
}
]
@ -107,18 +152,9 @@ window.app.mixin({
api: window._lnbitsApi,
utils: window._lnbitsUtils,
g: window.g,
utils: window._lnbitsUtils,
...WINDOW_SETTINGS
}
},
computed: {
isVueRoute() {
const currentPath = window.location.pathname
const matchedRoute = window.router.resolve(currentPath)
const isVueRoute = matchedRoute?.matched?.length > 0
return isVueRoute
}
},
// backwards compatibility for extensions, should not be used in the future
methods: {
copyText: window._lnbitsUtils.copyText,

View file

@ -1,4 +1,31 @@
window._lnbitsUtils = {
loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.onload = () => {
resolve()
}
script.onerror = () => {
reject(new Error(`Failed to load script ${src}`))
}
document.body.appendChild(script)
})
},
async loadTemplate(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`Failed to load template from ${url}`)
}
return response.text()
})
.then(html => {
const template = document.createElement('div')
template.innerHTML = html.trim()
document.body.appendChild(template)
})
},
copyText(text, message, position) {
Quasar.copyToClipboard(text).then(() => {
Quasar.Notify.create({

View file

@ -51,9 +51,12 @@
<lnbits-header-wallets
v-if="g.user && !g.isPublicPage"
></lnbits-header-wallets>
<router-view v-if="isVueRoute" :key="$route.path"></router-view>
<!-- FastAPI Content from extensions -->
<div v-else>{% block page %}{% endblock %}</div>
<!-- block page content from static extensions -->
<div v-if="$route.path === '{{ request.path }}'">
{% block page %}{% endblock %}
</div>
<!-- vue router-view -->
<router-view :key="$route.path"></router-view>
</q-page>
</q-page-container>
{% endblock %}
@ -64,6 +67,7 @@
</q-layout>
</div>
<script type="text/javascript">
const RENDERED_ROUTE = '{{ request.path }}'
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
Object.keys(WINDOW_SETTINGS).forEach(key => {
window[key] = WINDOW_SETTINGS[key]

View file

@ -17,11 +17,10 @@
</q-item>
<q-item
v-for="extension in userExtensions"
:key="extension.code"
clickable
:active="$route.path.startsWith(extension.url)"
:active="$route.path.startsWith('/' + extension.code)"
tag="a"
:href="extension.url"
:to="'/' + extension.code + '/'"
>
<q-item-section side>
<q-avatar size="md">
@ -33,7 +32,10 @@
><span v-text="extension.name"></span>
</q-item-label>
</q-item-section>
<q-item-section side v-show="$route.path.startsWith(extension.url)">
<q-item-section
side
v-show="$route.path.startsWith('/' + extension.code)"
>
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>