feat: dynamic extension loading via routes.json (#3605)
This commit is contained in:
parent
5f86627eae
commit
fd765e2060
7 changed files with 91 additions and 31 deletions
2
lnbits/static/bundle-components.min.js
vendored
2
lnbits/static/bundle-components.min.js
vendored
File diff suppressed because one or more lines are too long
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -9,27 +9,18 @@ window.app.component('lnbits-manage-extension-list', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'g.user.extensions': {
|
'g.user.extensions'() {
|
||||||
async handler() {
|
this.loadExtensions()
|
||||||
await this.loadExtensions()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
searchTerm() {
|
searchTerm() {
|
||||||
this.filterUserExtensionsByTerm()
|
this.filterUserExtensionsByTerm()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
map(data) {
|
|
||||||
const obj = {...data}
|
|
||||||
obj.url = ['/', obj.code, '/'].join('')
|
|
||||||
return obj
|
|
||||||
},
|
|
||||||
async loadExtensions() {
|
async loadExtensions() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
|
res = await LNbits.api.request('GET', '/api/v1/extension')
|
||||||
this.extensions = data
|
this.extensions = res.data.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map(extension => this.map(extension))
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
this.filterUserExtensionsByTerm()
|
this.filterUserExtensionsByTerm()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/node',
|
path: '/node',
|
||||||
|
|
@ -87,6 +127,11 @@ const routes = [
|
||||||
path: '/error',
|
path: '/error',
|
||||||
name: 'PageError',
|
name: 'PageError',
|
||||||
component: PageError
|
component: PageError
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'DynamicComponent',
|
||||||
|
component: DynamicComponent
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -107,18 +152,9 @@ window.app.mixin({
|
||||||
api: window._lnbitsApi,
|
api: window._lnbitsApi,
|
||||||
utils: window._lnbitsUtils,
|
utils: window._lnbitsUtils,
|
||||||
g: window.g,
|
g: window.g,
|
||||||
utils: window._lnbitsUtils,
|
|
||||||
...WINDOW_SETTINGS
|
...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
|
// backwards compatibility for extensions, should not be used in the future
|
||||||
methods: {
|
methods: {
|
||||||
copyText: window._lnbitsUtils.copyText,
|
copyText: window._lnbitsUtils.copyText,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,31 @@
|
||||||
window._lnbitsUtils = {
|
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) {
|
copyText(text, message, position) {
|
||||||
Quasar.copyToClipboard(text).then(() => {
|
Quasar.copyToClipboard(text).then(() => {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,12 @@
|
||||||
<lnbits-header-wallets
|
<lnbits-header-wallets
|
||||||
v-if="g.user && !g.isPublicPage"
|
v-if="g.user && !g.isPublicPage"
|
||||||
></lnbits-header-wallets>
|
></lnbits-header-wallets>
|
||||||
<router-view v-if="isVueRoute" :key="$route.path"></router-view>
|
<!-- block page content from static extensions -->
|
||||||
<!-- FastAPI Content from extensions -->
|
<div v-if="$route.path === '{{ request.path }}'">
|
||||||
<div v-else>{% block page %}{% endblock %}</div>
|
{% block page %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
<!-- vue router-view -->
|
||||||
|
<router-view :key="$route.path"></router-view>
|
||||||
</q-page>
|
</q-page>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -64,6 +67,7 @@
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
const RENDERED_ROUTE = '{{ request.path }}'
|
||||||
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
|
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
|
||||||
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||||
window[key] = WINDOW_SETTINGS[key]
|
window[key] = WINDOW_SETTINGS[key]
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,10 @@
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
v-for="extension in userExtensions"
|
v-for="extension in userExtensions"
|
||||||
:key="extension.code"
|
|
||||||
clickable
|
clickable
|
||||||
:active="$route.path.startsWith(extension.url)"
|
:active="$route.path.startsWith('/' + extension.code)"
|
||||||
tag="a"
|
tag="a"
|
||||||
:href="extension.url"
|
:to="'/' + extension.code + '/'"
|
||||||
>
|
>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-avatar size="md">
|
<q-avatar size="md">
|
||||||
|
|
@ -33,7 +32,10 @@
|
||||||
><span v-text="extension.name"></span>
|
><span v-text="extension.name"></span>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</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-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue