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
7 changed files with 91 additions and 31 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -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)
+45 -9
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,
+27
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({
+7 -3
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]
@@ -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>