feat: inherit from wuttaweb for AppInfoView, appinfo/configure template
This commit is contained in:
parent
2ffc067097
commit
f755460242
|
@ -703,7 +703,7 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
||||||
},
|
},
|
||||||
{'type': 'sep'},
|
{'type': 'sep'},
|
||||||
{
|
{
|
||||||
'title': "App Details",
|
'title': "App Info",
|
||||||
'route': 'appinfo',
|
'route': 'appinfo',
|
||||||
'perm': 'appinfo.list',
|
'perm': 'appinfo.list',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,247 +1,2 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/configure.mako" />
|
<%inherit file="wuttaweb:templates/appinfo/configure.mako" />
|
||||||
|
|
||||||
<%def name="form_content()">
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Basics</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<b-field grouped>
|
|
||||||
|
|
||||||
<b-field label="App Title">
|
|
||||||
<b-input name="rattail.app_title"
|
|
||||||
v-model="simpleSettings['rattail.app_title']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Node Type">
|
|
||||||
## TODO: should be a dropdown, app handler defines choices
|
|
||||||
<b-input name="rattail.node_type"
|
|
||||||
v-model="simpleSettings['rattail.node_type']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Node Title">
|
|
||||||
<b-input name="rattail.node_title"
|
|
||||||
v-model="simpleSettings['rattail.node_title']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field>
|
|
||||||
<b-checkbox name="rattail.production"
|
|
||||||
v-model="simpleSettings['rattail.production']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
Production Mode
|
|
||||||
</b-checkbox>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<div class="level-left">
|
|
||||||
<div class="level-item">
|
|
||||||
<b-field>
|
|
||||||
<b-checkbox name="rattail.running_from_source"
|
|
||||||
v-model="simpleSettings['rattail.running_from_source']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
Running from Source
|
|
||||||
</b-checkbox>
|
|
||||||
</b-field>
|
|
||||||
</div>
|
|
||||||
<div class="level-item">
|
|
||||||
<b-field label="Top-Level Package" horizontal
|
|
||||||
v-if="simpleSettings['rattail.running_from_source']">
|
|
||||||
<b-input name="rattail.running_from_source.rootpkg"
|
|
||||||
v-model="simpleSettings['rattail.running_from_source.rootpkg']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Display</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<b-field grouped>
|
|
||||||
|
|
||||||
<b-field label="Background Color">
|
|
||||||
<b-input name="tailbone.background_color"
|
|
||||||
v-model="simpleSettings['tailbone.background_color']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Grids</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<b-field grouped>
|
|
||||||
|
|
||||||
<b-field label="Default Page Size">
|
|
||||||
<b-input name="tailbone.grid.default_pagesize"
|
|
||||||
v-model="simpleSettings['tailbone.grid.default_pagesize']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Web Libraries</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<${b}-table :data="weblibs">
|
|
||||||
|
|
||||||
<${b}-table-column field="title"
|
|
||||||
label="Name"
|
|
||||||
v-slot="props">
|
|
||||||
{{ props.row.title }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column field="configured_version"
|
|
||||||
label="Version"
|
|
||||||
v-slot="props">
|
|
||||||
{{ props.row.configured_version || props.row.default_version }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column field="configured_url"
|
|
||||||
label="URL Override"
|
|
||||||
v-slot="props">
|
|
||||||
{{ props.row.configured_url }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column field="live_url"
|
|
||||||
label="Effective (Live) URL"
|
|
||||||
v-slot="props">
|
|
||||||
<span v-if="props.row.modified"
|
|
||||||
class="has-text-warning">
|
|
||||||
save settings and refresh page to see new URL
|
|
||||||
</span>
|
|
||||||
<span v-if="!props.row.modified">
|
|
||||||
{{ props.row.live_url }}
|
|
||||||
</span>
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column field="actions"
|
|
||||||
label="Actions"
|
|
||||||
v-slot="props">
|
|
||||||
<a href="#"
|
|
||||||
@click.prevent="editWebLibraryInit(props.row)">
|
|
||||||
% if request.use_oruga:
|
|
||||||
<o-icon icon="edit" />
|
|
||||||
% else:
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
% endif
|
|
||||||
Edit
|
|
||||||
</a>
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
</${b}-table>
|
|
||||||
|
|
||||||
% for weblib in weblibs:
|
|
||||||
${h.hidden('wuttaweb.libver.{}'.format(weblib['key']), **{':value': "simpleSettings['wuttaweb.libver.{}']".format(weblib['key'])})}
|
|
||||||
${h.hidden('wuttaweb.liburl.{}'.format(weblib['key']), **{':value': "simpleSettings['wuttaweb.liburl.{}']".format(weblib['key'])})}
|
|
||||||
% endfor
|
|
||||||
|
|
||||||
<${b}-modal has-modal-card
|
|
||||||
% if request.use_oruga:
|
|
||||||
v-model:active="editWebLibraryShowDialog"
|
|
||||||
% else:
|
|
||||||
:active.sync="editWebLibraryShowDialog"
|
|
||||||
% endif
|
|
||||||
>
|
|
||||||
<div class="modal-card">
|
|
||||||
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<p class="modal-card-title">Web Library: {{ editWebLibraryRecord.title }}</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="modal-card-body">
|
|
||||||
|
|
||||||
<b-field grouped>
|
|
||||||
|
|
||||||
<b-field label="Default Version">
|
|
||||||
<b-input v-model="editWebLibraryRecord.default_version"
|
|
||||||
disabled>
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Override Version">
|
|
||||||
<b-input v-model="editWebLibraryVersion">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Override URL">
|
|
||||||
<b-input v-model="editWebLibraryURL"
|
|
||||||
expanded />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Effective URL (as of last page load)">
|
|
||||||
<b-input v-model="editWebLibraryRecord.live_url"
|
|
||||||
disabled
|
|
||||||
expanded />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<b-button type="is-primary"
|
|
||||||
@click="editWebLibrarySave()"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="save">
|
|
||||||
Save
|
|
||||||
</b-button>
|
|
||||||
<b-button @click="editWebLibraryShowDialog = false">
|
|
||||||
Cancel
|
|
||||||
</b-button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</${b}-modal>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="modify_vue_vars()">
|
|
||||||
${parent.modify_vue_vars()}
|
|
||||||
<script>
|
|
||||||
|
|
||||||
ThisPageData.weblibs = ${json.dumps(weblibs)|n}
|
|
||||||
|
|
||||||
ThisPageData.editWebLibraryShowDialog = false
|
|
||||||
ThisPageData.editWebLibraryRecord = {}
|
|
||||||
ThisPageData.editWebLibraryVersion = null
|
|
||||||
ThisPageData.editWebLibraryURL = null
|
|
||||||
|
|
||||||
ThisPage.methods.editWebLibraryInit = function(row) {
|
|
||||||
this.editWebLibraryRecord = row
|
|
||||||
this.editWebLibraryVersion = row.configured_version
|
|
||||||
this.editWebLibraryURL = row.configured_url
|
|
||||||
this.editWebLibraryShowDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.methods.editWebLibrarySave = function() {
|
|
||||||
this.editWebLibraryRecord.configured_version = this.editWebLibraryVersion
|
|
||||||
this.editWebLibraryRecord.configured_url = this.editWebLibraryURL
|
|
||||||
this.editWebLibraryRecord.modified = true
|
|
||||||
|
|
||||||
this.simpleSettings[`wuttaweb.libver.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryVersion
|
|
||||||
this.simpleSettings[`wuttaweb.liburl.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryURL
|
|
||||||
|
|
||||||
this.settingsNeedSaved = true
|
|
||||||
this.editWebLibraryShowDialog = false
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</%def>
|
|
||||||
|
|
|
@ -666,6 +666,7 @@
|
||||||
<%def name="make_b_tooltip_component()">
|
<%def name="make_b_tooltip_component()">
|
||||||
<script type="text/x-template" id="b-tooltip-template">
|
<script type="text/x-template" id="b-tooltip-template">
|
||||||
<o-tooltip :label="label"
|
<o-tooltip :label="label"
|
||||||
|
:position="orugaPosition"
|
||||||
:multiline="multilined">
|
:multiline="multilined">
|
||||||
<slot />
|
<slot />
|
||||||
</o-tooltip>
|
</o-tooltip>
|
||||||
|
@ -676,6 +677,14 @@
|
||||||
props: {
|
props: {
|
||||||
label: String,
|
label: String,
|
||||||
multilined: Boolean,
|
multilined: Boolean,
|
||||||
|
position: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
orugaPosition() {
|
||||||
|
if (this.position) {
|
||||||
|
return this.position.replace(/^is-/, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -25,11 +25,7 @@ Settings Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
|
|
||||||
|
@ -37,201 +33,77 @@ from rattail.db.model import Setting
|
||||||
from rattail.settings import Setting as AppSetting
|
from rattail.settings import Setting as AppSetting
|
||||||
from rattail.util import import_module_path
|
from rattail.util import import_module_path
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms, grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView, View
|
from tailbone.views import MasterView, View
|
||||||
from wuttaweb.util import get_libver, get_liburl
|
from wuttaweb.util import get_libver, get_liburl
|
||||||
|
from wuttaweb.views.settings import AppInfoView as WuttaAppInfoView
|
||||||
|
|
||||||
|
|
||||||
class AppInfoView(MasterView):
|
class AppInfoView(WuttaAppInfoView):
|
||||||
"""
|
|
||||||
Master view for the overall app, to show/edit config etc.
|
|
||||||
"""
|
|
||||||
route_prefix = 'appinfo'
|
|
||||||
model_key = 'UNUSED'
|
|
||||||
model_title = "UNUSED"
|
|
||||||
model_title_plural = "App Details"
|
|
||||||
creatable = False
|
|
||||||
viewable = False
|
|
||||||
editable = False
|
|
||||||
deletable = False
|
|
||||||
filterable = False
|
|
||||||
pageable = False
|
|
||||||
configurable = True
|
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
'name',
|
|
||||||
'version',
|
|
||||||
'editable_project_location',
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_index_title(self):
|
|
||||||
app = self.get_rattail_app()
|
|
||||||
return "{} for {}".format(self.get_model_title_plural(),
|
|
||||||
app.get_title())
|
|
||||||
|
|
||||||
def get_data(self, session=None):
|
|
||||||
""" """
|
""" """
|
||||||
|
Session = Session
|
||||||
|
weblib_config_prefix = 'tailbone'
|
||||||
|
|
||||||
# nb. init with empty data, only load it upon user request
|
# TODO: for now we override to get tailbone searchable grid
|
||||||
if not self.request.GET.get('partial'):
|
def make_grid(self, **kwargs):
|
||||||
return []
|
""" """
|
||||||
|
return grids.Grid(self.request, **kwargs)
|
||||||
# TODO: pretty sure this is not cross-platform. probably some
|
|
||||||
# sort of pip methods belong on the app handler? or it should
|
|
||||||
# have a pip handler for all that?
|
|
||||||
pip = os.path.join(sys.prefix, 'bin', 'pip')
|
|
||||||
output = subprocess.check_output([pip, 'list', '--format=json'])
|
|
||||||
data = json.loads(output.decode('utf_8').strip())
|
|
||||||
|
|
||||||
# must avoid null values for sort to work right
|
|
||||||
for pkg in data:
|
|
||||||
pkg.setdefault('editable_project_location', '')
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
""" """
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# sort on frontend
|
|
||||||
g.sort_on_backend = False
|
|
||||||
g.sort_multiple = False
|
|
||||||
g.set_sort_defaults('name')
|
|
||||||
|
|
||||||
# name
|
# name
|
||||||
g.set_searchable('name')
|
g.set_searchable('name')
|
||||||
|
|
||||||
# editable_project_location
|
# editable_project_location
|
||||||
g.set_searchable('editable_project_location')
|
g.set_searchable('editable_project_location')
|
||||||
|
|
||||||
def template_kwargs_index(self, **kwargs):
|
|
||||||
kwargs = super().template_kwargs_index(**kwargs)
|
|
||||||
kwargs['configure_button_title'] = "Configure App"
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def get_weblibs(self):
|
|
||||||
""" """
|
|
||||||
return OrderedDict([
|
|
||||||
('vue', "Vue"),
|
|
||||||
('vue_resource', "vue-resource"),
|
|
||||||
('buefy', "Buefy"),
|
|
||||||
('buefy.css', "Buefy CSS"),
|
|
||||||
('fontawesome', "FontAwesome"),
|
|
||||||
('bb_vue', "(BB) vue"),
|
|
||||||
('bb_oruga', "(BB) @oruga-ui/oruga-next"),
|
|
||||||
('bb_oruga_bulma', "(BB) @oruga-ui/theme-bulma (JS)"),
|
|
||||||
('bb_oruga_bulma_css', "(BB) @oruga-ui/theme-bulma (CSS)"),
|
|
||||||
('bb_fontawesome_svg_core', "(BB) @fortawesome/fontawesome-svg-core"),
|
|
||||||
('bb_free_solid_svg_icons', "(BB) @fortawesome/free-solid-svg-icons"),
|
|
||||||
('bb_vue_fontawesome', "(BB) @fortawesome/vue-fontawesome"),
|
|
||||||
])
|
|
||||||
|
|
||||||
def configure_get_context(self, **kwargs):
|
def configure_get_context(self, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
context = super().configure_get_context(**kwargs)
|
context = super().configure_get_context(**kwargs)
|
||||||
simple_settings = context['simple_settings']
|
simple_settings = context['simple_settings']
|
||||||
weblibs = self.get_weblibs()
|
weblibs = context['weblibs']
|
||||||
|
|
||||||
for key in weblibs:
|
for weblib in weblibs:
|
||||||
title = weblibs[key]
|
key = weblib['key']
|
||||||
weblibs[key] = {
|
|
||||||
'key': key,
|
|
||||||
'title': title,
|
|
||||||
|
|
||||||
# nb. these values are exactly as configured, and are
|
|
||||||
# used for editing the settings
|
|
||||||
'configured_version': get_libver(self.request, key,
|
|
||||||
prefix='tailbone',
|
|
||||||
configured_only=True),
|
|
||||||
'configured_url': get_liburl(self.request, key,
|
|
||||||
prefix='tailbone',
|
|
||||||
configured_only=True),
|
|
||||||
|
|
||||||
# these are for informational purposes only
|
|
||||||
'default_version': get_libver(self.request, key,
|
|
||||||
prefix='tailbone',
|
|
||||||
default_only=True),
|
|
||||||
'live_url': get_liburl(self.request, key,
|
|
||||||
prefix='tailbone'),
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO: this is only needed to migrate legacy settings to
|
# TODO: this is only needed to migrate legacy settings to
|
||||||
# use the newer wutaweb setting names
|
# use the newer wuttaweb setting names
|
||||||
url = simple_settings[f'wuttaweb.liburl.{key}']
|
url = simple_settings[f'wuttaweb.liburl.{key}']
|
||||||
if not url and weblibs[key]['configured_url']:
|
if not url and weblib['configured_url']:
|
||||||
simple_settings[f'wuttaweb.liburl.{key}'] = weblibs[key]['configured_url']
|
simple_settings[f'wuttaweb.liburl.{key}'] = weblib['configured_url']
|
||||||
|
|
||||||
context['weblibs'] = list(weblibs.values())
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def configure_get_simple_settings(self):
|
def configure_get_simple_settings(self):
|
||||||
""" """
|
""" """
|
||||||
simple_settings = [
|
simple_settings = super().configure_get_simple_settings()
|
||||||
|
|
||||||
# basics
|
# TODO: the update home page redirect setting is off by
|
||||||
{'section': 'rattail',
|
# default for wuttaweb, but on for tailbone
|
||||||
'option': 'app_title'},
|
for setting in simple_settings:
|
||||||
{'section': 'rattail',
|
if setting['name'] == 'wuttaweb.home_redirect_to_login':
|
||||||
'option': 'node_type'},
|
value = self.config.get_bool('wuttaweb.home_redirect_to_login')
|
||||||
{'section': 'rattail',
|
if value is None:
|
||||||
'option': 'node_title'},
|
value = self.config.get_bool('tailbone.login_is_home', default=True)
|
||||||
{'section': 'rattail',
|
setting['default'] = value
|
||||||
'option': 'production',
|
break
|
||||||
'type': bool},
|
|
||||||
{'section': 'rattail',
|
|
||||||
'option': 'running_from_source',
|
|
||||||
'type': bool},
|
|
||||||
{'section': 'rattail',
|
|
||||||
'option': 'running_from_source.rootpkg'},
|
|
||||||
|
|
||||||
# display
|
|
||||||
{'section': 'tailbone',
|
|
||||||
'option': 'background_color'},
|
|
||||||
|
|
||||||
# grids
|
|
||||||
{'section': 'tailbone',
|
|
||||||
'option': 'grid.default_pagesize',
|
|
||||||
# TODO: seems like should enforce this, but validation is
|
|
||||||
# not setup yet
|
|
||||||
# 'type': int
|
|
||||||
},
|
|
||||||
|
|
||||||
# nb. these are no longer used (deprecated), but we keep
|
# nb. these are no longer used (deprecated), but we keep
|
||||||
# them defined here so the tool auto-deletes them
|
# them defined here so the tool auto-deletes them
|
||||||
{'section': 'tailbone',
|
|
||||||
'option': 'buefy_version'},
|
|
||||||
{'section': 'tailbone',
|
|
||||||
'option': 'vue_version'},
|
|
||||||
|
|
||||||
]
|
simple_settings.extend([
|
||||||
|
{'name': 'tailbone.buefy_version'},
|
||||||
|
{'name': 'tailbone.vue_version'},
|
||||||
|
])
|
||||||
|
|
||||||
def getval(key):
|
for key in self.get_weblibs():
|
||||||
return self.config.get(f'tailbone.{key}')
|
simple_settings.extend([
|
||||||
|
{'name': f'tailbone.libver.{key}'},
|
||||||
weblibs = self.get_weblibs()
|
{'name': f'tailbone.liburl.{key}'},
|
||||||
for key, title in weblibs.items():
|
])
|
||||||
|
|
||||||
simple_settings.append({
|
|
||||||
'section': 'wuttaweb',
|
|
||||||
'option': f"libver.{key}",
|
|
||||||
'default': getval(f"libver.{key}"),
|
|
||||||
})
|
|
||||||
simple_settings.append({
|
|
||||||
'section': 'wuttaweb',
|
|
||||||
'option': f"liburl.{key}",
|
|
||||||
'default': getval(f"liburl.{key}"),
|
|
||||||
})
|
|
||||||
|
|
||||||
# nb. these are no longer used (deprecated), but we keep
|
|
||||||
# them defined here so the tool auto-deletes them
|
|
||||||
simple_settings.append({
|
|
||||||
'section': 'tailbone',
|
|
||||||
'option': f"libver.{key}",
|
|
||||||
})
|
|
||||||
simple_settings.append({
|
|
||||||
'section': 'tailbone',
|
|
||||||
'option': f"liburl.{key}",
|
|
||||||
})
|
|
||||||
|
|
||||||
return simple_settings
|
return simple_settings
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue