488 lines
15 KiB
Mako
488 lines
15 KiB
Mako
## -*- coding: utf-8; -*-
|
|
<%inherit file="wuttaweb:templates/base.mako" />
|
|
<%namespace name="base_meta" file="/base_meta.mako" />
|
|
<%namespace file="/formposter.mako" import="declare_formposter_mixin" />
|
|
<%namespace file="/grids/filter-components.mako" import="make_grid_filter_components" />
|
|
<%namespace name="page_help" file="/page_help.mako" />
|
|
|
|
<%def name="base_styles()">
|
|
${parent.base_styles()}
|
|
<style>
|
|
|
|
.filters .filter-fieldname .field,
|
|
.filters .filter-fieldname .field label {
|
|
width: 100%;
|
|
}
|
|
|
|
.filters .filter-fieldname,
|
|
.filters .filter-fieldname .field label,
|
|
.filters .filter-fieldname .button {
|
|
justify-content: left;
|
|
}
|
|
|
|
.filters .filter-verb .select,
|
|
.filters .filter-verb .select select {
|
|
width: 100%;
|
|
}
|
|
|
|
% if filter_fieldname_width is not Undefined:
|
|
|
|
.filters .filter-fieldname,
|
|
.filters .filter-fieldname .button {
|
|
min-width: ${filter_fieldname_width};
|
|
}
|
|
|
|
.filters .filter-verb {
|
|
min-width: ${filter_verb_width};
|
|
}
|
|
|
|
% endif
|
|
|
|
</style>
|
|
</%def>
|
|
|
|
<%def name="before_content()">
|
|
## TODO: this must come before the self.body() call..but why?
|
|
${declare_formposter_mixin()}
|
|
</%def>
|
|
|
|
<%def name="render_navbar_brand()">
|
|
<div class="navbar-brand">
|
|
<a class="navbar-item" href="${url('home')}"
|
|
v-show="!menuSearchActive">
|
|
<div style="display: flex; align-items: center;">
|
|
${base_meta.header_logo()}
|
|
<div id="navbar-brand-title">
|
|
${base_meta.global_title()}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
<div v-show="menuSearchActive"
|
|
class="navbar-item">
|
|
<b-autocomplete ref="menuSearchAutocomplete"
|
|
v-model="menuSearchTerm"
|
|
:data="menuSearchFilteredData"
|
|
field="label"
|
|
open-on-focus
|
|
keep-first
|
|
icon-pack="fas"
|
|
clearable
|
|
@keydown.native="menuSearchKeydown"
|
|
@select="menuSearchSelect">
|
|
</b-autocomplete>
|
|
</div>
|
|
<a role="button" class="navbar-burger" data-target="navbar-menu" aria-label="menu" aria-expanded="false">
|
|
<span aria-hidden="true"></span>
|
|
<span aria-hidden="true"></span>
|
|
<span aria-hidden="true"></span>
|
|
</a>
|
|
</div>
|
|
</%def>
|
|
|
|
<%def name="render_navbar_start()">
|
|
<div class="navbar-start">
|
|
|
|
<div v-if="menuSearchData.length"
|
|
class="navbar-item">
|
|
<b-button type="is-primary"
|
|
size="is-small"
|
|
@click="menuSearchInit()">
|
|
<span><i class="fa fa-search"></i></span>
|
|
</b-button>
|
|
</div>
|
|
|
|
% 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 = 'menu_{}_shown'.format(item_hash) %>
|
|
<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>
|
|
</%def>
|
|
|
|
<%def name="render_theme_picker()">
|
|
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
|
|
<div class="level-item">
|
|
${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()}
|
|
</div>
|
|
% endif
|
|
</%def>
|
|
|
|
<%def name="render_feedback_button()">
|
|
|
|
<div class="level-item">
|
|
<page-help
|
|
% if can_edit_help:
|
|
@configure-fields-help="configureFieldsHelp = true"
|
|
% endif
|
|
/>
|
|
</div>
|
|
|
|
% if request.has_perm('common.feedback'):
|
|
<feedback-form
|
|
action="${url('feedback')}"
|
|
:message="feedbackMessage">
|
|
</feedback-form>
|
|
% endif
|
|
</%def>
|
|
|
|
<%def name="render_this_page_component()">
|
|
<this-page @change-content-title="changeContentTitle"
|
|
% if can_edit_help:
|
|
:configure-fields-help="configureFieldsHelp"
|
|
% endif
|
|
/>
|
|
</%def>
|
|
|
|
<%def name="render_vue_templates()">
|
|
${parent.render_vue_templates()}
|
|
|
|
${page_help.render_template()}
|
|
${page_help.declare_vars()}
|
|
|
|
% if request.has_perm('common.feedback'):
|
|
<script type="text/x-template" id="feedback-template">
|
|
<div>
|
|
|
|
<div class="level-item">
|
|
<b-button type="is-primary"
|
|
@click="showFeedback()"
|
|
icon-pack="fas"
|
|
icon-left="comment">
|
|
Feedback
|
|
</b-button>
|
|
</div>
|
|
|
|
<b-modal has-modal-card
|
|
:active.sync="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
|
|
>
|
|
</b-input>
|
|
</b-field>
|
|
|
|
<b-field label="Referring URL">
|
|
<b-input
|
|
v-model="referrer"
|
|
disabled="true">
|
|
</b-input>
|
|
</b-field>
|
|
|
|
<b-field label="Message">
|
|
<b-input type="textarea"
|
|
v-model="message"
|
|
ref="textarea">
|
|
</b-input>
|
|
</b-field>
|
|
|
|
% if config.get_bool('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">
|
|
<b-button @click="showDialog = false">
|
|
Cancel
|
|
</b-button>
|
|
<b-button type="is-primary"
|
|
icon-pack="fas"
|
|
icon-left="paper-plane"
|
|
@click="sendFeedback()"
|
|
:disabled="sendingFeedback || !message.trim()">
|
|
{{ sendingFeedback ? "Working, please wait..." : "Send Message" }}
|
|
</b-button>
|
|
</footer>
|
|
</div>
|
|
</b-modal>
|
|
|
|
</div>
|
|
</script>
|
|
<script>
|
|
|
|
const FeedbackForm = {
|
|
template: '#feedback-template',
|
|
mixins: [SimpleRequestMixin],
|
|
props: [
|
|
'action',
|
|
'message',
|
|
],
|
|
methods: {
|
|
|
|
showFeedback() {
|
|
this.referrer = location.href
|
|
this.showDialog = true
|
|
this.$nextTick(function() {
|
|
this.$refs.textarea.focus()
|
|
})
|
|
},
|
|
|
|
% if config.get_bool('tailbone.feedback_allows_reply'):
|
|
pleaseReplyChanged(value) {
|
|
this.$nextTick(() => {
|
|
this.$refs.userEmail.focus()
|
|
})
|
|
},
|
|
% endif
|
|
|
|
sendFeedback() {
|
|
this.sendingFeedback = true
|
|
|
|
const params = {
|
|
referrer: this.referrer,
|
|
user: this.userUUID,
|
|
user_name: this.userName,
|
|
% if config.get_bool('tailbone.feedback_allows_reply'):
|
|
please_reply_to: this.pleaseReply ? this.userEmail : null,
|
|
% endif
|
|
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.showDialog = false
|
|
// clear out message, in case they need to send another
|
|
this.message = ""
|
|
this.sendingFeedback = false
|
|
|
|
}, response => { // failure
|
|
this.sendingFeedback = false
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
const FeedbackFormData = {
|
|
referrer: null,
|
|
userUUID: null,
|
|
userName: null,
|
|
userEmail: null,
|
|
% if config.get_bool('tailbone.feedback_allows_reply'):
|
|
pleaseReply: false,
|
|
% endif
|
|
showDialog: false,
|
|
sendingFeedback: false,
|
|
}
|
|
|
|
</script>
|
|
% endif
|
|
</%def>
|
|
|
|
<%def name="modify_vue_vars()">
|
|
${parent.modify_vue_vars()}
|
|
<script>
|
|
|
|
##############################
|
|
## menu search
|
|
##############################
|
|
|
|
WholePageData.menuSearchActive = false
|
|
WholePageData.menuSearchTerm = ''
|
|
WholePageData.menuSearchData = ${json.dumps(global_search_data or [])|n}
|
|
|
|
WholePage.computed.menuSearchFilteredData = function() {
|
|
if (!this.menuSearchTerm.length) {
|
|
return this.menuSearchData
|
|
}
|
|
|
|
const terms = []
|
|
for (let term of this.menuSearchTerm.toLowerCase().split(' ')) {
|
|
term = term.trim()
|
|
if (term) {
|
|
terms.push(term)
|
|
}
|
|
}
|
|
if (!terms.length) {
|
|
return this.menuSearchData
|
|
}
|
|
|
|
// all terms must match
|
|
return this.menuSearchData.filter((option) => {
|
|
const label = option.label.toLowerCase()
|
|
for (const term of terms) {
|
|
if (label.indexOf(term) < 0) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
WholePage.methods.globalKey = function(event) {
|
|
|
|
// Ctrl+8 opens menu search
|
|
if (event.target.tagName == 'BODY') {
|
|
if (event.ctrlKey && event.key == '8') {
|
|
this.menuSearchInit()
|
|
}
|
|
}
|
|
}
|
|
|
|
WholePage.mounted = function() {
|
|
window.addEventListener('keydown', this.globalKey)
|
|
for (let hook of this.mountedHooks) {
|
|
hook(this)
|
|
}
|
|
}
|
|
|
|
WholePage.beforeDestroy = function() {
|
|
window.removeEventListener('keydown', this.globalKey)
|
|
}
|
|
|
|
WholePage.methods.menuSearchInit = function() {
|
|
this.menuSearchTerm = ''
|
|
this.menuSearchActive = true
|
|
this.$nextTick(() => {
|
|
this.$refs.menuSearchAutocomplete.focus()
|
|
})
|
|
}
|
|
|
|
WholePage.methods.menuSearchKeydown = function(event) {
|
|
|
|
// ESC will dismiss searchbox
|
|
if (event.which == 27) {
|
|
this.menuSearchActive = false
|
|
}
|
|
}
|
|
|
|
WholePage.methods.menuSearchSelect = function(option) {
|
|
location.href = option.url
|
|
}
|
|
|
|
##############################
|
|
## theme picker
|
|
##############################
|
|
|
|
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
|
|
|
|
WholePageData.globalTheme = ${json.dumps(theme or None)|n}
|
|
## WholePageData.referrer = location.href
|
|
|
|
WholePage.methods.changeTheme = function() {
|
|
this.$refs.themePickerForm.submit()
|
|
}
|
|
|
|
% endif
|
|
|
|
##############################
|
|
## feedback
|
|
##############################
|
|
|
|
% if request.has_perm('common.feedback'):
|
|
|
|
WholePageData.feedbackMessage = ""
|
|
|
|
% if request.user:
|
|
FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n}
|
|
FeedbackFormData.userName = ${json.dumps(str(request.user))|n}
|
|
% endif
|
|
|
|
% endif
|
|
|
|
##############################
|
|
## edit fields help
|
|
##############################
|
|
|
|
% if can_edit_help:
|
|
WholePageData.configureFieldsHelp = false
|
|
% endif
|
|
|
|
</script>
|
|
</%def>
|
|
|
|
<%def name="make_vue_components()">
|
|
${parent.make_vue_components()}
|
|
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.datepicker.js') + f'?ver={tailbone.__version__}')}
|
|
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.numericinput.js') + f'?ver={tailbone.__version__}')}
|
|
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.oncebutton.js') + f'?ver={tailbone.__version__}')}
|
|
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + f'?ver={tailbone.__version__}')}
|
|
${make_grid_filter_components()}
|
|
${page_help.make_component()}
|
|
% if request.has_perm('common.feedback'):
|
|
<script>
|
|
FeedbackForm.data = function() { return FeedbackFormData }
|
|
Vue.component('feedback-form', FeedbackForm)
|
|
</script>
|
|
% endif
|
|
</%def>
|
|
|