tailbone/tailbone/templates/themes/butterball/base.mako
Lance Edgar da0f6bd5e1 feat: use wuttaweb for get_liburl() logic
thankfully this is already handled and we can remove from tailbone.
although this adds some new cruft as well, to handle auto-migrating
any existing liburl config for apps.

eventually once all apps have migrated to new settings we can remove
the prefix from our calls here but also in wuttaweb signature
2024-08-15 23:12:02 -05:00

1226 lines
41 KiB
Mako

## -*- coding: utf-8; -*-
<%namespace name="base_meta" file="/base_meta.mako" />
<%namespace name="page_help" file="/page_help.mako" />
<%namespace file="/field-components.mako" import="make_field_components" />
<%namespace file="/formposter.mako" import="declare_formposter_mixin" />
<%namespace file="/grids/filter-components.mako" import="make_grid_filter_components" />
<%namespace file="/buefy-components.mako" import="make_buefy_components" />
<%namespace file="/buefy-plugin.mako" import="make_buefy_plugin" />
<%namespace file="/http-plugin.mako" import="make_http_plugin" />
## <%namespace file="/grids/nav.mako" import="grid_index_nav" />
## <%namespace name="multi_file_upload" file="/multi_file_upload.mako" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>${base_meta.global_title()} &raquo; ${capture(self.title)|n}</title>
${base_meta.favicon()}
${self.header_core()}
${self.head_tags()}
</head>
<body>
<div id="app" style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
<whole-page></whole-page>
</div>
## TODO: this must come before the self.body() call..but why?
${declare_formposter_mixin()}
## global components used by various (but not all) pages
${make_field_components()}
${make_grid_filter_components()}
## global components for buefy-based template compatibility
${make_http_plugin()}
${make_buefy_plugin()}
${make_buefy_components()}
## special global components, used by WholePage
${self.make_menu_search_component()}
${page_help.render_template()}
${page_help.declare_vars()}
% if request.has_perm('common.feedback'):
${self.make_feedback_component()}
% endif
## WholePage component
${self.make_whole_page_component()}
## content body from derived/child template
${self.body()}
## Vue app
${self.make_whole_page_app()}
</body>
</html>
<%def name="title()"></%def>
<%def name="content_title()">
${self.title()}
</%def>
<%def name="header_core()">
${self.core_javascript()}
${self.core_styles()}
</%def>
<%def name="core_javascript()">
<script type="importmap">
{
## TODO: eventually version / url should be configurable
"imports": {
"vue": "${h.get_liburl(request, 'bb_vue', prefix='tailbone')}",
"@oruga-ui/oruga-next": "${h.get_liburl(request, 'bb_oruga', prefix='tailbone')}",
"@oruga-ui/theme-bulma": "${h.get_liburl(request, 'bb_oruga_bulma', prefix='tailbone')}",
"@fortawesome/fontawesome-svg-core": "${h.get_liburl(request, 'bb_fontawesome_svg_core', prefix='tailbone')}",
"@fortawesome/free-solid-svg-icons": "${h.get_liburl(request, 'bb_free_solid_svg_icons', prefix='tailbone')}",
"@fortawesome/vue-fontawesome": "${h.get_liburl(request, 'bb_vue_fontawesome', prefix='tailbone')}"
}
}
</script>
<script>
// empty stub to avoid errors for older buefy templates
const Vue = {
component(tagname, classname) {},
}
</script>
</%def>
<%def name="core_styles()">
% if user_css:
${h.stylesheet_link(user_css)}
% else:
${h.stylesheet_link(h.get_liburl(request, 'bb_oruga_bulma_css', prefix='tailbone'))}
% endif
</%def>
<%def name="head_tags()">
${self.extra_javascript()}
${self.extra_styles()}
</%def>
<%def name="extra_javascript()">
## ## some commonly-useful logic for detecting (non-)numeric input
## ${h.javascript_link(request.static_url('tailbone:static/js/numeric.js') + '?ver={}'.format(tailbone.__version__))}
##
## ## debounce, for better autocomplete performance
## ${h.javascript_link(request.static_url('tailbone:static/js/debounce.js') + '?ver={}'.format(tailbone.__version__))}
## ## Tailbone / Buefy stuff
## ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.numericinput.js') + '?ver={}'.format(tailbone.__version__))}
## ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + '?ver={}'.format(tailbone.__version__))}
## <script type="text/javascript">
##
## ## NOTE: this code was copied from
## ## https://bulma.io/documentation/components/navbar/#navbar-menu
##
## document.addEventListener('DOMContentLoaded', () => {
##
## // Get all "navbar-burger" elements
## const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0)
##
## // Add a click event on each of them
## $navbarBurgers.forEach( el => {
## el.addEventListener('click', () => {
##
## // Get the target from the "data-target" attribute
## const target = el.dataset.target
## const $target = document.getElementById(target)
##
## // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
## el.classList.toggle('is-active')
## $target.classList.toggle('is-active')
##
## })
## })
## })
##
## </script>
</%def>
<%def name="extra_styles()">
## ${h.stylesheet_link(request.static_url('tailbone:static/css/base.css') + '?ver={}'.format(tailbone.__version__))}
## ${h.stylesheet_link(request.static_url('tailbone:static/css/layout.css') + '?ver={}'.format(tailbone.__version__))}
## ${h.stylesheet_link(request.static_url('tailbone:static/css/grids.css') + '?ver={}'.format(tailbone.__version__))}
## ${h.stylesheet_link(request.static_url('tailbone:static/css/filters.css') + '?ver={}'.format(tailbone.__version__))}
## ${h.stylesheet_link(request.static_url('tailbone:static/css/forms.css') + '?ver={}'.format(tailbone.__version__))}
${h.stylesheet_link(request.static_url('tailbone:static/css/grids.rowstatus.css') + '?ver={}'.format(tailbone.__version__))}
${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css') + '?ver={}'.format(tailbone.__version__))}
## nb. this is used (only?) in /generate-feature page
${h.stylesheet_link(request.static_url('tailbone:static/css/codehilite.css') + '?ver={}'.format(tailbone.__version__))}
<style>
/* ****************************** */
/* page */
/* ****************************** */
/* nb. helps force footer to bottom of screen */
html, body {
height: 100%;
}
## maybe add testing watermark
% if not request.rattail_config.production():
html, .navbar, .footer {
background-image: url(${request.static_url('tailbone:static/img/testing.png')});
}
% endif
## maybe force global background color
% if background_color:
body, .navbar, .footer {
background-color: ${background_color};
}
% endif
#content-title h1 {
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
## TODO: is this a good idea?
h1.title {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0 !important;
}
#context-menu {
margin-bottom: 1em;
/* margin-left: 1em; */
text-align: right;
/* white-space: nowrap; */
}
## TODO: ugh why is this needed to center modal on screen?
.modal .modal-content .modal-card {
margin: auto;
}
.object-helpers .panel {
margin: 1rem;
margin-bottom: 1.5rem;
}
/* ****************************** */
/* grids */
/* ****************************** */
.filters .filter-fieldname .button {
min-width: ${filter_fieldname_width};
justify-content: left;
}
.filters .filter-verb {
min-width: ${filter_verb_width};
}
.grid-tools {
display: flex;
gap: 0.5rem;
justify-content: end;
}
a.grid-action {
align-items: center;
display: inline-flex;
gap: 0.1rem;
white-space: nowrap;
}
/**************************************************
* grid rows which are "checked" (selected)
**************************************************/
/* TODO: this references some color values, whereas it would be preferable
* to refer to some sort of "state" instead, color of which was
* configurable. b/c these are just the default Buefy theme colors. */
tr.is-checked {
background-color: #7957d5;
color: white;
}
tr.is-checked:hover {
color: #363636;
}
tr.is-checked a {
color: white;
}
tr.is-checked:hover a {
color: #7957d5;
}
/* ****************************** */
/* forms */
/* ****************************** */
/* note that these should only apply to "normal" primary forms */
.form {
padding-left: 5em;
}
/* .form-wrapper .form .field.is-horizontal .field-label .label, */
.form-wrapper .field.is-horizontal .field-label {
text-align: left;
white-space: nowrap;
min-width: 18em;
}
.form-wrapper .form .field.is-horizontal .field-body {
min-width: 30em;
}
.form-wrapper .form .field.is-horizontal .field-body .autocomplete,
.form-wrapper .form .field.is-horizontal .field-body .autocomplete .dropdown-trigger,
.form-wrapper .form .field.is-horizontal .field-body .select,
.form-wrapper .form .field.is-horizontal .field-body .select select {
width: 100%;
}
.form-wrapper .form .buttons {
padding-left: 10rem;
}
/******************************
* fix datepicker within modals
* TODO: someday this may not be necessary? cf.
* https://github.com/buefy/buefy/issues/292#issuecomment-347365637
******************************/
/* TODO: this does change some things, but does not actually work 100% */
/* right for oruga 0.8.7 or 0.8.9 */
.modal .animation-content .modal-card {
overflow: visible !important;
}
.modal-card-body {
overflow: visible !important;
}
/* TODO: a simpler option we might try sometime instead? */
/* cf. https://github.com/buefy/buefy/issues/292#issuecomment-1073851313 */
/* .dropdown-content{ */
/* position: fixed; */
/* } */
</style>
${base_meta.extra_styles()}
</%def>
<%def name="make_feedback_component()">
<% request.register_component('feedback-form', 'FeedbackForm') %>
<script type="text/x-template" id="feedback-form-template">
<div>
<o-button variant="primary"
@click="showFeedback()"
icon-left="comment">
Feedback
</o-button>
<o-modal v-model:active="showDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">
User Feedback
</p>
</header>
<section class="modal-card-body">
<p class="block">
Questions, suggestions, comments, complaints, etc.
<span class="red">regarding this website</span> are
welcome and may be submitted below.
</p>
<b-field label="User Name">
<b-input v-model="userName"
% if request.user:
disabled
% endif
expanded>
</b-input>
</b-field>
<b-field label="Referring URL">
<b-input
v-model="referrer"
disabled expanded>
</b-input>
</b-field>
<o-field label="Message">
<o-input type="textarea"
v-model="message"
ref="message"
expanded>
</o-input>
</o-field>
% if request.rattail_config.getbool('tailbone', 'feedback_allows_reply'):
<div class="level">
<div class="level-left">
<div class="level-item">
<b-checkbox v-model="pleaseReply"
@input="pleaseReplyChanged">
Please email me back{{ pleaseReply ? " at: " : "" }}
</b-checkbox>
</div>
<div class="level-item" v-show="pleaseReply">
<b-input v-model="userEmail"
ref="userEmail">
</b-input>
</div>
</div>
</div>
% endif
</section>
<footer class="modal-card-foot">
<o-button @click="showDialog = false">
Cancel
</o-button>
<o-button variant="primary"
@click="sendFeedback()"
:disabled="sending || !message?.trim()">
{{ sending ? "Working, please wait..." : "Send Message" }}
</o-button>
</footer>
</div>
</o-modal>
</div>
</script>
<script>
const FeedbackForm = {
template: '#feedback-form-template',
mixins: [SimpleRequestMixin],
props: {
action: String,
},
data() {
return {
referrer: null,
% if request.user:
userUUID: ${json.dumps(request.user.uuid)|n},
userName: ${json.dumps(str(request.user))|n},
% else:
userUUID: null,
userName: null,
% endif
message: null,
pleaseReply: false,
userEmail: null,
showDialog: false,
sending: false,
}
},
methods: {
pleaseReplyChanged(value) {
this.$nextTick(() => {
this.$refs.userEmail.focus()
})
},
showFeedback() {
this.referrer = location.href
this.message = null
this.showDialog = true
this.$nextTick(function() {
this.$refs.message.focus()
})
},
sendFeedback() {
this.sending = true
const params = {
referrer: this.referrer,
user: this.userUUID,
user_name: this.userName,
please_reply_to: this.pleaseReply ? this.userEmail : '',
message: this.message?.trim(),
}
this.simplePOST(this.action, params, response => {
this.$buefy.toast.open({
message: "Message sent! Thank you for your feedback.",
type: 'is-info',
duration: 4000, // 4 seconds
})
this.sending = false
this.showDialog = false
}, response => {
this.sending = false
})
},
}
}
</script>
</%def>
<%def name="make_menu_search_component()">
<% request.register_component('menu-search', 'MenuSearch') %>
<script type="text/x-template" id="menu-search-template">
<div style="display: flex;">
<a v-show="!searchActive"
href="${url('home')}"
class="navbar-item"
style="display: flex; gap: 0.5rem;">
${base_meta.header_logo()}
<div id="global-header-title">
${base_meta.global_title()}
</div>
</a>
<div v-show="searchActive"
class="navbar-item">
<o-autocomplete ref="searchAutocomplete"
v-model="searchTerm"
:data="searchFilteredData"
field="label"
open-on-focus
keep-first
icon-pack="fas"
clearable
@select="searchSelect">
</o-autocomplete>
</div>
</div>
</script>
<script>
const MenuSearch = {
template: '#menu-search-template',
props: {
searchData: Array,
},
data() {
return {
searchActive: false,
searchTerm: null,
searchInput: null,
}
},
computed: {
searchFilteredData() {
if (!this.searchTerm || !this.searchTerm.length) {
return this.searchData
}
let terms = []
for (let term of this.searchTerm.toLowerCase().split(' ')) {
term = term.trim()
if (term) {
terms.push(term)
}
}
if (!terms.length) {
return this.searchData
}
// all terms must match
return this.searchData.filter((option) => {
let label = option.label.toLowerCase()
for (let term of terms) {
if (label.indexOf(term) < 0) {
return false
}
}
return true
})
},
},
mounted() {
this.searchInput = this.$refs.searchAutocomplete.$el.querySelector('input')
this.searchInput.addEventListener('keydown', this.searchKeydown)
},
beforeDestroy() {
this.searchInput.removeEventListener('keydown', this.searchKeydown)
},
methods: {
searchInit() {
this.searchTerm = ''
this.searchActive = true
this.$nextTick(() => {
this.$refs.searchAutocomplete.focus()
})
},
searchKeydown(event) {
// ESC will dismiss searchbox
if (event.which == 27) {
this.searchActive = false
}
},
searchSelect(option) {
location.href = option.url
},
},
}
</script>
</%def>
<%def name="render_whole_page_template()">
<script type="text/x-template" id="whole-page-template">
<div id="whole-page" style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
<div class="header-wrapper">
<header>
<!-- this main menu, with search -->
<nav class="navbar" role="navigation" aria-label="main navigation"
style="display: flex; align-items: center;">
<div class="navbar-brand">
<menu-search :search-data="globalSearchData"
ref="menuSearch" />
<a role="button" class="navbar-burger" data-target="navbarMenu" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" id="navbarMenu"
style="display: flex; align-items: center;"
>
<div class="navbar-start">
## global search button
<div v-if="globalSearchData.length"
class="navbar-item">
<o-button variant="primary"
size="small"
@click="globalSearchInit()">
<o-icon icon="search" size="small" />
</o-button>
</div>
## main menu
% for topitem in menus:
% if topitem['is_link']:
${h.link_to(topitem['title'], topitem['url'], target=topitem['target'], class_='navbar-item')}
% else:
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">${topitem['title']}</a>
<div class="navbar-dropdown">
% for item in topitem['items']:
% if item['is_menu']:
<% item_hash = id(item) %>
<% toggle = f'menu_{item_hash}_shown' %>
<div>
<a class="navbar-link" @click.prevent="toggleNestedMenu('${item_hash}')">
${item['title']}
</a>
</div>
% for subitem in item['items']:
% if subitem['is_sep']:
<hr class="navbar-divider" v-show="${toggle}">
% else:
${h.link_to("{}".format(subitem['title']), subitem['url'], class_='navbar-item nested', target=subitem['target'], **{'v-show': toggle})}
% endif
% endfor
% else:
% if item['is_sep']:
<hr class="navbar-divider">
% else:
${h.link_to(item['title'], item['url'], class_='navbar-item', target=item['target'])}
% endif
% endif
% endfor
</div>
</div>
% endif
% endfor
</div><!-- navbar-start -->
${self.render_navbar_end()}
</div>
</nav>
<!-- nb. this has index title, help button etc. -->
<nav style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem;">
## Current Context
<div style="display: flex; gap: 0.5rem; align-items: center;">
% if master:
% if master.listing:
<h1 class="title">
${index_title}
</h1>
% if master.creatable and getattr(master, 'show_create_link', True) and master.has_perm('create'):
<once-button type="is-primary"
tag="a" href="${url('{}.create'.format(route_prefix))}"
icon-left="plus"
style="margin-left: 1rem;"
text="Create New">
</once-button>
% endif
% elif index_url:
<h1 class="title">
${h.link_to(index_title, index_url)}
</h1>
% if parent_url is not Undefined:
<h1 class="title">
&nbsp;&raquo;
</h1>
<h1 class="title">
${h.link_to(parent_title, parent_url)}
</h1>
% elif instance_url is not Undefined:
<h1 class="title">
&nbsp;&raquo;
</h1>
<h1 class="title">
${h.link_to(instance_title, instance_url)}
</h1>
% elif master.creatable and getattr(master, 'show_create_link', True) and master.has_perm('create'):
% if not request.matched_route.name.endswith('.create'):
<once-button type="is-primary"
tag="a" href="${url('{}.create'.format(route_prefix))}"
icon-left="plus"
style="margin-left: 1rem;"
text="Create New">
</once-button>
% endif
% endif
## % if master.viewing and grid_index:
## ${grid_index_nav()}
## % endif
% else:
<h1 class="title">
${index_title}
</h1>
% endif
% elif index_title:
% if index_url:
<h1 class="title">
${h.link_to(index_title, index_url)}
</h1>
% else:
<h1 class="title">
${index_title}
</h1>
% endif
% endif
% if expose_db_picker is not Undefined and expose_db_picker:
<span>DB:</span>
${h.form(url('change_db_engine'), ref='dbPickerForm')}
${h.csrf_token(request)}
${h.hidden('engine_type', value=master.engine_type_key)}
<input type="hidden" name="referrer" :value="referrer" />
<b-select name="dbkey"
v-model="dbSelected"
@input="changeDB()">
% for option in db_picker_options:
<option value="${option.value}">
${option.label}
</option>
% endfor
</b-select>
${h.end_form()}
% endif
</div>
<div style="display: flex; gap: 0.5rem;">
## Quickie Lookup
% if quickie is not Undefined and quickie and request.has_perm(quickie.perm):
${h.form(quickie.url, method='get', style='display: flex; gap: 0.5rem; margin-right: 1rem;')}
<b-input name="entry"
placeholder="${quickie.placeholder}"
autocomplete="off">
</b-input>
<o-button variant="primary"
native-type="submit"
icon-left="search">
Lookup
</o-button>
${h.end_form()}
% endif
% if master and master.configurable and master.has_perm('configure'):
% if not request.matched_route.name.endswith('.configure'):
<once-button type="is-primary"
tag="a"
href="${url('{}.configure'.format(route_prefix))}"
icon-left="cog"
text="${(configure_button_title or "Configure") if configure_button_title is not Undefined else "Configure"}">
</once-button>
% endif
% endif
## Theme Picker
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
${h.form(url('change_theme'), method="post", ref='themePickerForm')}
${h.csrf_token(request)}
<input type="hidden" name="referrer" :value="referrer" />
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span>Theme:</span>
<b-select name="theme"
v-model="globalTheme"
@input="changeTheme()">
% for option in theme_picker_options:
<option value="${option.value}">
${option.label}
</option>
% endfor
</b-select>
</div>
${h.end_form()}
% endif
% if help_url or help_markdown or can_edit_help:
<page-help
% if can_edit_help:
@configure-fields-help="configureFieldsHelp = true"
% endif
>
</page-help>
% endif
## Feedback Button / Dialog
% if request.has_perm('common.feedback'):
<feedback-form action="${url('feedback')}" />
% endif
</div>
</nav>
</header>
## Page Title
% if capture(self.content_title):
<section class="has-background-primary"
## TODO: id is only for css, do we need it?
id="content-title"
style="padding: 0.5rem; padding-left: 1rem;">
<div style="display: flex; align-items: center; gap: 1rem;">
<h1 class="title has-text-white" v-html="contentTitleHTML" />
<div style="flex-grow: 1; display: flex; gap: 0.5rem;">
${self.render_instance_header_title_extras()}
</div>
<div style="display: flex; gap: 0.5rem;">
${self.render_instance_header_buttons()}
</div>
</div>
</section>
% endif
</div> <!-- header-wrapper -->
<div class="content-wrapper"
style="flex-grow: 1; padding: 0.5rem;">
## Page Body
<section id="page-body">
% if request.session.peek_flash('error'):
% for error in request.session.pop_flash('error'):
<b-notification type="is-warning">
${error}
</b-notification>
% endfor
% endif
% if request.session.peek_flash('warning'):
% for msg in request.session.pop_flash('warning'):
<b-notification type="is-warning">
${msg}
</b-notification>
% endfor
% endif
% if request.session.peek_flash():
% for msg in request.session.pop_flash():
<b-notification type="is-info">
${msg}
</b-notification>
% endfor
% endif
## true page content
<div>
${self.render_this_page_component()}
</div>
</section>
</div><!-- content-wrapper -->
## Footer
<footer class="footer">
<div class="content">
${base_meta.footer()}
</div>
</footer>
</div>
</script>
## ${multi_file_upload.render_template()}
</%def>
<%def name="render_this_page_component()">
<this-page @change-content-title="changeContentTitle"
% if can_edit_help:
:configure-fields-help="configureFieldsHelp"
% endif
>
</this-page>
</%def>
<%def name="render_navbar_end()">
<div class="navbar-end">
${self.render_user_menu()}
</div>
</%def>
<%def name="render_user_menu()">
% if request.user:
<div class="navbar-item has-dropdown is-hoverable">
% if messaging_enabled:
<a class="navbar-link ${'has-background-danger has-text-white' if request.is_root else ''}">${request.user}${" ({})".format(inbox_count) if inbox_count else ''}</a>
% else:
<a class="navbar-link ${'has-background-danger has-text-white' if request.is_root else ''}">${request.user}</a>
% endif
<div class="navbar-dropdown">
% if request.is_root:
${h.form(url('stop_root'), ref='stopBeingRootForm')}
${h.csrf_token(request)}
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
<a @click="stopBeingRoot()"
class="navbar-item has-background-danger has-text-white">
Stop being root
</a>
${h.end_form()}
% elif request.is_admin:
${h.form(url('become_root'), ref='startBeingRootForm')}
${h.csrf_token(request)}
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
<a @click="startBeingRoot()"
class="navbar-item has-background-danger has-text-white">
Become root
</a>
${h.end_form()}
% endif
% if messaging_enabled:
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
% endif
% if request.is_root or not request.user.prevent_password_change:
${h.link_to("Change Password", url('change_password'), class_='navbar-item')}
% endif
${h.link_to("Edit Preferences", url('my.preferences'), class_='navbar-item')}
${h.link_to("Logout", url('logout'), class_='navbar-item')}
</div>
</div>
% else:
${h.link_to("Login", url('login'), class_='navbar-item')}
% endif
</%def>
<%def name="render_instance_header_title_extras()"></%def>
<%def name="render_instance_header_buttons()">
${self.render_crud_header_buttons()}
${self.render_prevnext_header_buttons()}
</%def>
<%def name="render_crud_header_buttons()">
% if master and master.viewing and not getattr(master, 'cloning', False):
## TODO: is there a better way to check if viewing parent?
% if parent_instance is Undefined:
% if master.editable and instance_editable and master.has_perm('edit'):
<once-button tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit"
text="Edit This">
</once-button>
% endif
% if not getattr(master, 'cloning', False) and getattr(master, 'cloneable', False) and master.has_perm('clone'):
<once-button tag="a" href="${master.get_action_url('clone', instance)}"
icon-left="object-ungroup"
text="Clone This">
</once-button>
% endif
% if master.deletable and instance_deletable and master.has_perm('delete'):
<once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger"
icon-left="trash"
text="Delete This">
</once-button>
% endif
% else:
## viewing row
% if instance_deletable and master.has_perm('delete_row'):
<once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger"
icon-left="trash"
text="Delete This">
</once-button>
% endif
% endif
% elif master and master.editing:
% if master.viewable and master.has_perm('view'):
<once-button tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
text="View This">
</once-button>
% endif
% if master.deletable and instance_deletable and master.has_perm('delete'):
<once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger"
icon-left="trash"
text="Delete This">
</once-button>
% endif
% elif master and master.deleting:
% if master.viewable and master.has_perm('view'):
<once-button tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
text="View This">
</once-button>
% endif
% if master.editable and instance_editable and master.has_perm('edit'):
<once-button tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit"
text="Edit This">
</once-button>
% endif
% elif master and getattr(master, 'cloning', False):
% if master.viewable and master.has_perm('view'):
<once-button tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
text="View This">
</once-button>
% endif
% endif
</%def>
<%def name="render_prevnext_header_buttons()">
% if show_prev_next is not Undefined and show_prev_next:
% if prev_url:
<b-button tag="a" href="${prev_url}"
icon-pack="fas"
icon-left="arrow-left">
Older
</b-button>
% else:
<b-button tag="a" href="#"
disabled
icon-pack="fas"
icon-left="arrow-left">
Older
</b-button>
% endif
% if next_url:
<b-button tag="a" href="${next_url}"
icon-pack="fas"
icon-left="arrow-right">
Newer
</b-button>
% else:
<b-button tag="a" href="#"
disabled
icon-pack="fas"
icon-left="arrow-right">
Newer
</b-button>
% endif
% endif
</%def>
<%def name="declare_whole_page_vars()">
## ${multi_file_upload.declare_vars()}
<script>
const WholePage = {
template: '#whole-page-template',
mixins: [SimpleRequestMixin],
computed: {},
mounted() {
window.addEventListener('keydown', this.globalKey)
for (let hook of this.mountedHooks) {
hook(this)
}
},
beforeDestroy() {
window.removeEventListener('keydown', this.globalKey)
},
methods: {
changeContentTitle(newTitle) {
this.contentTitleHTML = newTitle
},
% if expose_db_picker is not Undefined and expose_db_picker:
changeDB() {
this.$refs.dbPickerForm.submit()
},
% endif
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
changeTheme() {
this.$refs.themePickerForm.submit()
},
% endif
globalKey(event) {
// Ctrl+8 opens global search
if (event.target.tagName == 'BODY') {
if (event.ctrlKey && event.key == '8') {
this.globalSearchInit()
}
}
},
globalSearchInit() {
this.$refs.menuSearch.searchInit()
},
toggleNestedMenu(hash) {
const key = 'menu_' + hash + '_shown'
this[key] = !this[key]
},
% if request.is_admin:
startBeingRoot() {
this.$refs.startBeingRootForm.submit()
},
stopBeingRoot() {
this.$refs.stopBeingRootForm.submit()
},
% endif
},
}
const WholePageData = {
contentTitleHTML: ${json.dumps(capture(self.content_title))|n},
globalSearchData: ${json.dumps(global_search_data)|n},
mountedHooks: [],
% if expose_db_picker is not Undefined and expose_db_picker:
dbSelected: ${json.dumps(db_picker_selected)|n},
% endif
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
globalTheme: ${json.dumps(theme)|n},
referrer: location.href,
% endif
% if can_edit_help:
configureFieldsHelp: false,
% endif
}
## declare nested menu visibility toggle flags
% for topitem in menus:
% if topitem['is_menu']:
% for item in topitem['items']:
% if item['is_menu']:
WholePageData.menu_${id(item)}_shown = false
% endif
% endfor
% endif
% endfor
</script>
</%def>
<%def name="modify_whole_page_vars()"></%def>
## TODO: do we really need this?
## <%def name="finalize_whole_page_vars()"></%def>
<%def name="make_whole_page_component()">
${self.render_whole_page_template()}
${self.declare_whole_page_vars()}
${self.modify_whole_page_vars()}
## ${self.finalize_whole_page_vars()}
${page_help.make_component()}
## ${multi_file_upload.make_component()}
<script>
WholePage.data = () => { return WholePageData }
</script>
<% request.register_component('whole-page', 'WholePage') %>
</%def>
<%def name="make_whole_page_app()">
<script type="module">
import {createApp} from 'vue'
import {Oruga} from '@oruga-ui/oruga-next'
import {bulmaConfig} from '@oruga-ui/theme-bulma'
import { library } from "@fortawesome/fontawesome-svg-core"
import { fas } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
library.add(fas)
const app = createApp()
app.component('vue-fontawesome', FontAwesomeIcon)
% if hasattr(request, '_tailbone_registered_components'):
% for tagname, classname in request._tailbone_registered_components.items():
app.component('${tagname}', ${classname})
% endfor
% endif
app.use(Oruga, {
...bulmaConfig,
iconComponent: 'vue-fontawesome',
iconPack: 'fas',
})
app.use(HttpPlugin)
app.use(BuefyPlugin)
app.mount('#app')
</script>
</%def>