Add initial support for Vue 3 + Oruga, via "butterball" theme

just a savepoint, still have lots to do and test before this really works
This commit is contained in:
Lance Edgar 2024-04-24 16:13:14 -05:00
parent 5aa8d1f9a3
commit 2eaeb1891d
29 changed files with 3212 additions and 315 deletions

View file

@ -1103,6 +1103,10 @@ class Form(object):
# only declare label template if it's complex # only declare label template if it's complex
html = [html] html = [html]
# TODO: figure out why complex label does not work for oruga
if self.request.use_oruga:
attrs['label'] = label
else:
if len(label_contents) > 1: if len(label_contents) > 1:
# nb. must apply hack to get <template #label> as final result # nb. must apply hack to get <template #label> as final result

View file

@ -27,7 +27,9 @@ Event Subscribers
import six import six
import json import json
import datetime import datetime
import logging
import warnings import warnings
from collections import OrderedDict
import rattail import rattail
@ -41,7 +43,11 @@ from tailbone import helpers
from tailbone.db import Session from tailbone.db import Session
from tailbone.config import csrf_header_name, should_expose_websockets from tailbone.config import csrf_header_name, should_expose_websockets
from tailbone.menus import make_simple_menus from tailbone.menus import make_simple_menus
from tailbone.util import get_available_themes, get_global_search_options from tailbone.util import (get_available_themes, get_global_search_options,
should_use_oruga)
log = logging.getLogger(__name__)
def new_request(event): def new_request(event):
@ -92,6 +98,11 @@ def new_request(event):
request.set_property(user, reify=True) request.set_property(user, reify=True)
def use_oruga(request):
return should_use_oruga(request)
request.set_property(use_oruga, reify=True)
# assign client IP address to the session, for sake of versioning # assign client IP address to the session, for sake of versioning
Session().continuum_remote_addr = request.client_addr Session().continuum_remote_addr = request.client_addr
@ -119,6 +130,25 @@ def new_request(event):
return False return False
request.has_any_perm = has_any_perm request.has_any_perm = has_any_perm
def register_component(tagname, classname):
"""
Register a Vue 3 component, so the base template knows to
declare it for use within the app (page).
"""
if not hasattr(request, '_tailbone_registered_components'):
request._tailbone_registered_components = OrderedDict()
if tagname in request._tailbone_registered_components:
log.warning("component with tagname '%s' already registered "
"with class '%s' but we are replacing that with "
"class '%s'",
tagname,
request._tailbone_registered_components[tagname],
classname)
request._tailbone_registered_components[tagname] = classname
request.register_component = register_component
def before_render(event): def before_render(event):
""" """
@ -143,6 +173,7 @@ def before_render(event):
renderer_globals['colander'] = colander renderer_globals['colander'] = colander
renderer_globals['deform'] = deform renderer_globals['deform'] = deform
renderer_globals['csrf_header_name'] = csrf_header_name(request.rattail_config) renderer_globals['csrf_header_name'] = csrf_header_name(request.rattail_config)
renderer_globals['b'] = 'o' if request.use_oruga else 'b' # for buefy
# theme - we only want do this for classic web app, *not* API # theme - we only want do this for classic web app, *not* API
# TODO: so, clearly we need a better way to distinguish the two # TODO: so, clearly we need a better way to distinguish the two

View file

@ -100,27 +100,27 @@
<h3 class="block is-size-3">Web Libraries</h3> <h3 class="block is-size-3">Web Libraries</h3>
<div class="block" style="padding-left: 2rem;"> <div class="block" style="padding-left: 2rem;">
<b-table :data="weblibs"> <${b}-table :data="weblibs">
<b-table-column field="title" <${b}-table-column field="title"
label="Name" label="Name"
v-slot="props"> v-slot="props">
{{ props.row.title }} {{ props.row.title }}
</b-table-column> </${b}-table-column>
<b-table-column field="configured_version" <${b}-table-column field="configured_version"
label="Version" label="Version"
v-slot="props"> v-slot="props">
{{ props.row.configured_version || props.row.default_version }} {{ props.row.configured_version || props.row.default_version }}
</b-table-column> </${b}-table-column>
<b-table-column field="configured_url" <${b}-table-column field="configured_url"
label="URL Override" label="URL Override"
v-slot="props"> v-slot="props">
{{ props.row.configured_url }} {{ props.row.configured_url }}
</b-table-column> </${b}-table-column>
<b-table-column field="live_url" <${b}-table-column field="live_url"
label="Effective (Live) URL" label="Effective (Live) URL"
v-slot="props"> v-slot="props">
<span v-if="props.row.modified" <span v-if="props.row.modified"
@ -130,19 +130,23 @@
<span v-if="!props.row.modified"> <span v-if="!props.row.modified">
{{ props.row.live_url }} {{ props.row.live_url }}
</span> </span>
</b-table-column> </${b}-table-column>
<b-table-column field="actions" <${b}-table-column field="actions"
label="Actions" label="Actions"
v-slot="props"> v-slot="props">
<a href="#" <a href="#"
@click.prevent="editWebLibraryInit(props.row)"> @click.prevent="editWebLibraryInit(props.row)">
% if request.use_oruga:
<o-icon icon="edit" />
% else:
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
% endif
Edit Edit
</a> </a>
</b-table-column> </${b}-table-column>
</b-table> </${b}-table>
% for weblib in weblibs: % for weblib in weblibs:
${h.hidden('tailbone.libver.{}'.format(weblib['key']), **{':value': "simpleSettings['tailbone.libver.{}']".format(weblib['key'])})} ${h.hidden('tailbone.libver.{}'.format(weblib['key']), **{':value': "simpleSettings['tailbone.libver.{}']".format(weblib['key'])})}
@ -175,14 +179,14 @@
</b-field> </b-field>
<b-field label="Override URL"> <b-field label="Override URL">
<b-input v-model="editWebLibraryURL"> <b-input v-model="editWebLibraryURL"
</b-input> expanded />
</b-field> </b-field>
<b-field label="Effective URL (as of last page load)"> <b-field label="Effective URL (as of last page load)">
<b-input v-model="editWebLibraryRecord.live_url" <b-input v-model="editWebLibraryRecord.live_url"
disabled> disabled
</b-input> expanded />
</b-field> </b-field>
</section> </section>

View file

@ -28,10 +28,11 @@
</div> </div>
<b-collapse class="panel" open> <${b}-collapse class="panel" open>
<template #trigger="props"> <template #trigger="props">
<div class="panel-heading" <div class="panel-heading"
style="cursor: pointer;"
role="button"> role="button">
## TODO: for some reason buefy will "reuse" the icon ## TODO: for some reason buefy will "reuse" the icon
@ -57,30 +58,31 @@
<div class="panel-block"> <div class="panel-block">
<div style="width: 100%;"> <div style="width: 100%;">
<b-table :data="configFiles"> <${b}-table :data="configFiles">
<b-table-column field="priority" <${b}-table-column field="priority"
label="Priority" label="Priority"
v-slot="props"> v-slot="props">
{{ props.row.priority }} {{ props.row.priority }}
</b-table-column> </${b}-table-column>
<b-table-column field="path" <${b}-table-column field="path"
label="File Path" label="File Path"
v-slot="props"> v-slot="props">
{{ props.row.path }} {{ props.row.path }}
</b-table-column> </${b}-table-column>
</b-table> </${b}-table>
</div> </div>
</div> </div>
</b-collapse> </${b}-collapse>
<b-collapse class="panel" <${b}-collapse class="panel"
:open="false"> :open="false">
<template #trigger="props"> <template #trigger="props">
<div class="panel-heading" <div class="panel-heading"
style="cursor: pointer;"
role="button"> role="button">
## TODO: for some reason buefy will "reuse" the icon ## TODO: for some reason buefy will "reuse" the icon
@ -109,7 +111,7 @@
${parent.render_grid_component()} ${parent.render_grid_component()}
</div> </div>
</div> </div>
</b-collapse> </${b}-collapse>
</%def> </%def>
<%def name="modify_this_page_vars()"> <%def name="modify_this_page_vars()">

View file

@ -48,7 +48,12 @@
${h.hidden('profiles', **{':value': 'JSON.stringify(profilesData)'})} ${h.hidden('profiles', **{':value': 'JSON.stringify(profilesData)'})}
<b-notification type="is-warning" <b-notification type="is-warning"
:active.sync="showConfigFilesNote"> % if request.use_oruga:
v-model:active="showConfigFilesNote"
% else:
:active.sync="showConfigFilesNote"
% endif
>
## TODO: should link to some ratman page here, yes? ## TODO: should link to some ratman page here, yes?
<p class="block"> <p class="block">
This tool works by modifying settings in the DB.&nbsp; It This tool works by modifying settings in the DB.&nbsp; It
@ -101,52 +106,52 @@
</div> </div>
</div> </div>
<b-table :data="filteredProfilesData" <${b}-table :data="filteredProfilesData"
:row-class="(row, i) => row.enabled ? null : 'has-background-warning'"> :row-class="(row, i) => row.enabled ? null : 'has-background-warning'">
<b-table-column field="key" <${b}-table-column field="key"
label="Watcher Key" label="Watcher Key"
v-slot="props"> v-slot="props">
{{ props.row.key }} {{ props.row.key }}
</b-table-column> </${b}-table-column>
<b-table-column field="watcher_spec" <${b}-table-column field="watcher_spec"
label="Watcher Spec" label="Watcher Spec"
v-slot="props"> v-slot="props">
{{ props.row.watcher_spec }} {{ props.row.watcher_spec }}
</b-table-column> </${b}-table-column>
<b-table-column field="watcher_dbkey" <${b}-table-column field="watcher_dbkey"
label="DB Key" label="DB Key"
v-slot="props"> v-slot="props">
{{ props.row.watcher_dbkey }} {{ props.row.watcher_dbkey }}
</b-table-column> </${b}-table-column>
<b-table-column field="watcher_delay" <${b}-table-column field="watcher_delay"
label="Loop Delay" label="Loop Delay"
v-slot="props"> v-slot="props">
{{ props.row.watcher_delay }} sec {{ props.row.watcher_delay }} sec
</b-table-column> </${b}-table-column>
<b-table-column field="watcher_retry_attempts" <${b}-table-column field="watcher_retry_attempts"
label="Attempts / Delay" label="Attempts / Delay"
v-slot="props"> v-slot="props">
{{ props.row.watcher_retry_attempts }} / {{ props.row.watcher_retry_delay }} sec {{ props.row.watcher_retry_attempts }} / {{ props.row.watcher_retry_delay }} sec
</b-table-column> </${b}-table-column>
<b-table-column field="watcher_default_runas" <${b}-table-column field="watcher_default_runas"
label="Default Runas" label="Default Runas"
v-slot="props"> v-slot="props">
{{ props.row.watcher_default_runas }} {{ props.row.watcher_default_runas }}
</b-table-column> </${b}-table-column>
<b-table-column label="Consumers" <${b}-table-column label="Consumers"
v-slot="props"> v-slot="props">
{{ consumerShortList(props.row) }} {{ consumerShortList(props.row) }}
</b-table-column> </${b}-table-column>
## <b-table-column field="notes" label="Notes"> ## <${b}-table-column field="notes" label="Notes">
## TODO ## TODO
## ## {{ props.row.notes }} ## ## {{ props.row.notes }}
## </b-table-column> ## </${b}-table-column>
<b-table-column field="enabled" <${b}-table-column field="enabled"
label="Enabled" label="Enabled"
v-slot="props"> v-slot="props">
{{ props.row.enabled ? "Yes" : "No" }} {{ props.row.enabled ? "Yes" : "No" }}
</b-table-column> </${b}-table-column>
<b-table-column label="Actions" <${b}-table-column label="Actions"
v-slot="props" v-slot="props"
v-if="useProfileSettings"> v-if="useProfileSettings">
<a href="#" <a href="#"
@ -162,14 +167,14 @@
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
Delete Delete
</a> </a>
</b-table-column> </${b}-table-column>
<template slot="empty"> <template #empty>
<section class="section"> <section class="section">
<div class="content has-text-grey has-text-centered"> <div class="content has-text-grey has-text-centered">
<p> <p>
<b-icon <b-icon
pack="fas" pack="fas"
icon="fas fa-sad-tear" icon="sad-tear"
size="is-large"> size="is-large">
</b-icon> </b-icon>
</p> </p>
@ -177,7 +182,7 @@
</div> </div>
</section> </section>
</template> </template>
</b-table> </${b}-table>
<b-modal :active.sync="editProfileShowDialog"> <b-modal :active.sync="editProfileShowDialog">
<div class="card"> <div class="card">
@ -199,12 +204,12 @@
</b-field> </b-field>
<b-field grouped> <b-field grouped expanded>
<b-field label="Watcher Spec" <b-field label="Watcher Spec"
:type="editingProfileWatcherSpec ? null : 'is-danger'" :type="editingProfileWatcherSpec ? null : 'is-danger'"
expanded> expanded>
<b-input v-model="editingProfileWatcherSpec"> <b-input v-model="editingProfileWatcherSpec" expanded>
</b-input> </b-input>
</b-field> </b-field>
@ -293,19 +298,19 @@
</div> </div>
<b-table :data="editingProfilePendingWatcherKwargs" <${b}-table :data="editingProfilePendingWatcherKwargs"
style="margin-left: 1rem;"> style="margin-left: 1rem;">
<b-table-column field="key" <${b}-table-column field="key"
label="Key" label="Key"
v-slot="props"> v-slot="props">
{{ props.row.key }} {{ props.row.key }}
</b-table-column> </${b}-table-column>
<b-table-column field="value" <${b}-table-column field="value"
label="Value" label="Value"
v-slot="props"> v-slot="props">
{{ props.row.value }} {{ props.row.value }}
</b-table-column> </${b}-table-column>
<b-table-column label="Actions" <${b}-table-column label="Actions"
v-slot="props"> v-slot="props">
<a href="#" <a href="#"
@click.prevent="editProfileWatcherKwarg(props.row)"> @click.prevent="editProfileWatcherKwarg(props.row)">
@ -319,14 +324,14 @@
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
Delete Delete
</a> </a>
</b-table-column> </${b}-table-column>
<template slot="empty"> <template #empty>
<section class="section"> <section class="section">
<div class="content has-text-grey has-text-centered"> <div class="content has-text-grey has-text-centered">
<p> <p>
<b-icon <b-icon
pack="fas" pack="fas"
icon="fas fa-sad-tear" icon="sad-tear"
size="is-large"> size="is-large">
</b-icon> </b-icon>
</p> </p>
@ -334,7 +339,7 @@
</div> </div>
</section> </section>
</template> </template>
</b-table> </${b}-table>
</div> </div>
@ -350,19 +355,19 @@
</b-checkbox> </b-checkbox>
</b-field> </b-field>
<b-table :data="editingProfilePendingConsumers" <${b}-table :data="editingProfilePendingConsumers"
v-if="!editingProfileWatcherConsumesSelf" v-if="!editingProfileWatcherConsumesSelf"
:row-class="(row, i) => row.enabled ? null : 'has-background-warning'"> :row-class="(row, i) => row.enabled ? null : 'has-background-warning'">
<b-table-column field="key" <${b}-table-column field="key"
label="Consumer" label="Consumer"
v-slot="props"> v-slot="props">
{{ props.row.key }} {{ props.row.key }}
</b-table-column> </${b}-table-column>
<b-table-column style="white-space: nowrap;" <${b}-table-column style="white-space: nowrap;"
v-slot="props"> v-slot="props">
{{ props.row.consumer_delay }} / {{ props.row.consumer_retry_attempts }} / {{ props.row.consumer_retry_delay }} {{ props.row.consumer_delay }} / {{ props.row.consumer_retry_attempts }} / {{ props.row.consumer_retry_delay }}
</b-table-column> </${b}-table-column>
<b-table-column label="Actions" <${b}-table-column label="Actions"
v-slot="props"> v-slot="props">
<a href="#" <a href="#"
class="grid-action" class="grid-action"
@ -377,14 +382,14 @@
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
Delete Delete
</a> </a>
</b-table-column> </${b}-table-column>
<template slot="empty"> <template #empty>
<section class="section"> <section class="section">
<div class="content has-text-grey has-text-centered"> <div class="content has-text-grey has-text-centered">
<p> <p>
<b-icon <b-icon
pack="fas" pack="fas"
icon="fas fa-sad-tear" icon="sad-tear"
size="is-large"> size="is-large">
</b-icon> </b-icon>
</p> </p>
@ -392,7 +397,7 @@
</div> </div>
</section> </section>
</template> </template>
</b-table> </${b}-table>
</div> </div>
@ -526,7 +531,8 @@
expanded> expanded>
<b-input name="supervisor_process_name" <b-input name="supervisor_process_name"
v-model="supervisorProcessName" v-model="supervisorProcessName"
@input="settingsNeedSaved = true"> @input="settingsNeedSaved = true"
expanded>
</b-input> </b-input>
</b-field> </b-field>
@ -535,7 +541,8 @@
expanded> expanded>
<b-input name="restart_command" <b-input name="restart_command"
v-model="restartCommand" v-model="restartCommand"
@input="settingsNeedSaved = true"> @input="settingsNeedSaved = true"
expanded>
</b-input> </b-input>
</b-field> </b-field>

View file

@ -47,79 +47,79 @@
</div> </div>
</b-field> </b-field>
<b-field label="Watcher Status"> <h3 class="is-size-3">Watcher Status</h3>
<b-table :data="watchers">
<b-table-column field="key" <${b}-table :data="watchers">
<${b}-table-column field="key"
label="Watcher" label="Watcher"
v-slot="props"> v-slot="props">
{{ props.row.key }} {{ props.row.key }}
</b-table-column> </${b}-table-column>
<b-table-column field="spec" <${b}-table-column field="spec"
label="Spec" label="Spec"
v-slot="props"> v-slot="props">
{{ props.row.spec }} {{ props.row.spec }}
</b-table-column> </${b}-table-column>
<b-table-column field="dbkey" <${b}-table-column field="dbkey"
label="DB Key" label="DB Key"
v-slot="props"> v-slot="props">
{{ props.row.dbkey }} {{ props.row.dbkey }}
</b-table-column> </${b}-table-column>
<b-table-column field="delay" <${b}-table-column field="delay"
label="Delay" label="Delay"
v-slot="props"> v-slot="props">
{{ props.row.delay }} second(s) {{ props.row.delay }} second(s)
</b-table-column> </${b}-table-column>
<b-table-column field="lastrun" <${b}-table-column field="lastrun"
label="Last Watched" label="Last Watched"
v-slot="props"> v-slot="props">
<span v-html="props.row.lastrun"></span> <span v-html="props.row.lastrun"></span>
</b-table-column> </${b}-table-column>
<b-table-column field="status" <${b}-table-column field="status"
label="Status" label="Status"
v-slot="props"> v-slot="props">
<span :class="props.row.status == 'okay' ? 'has-background-success' : 'has-background-warning'"> <span :class="props.row.status == 'okay' ? 'has-background-success' : 'has-background-warning'">
{{ props.row.status }} {{ props.row.status }}
</span> </span>
</b-table-column> </${b}-table-column>
</b-table> </${b}-table>
</b-field>
<b-field label="Consumer Status"> <h3 class="is-size-3">Consumer Status</h3>
<b-table :data="consumers">
<b-table-column field="key" <${b}-table :data="consumers">
<${b}-table-column field="key"
label="Consumer" label="Consumer"
v-slot="props"> v-slot="props">
{{ props.row.key }} {{ props.row.key }}
</b-table-column> </${b}-table-column>
<b-table-column field="spec" <${b}-table-column field="spec"
label="Spec" label="Spec"
v-slot="props"> v-slot="props">
{{ props.row.spec }} {{ props.row.spec }}
</b-table-column> </${b}-table-column>
<b-table-column field="dbkey" <${b}-table-column field="dbkey"
label="DB Key" label="DB Key"
v-slot="props"> v-slot="props">
{{ props.row.dbkey }} {{ props.row.dbkey }}
</b-table-column> </${b}-table-column>
<b-table-column field="delay" <${b}-table-column field="delay"
label="Delay" label="Delay"
v-slot="props"> v-slot="props">
{{ props.row.delay }} second(s) {{ props.row.delay }} second(s)
</b-table-column> </${b}-table-column>
<b-table-column field="changes" <${b}-table-column field="changes"
label="Pending Changes" label="Pending Changes"
v-slot="props"> v-slot="props">
{{ props.row.changes }} {{ props.row.changes }}
</b-table-column> </${b}-table-column>
<b-table-column field="status" <${b}-table-column field="status"
label="Status" label="Status"
v-slot="props"> v-slot="props">
<span :class="props.row.status == 'okay' ? 'has-background-success' : 'has-background-warning'"> <span :class="props.row.status == 'okay' ? 'has-background-success' : 'has-background-warning'">
{{ props.row.status }} {{ props.row.status }}
</span> </span>
</b-table-column> </${b}-table-column>
</b-table> </${b}-table>
</b-field>
</%def> </%def>
<%def name="modify_this_page_vars()"> <%def name="modify_this_page_vars()">

View file

@ -1,5 +1,7 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<% request.register_component(form.component, form.component_studly) %>
<script type="text/x-template" id="${form.component}-template"> <script type="text/x-template" id="${form.component}-template">
<div> <div>

View file

@ -106,55 +106,68 @@
</div> </div>
</div> </div>
<b-table <${b}-table
:data="new_table.columns"> :data="new_table.columns">
<b-table-column field="name" <${b}-table-column field="name"
label="Name" label="Name"
v-slot="props"> v-slot="props">
{{ props.row.name }} {{ props.row.name }}
</b-table-column> </${b}-table-column>
<b-table-column field="data_type" <${b}-table-column field="data_type"
label="Data Type" label="Data Type"
v-slot="props"> v-slot="props">
{{ props.row.data_type }} {{ props.row.data_type }}
</b-table-column> </${b}-table-column>
<b-table-column field="nullable" <${b}-table-column field="nullable"
label="Nullable" label="Nullable"
v-slot="props"> v-slot="props">
{{ props.row.nullable }} {{ props.row.nullable }}
</b-table-column> </${b}-table-column>
<b-table-column field="description" <${b}-table-column field="description"
label="Description" label="Description"
v-slot="props"> v-slot="props">
{{ props.row.description }} {{ props.row.description }}
</b-table-column> </${b}-table-column>
<b-table-column field="actions" <${b}-table-column field="actions"
label="Actions" label="Actions"
v-slot="props"> v-slot="props">
<a href="#" class="grid-action" <a href="#" class="grid-action"
@click.prevent="editColumnRow(props.row)"> @click.prevent="editColumnRow(props)">
% if request.use_oruga:
<o-icon icon="edit" />
% else:
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
% endif
Edit Edit
</a> </a>
&nbsp; &nbsp;
<a href="#" class="grid-action has-text-danger" <a href="#" class="grid-action has-text-danger"
@click.prevent="deleteColumn(props.index)"> @click.prevent="deleteColumn(props.index)">
% if request.use_oruga:
<o-icon icon="trash" />
% else:
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
% endif
Delete Delete
</a> </a>
&nbsp; &nbsp;
</b-table-column> </${b}-table-column>
</b-table> </${b}-table>
<b-modal has-modal-card <${b}-modal has-modal-card
:active.sync="showingEditColumn"> % if request.use_oruga:
v-model:active="showingEditColumn"
% else:
:active.sync="showingEditColumn"
% endif
>
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head"> <header class="modal-card-head">
@ -197,7 +210,7 @@
</b-button> </b-button>
</footer> </footer>
</div> </div>
</b-modal> </${b}-modal>
</div> </div>
</b-field> </b-field>
@ -318,6 +331,7 @@
ThisPageData.showingEditColumn = false ThisPageData.showingEditColumn = false
ThisPageData.editingColumn = null ThisPageData.editingColumn = null
ThisPageData.editingColumnIndex = null
ThisPageData.editingColumnName = null ThisPageData.editingColumnName = null
ThisPageData.editingColumnDataType = null ThisPageData.editingColumnDataType = null
ThisPageData.editingColumnNullable = null ThisPageData.editingColumnNullable = null
@ -325,6 +339,7 @@
ThisPage.methods.addColumn = function(column) { ThisPage.methods.addColumn = function(column) {
this.editingColumn = null this.editingColumn = null
this.editingColumnIndex = null
this.editingColumnName = null this.editingColumnName = null
this.editingColumnDataType = null this.editingColumnDataType = null
this.editingColumnNullable = true this.editingColumnNullable = true
@ -332,8 +347,10 @@
this.showingEditColumn = true this.showingEditColumn = true
} }
ThisPage.methods.editColumnRow = function(column) { ThisPage.methods.editColumnRow = function(props) {
const column = props.row
this.editingColumn = column this.editingColumn = column
this.editingColumnIndex = props.index
this.editingColumnName = column.name this.editingColumnName = column.name
this.editingColumnDataType = column.data_type this.editingColumnDataType = column.data_type
this.editingColumnNullable = column.nullable this.editingColumnNullable = column.nullable
@ -343,7 +360,7 @@
ThisPage.methods.saveColumn = function() { ThisPage.methods.saveColumn = function() {
if (this.editingColumn) { if (this.editingColumn) {
column = this.editingColumn column = this.new_table.columns[this.editingColumnIndex]
} else { } else {
column = {} column = {}
this.new_table.columns.push(column) this.new_table.columns.push(column)

View file

@ -1,5 +1,5 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<b-table <${b}-table
:data="${data_prop}" :data="${data_prop}"
icon-pack="fas" icon-pack="fas"
striped striped
@ -21,7 +21,7 @@
> >
% for i, column in enumerate(grid_columns): % for i, column in enumerate(grid_columns):
<b-table-column field="${column['field']}" <${b}-table-column field="${column['field']}"
% if not empty_labels: % if not empty_labels:
label="${column['label']}" label="${column['label']}"
% elif i > 0: % elif i > 0:
@ -50,11 +50,11 @@
% else: % else:
<span v-html="props.row.${column['field']}"></span> <span v-html="props.row.${column['field']}"></span>
% endif % endif
</b-table-column> </${b}-table-column>
% endfor % endfor
% if grid.main_actions or grid.more_actions: % if grid.main_actions or grid.more_actions:
<b-table-column field="actions" <${b}-table-column field="actions"
label="Actions" label="Actions"
v-slot="props"> v-slot="props">
% for action in grid.main_actions: % for action in grid.main_actions:
@ -68,12 +68,16 @@
@click.prevent="${action.click_handler}" @click.prevent="${action.click_handler}"
% endif % endif
> >
% if request.use_oruga:
<o-icon icon="${action.icon}" />
% else:
<i class="fas fa-${action.icon}"></i> <i class="fas fa-${action.icon}"></i>
% endif
${action.label} ${action.label}
</a> </a>
&nbsp; &nbsp;
% endfor % endfor
</b-table-column> </${b}-table-column>
% endif % endif
<template #empty> <template #empty>
@ -99,4 +103,4 @@
</template> </template>
% endif % endif
</b-table> </${b}-table>

View file

@ -1,5 +1,7 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<% request.register_component(grid.component, grid.component_studly) %>
<script type="text/x-template" id="${grid.component}-template"> <script type="text/x-template" id="${grid.component}-template">
<div> <div>
@ -38,7 +40,7 @@
</div> </div>
<b-table <${b}-table
:data="visibleData" :data="visibleData"
:loading="loading" :loading="loading"
:row-class="getRowClass" :row-class="getRowClass"
@ -51,7 +53,11 @@
:checkable="checkable" :checkable="checkable"
% if grid.checkboxes: % if grid.checkboxes:
% if request.use_oruga:
v-model:checked-rows="checkedRows"
% else:
:checked-rows.sync="checkedRows" :checked-rows.sync="checkedRows"
% endif
% if grid.clicking_row_checks_box: % if grid.clicking_row_checks_box:
@click="rowClick" @click="rowClick"
% endif % endif
@ -111,7 +117,7 @@
:narrowed="true"> :narrowed="true">
% for column in grid_columns: % for column in grid_columns:
<b-table-column field="${column['field']}" <${b}-table-column field="${column['field']}"
label="${column['label']}" label="${column['label']}"
v-slot="props" v-slot="props"
:sortable="${json.dumps(column['sortable'])}" :sortable="${json.dumps(column['sortable'])}"
@ -132,11 +138,11 @@
% else: % else:
<span v-html="props.row.${column['field']}"></span> <span v-html="props.row.${column['field']}"></span>
% endif % endif
</b-table-column> </${b}-table-column>
% endfor % endfor
% if grid.main_actions or grid.more_actions: % if grid.main_actions or grid.more_actions:
<b-table-column field="actions" <${b}-table-column field="actions"
label="Actions" label="Actions"
v-slot="props"> v-slot="props">
## TODO: we do not currently differentiate for "main vs. more" ## TODO: we do not currently differentiate for "main vs. more"
@ -152,12 +158,17 @@
target="${action.target}" target="${action.target}"
% endif % endif
> >
% if request.use_oruga:
<o-icon icon="${action.icon}" />
<span>${action.render_label()|n}</span>
% else:
${action.render_icon()|n} ${action.render_icon()|n}
${action.render_label()|n} ${action.render_label()|n}
% endif
</a> </a>
&nbsp; &nbsp;
% endfor % endfor
</b-table-column> </${b}-table-column>
% endif % endif
<template #empty> <template #empty>
@ -183,7 +194,11 @@
size="is-small" size="is-small"
@click="copyDirectLink()" @click="copyDirectLink()"
title="Copy link to clipboard"> title="Copy link to clipboard">
% if request.use_oruga:
<o-icon icon="share-alt" />
% else:
<span><i class="fa fa-share-alt"></i></span> <span><i class="fa fa-share-alt"></i></span>
% endif
</b-button> </b-button>
% else: % else:
<div></div> <div></div>
@ -213,7 +228,7 @@
</div> </div>
</template> </template>
</b-table> </${b}-table>
## dummy input field needed for sharing links on *insecure* sites ## dummy input field needed for sharing links on *insecure* sites
% if request.scheme == 'http': % if request.scheme == 'http':
@ -523,6 +538,12 @@
}, },
perPageUpdated(value) { perPageUpdated(value) {
// nb. buefy passes value, oruga passes event
if (value.target) {
value = event.target.value
}
this.loadAsyncData({ this.loadAsyncData({
pagesize: value, pagesize: value,
}) })
@ -530,6 +551,11 @@
onSort(field, order, event) { onSort(field, order, event) {
// nb. buefy passes field name, oruga passes object
if (field.field) {
field = field.field
}
if (event.ctrlKey) { if (event.ctrlKey) {
// engage or enhance multi-column sorting // engage or enhance multi-column sorting

View file

@ -7,6 +7,7 @@
</%def> </%def>
<%def name="make_grid_filter_numeric_value_component()"> <%def name="make_grid_filter_numeric_value_component()">
<% request.register_component('grid-filter-numeric-value', 'GridFilterNumericValue') %>
<script type="text/x-template" id="grid-filter-numeric-value-template"> <script type="text/x-template" id="grid-filter-numeric-value-template">
<div class="level"> <div class="level">
<div class="level-left"> <div class="level-left">
@ -95,13 +96,14 @@
</%def> </%def>
<%def name="make_grid_filter_date_value_component()"> <%def name="make_grid_filter_date_value_component()">
<% request.register_component('grid-filter-date-value', 'GridFilterDateValue') %>
<script type="text/x-template" id="grid-filter-date-value-template"> <script type="text/x-template" id="grid-filter-date-value-template">
<div class="level"> <div class="level">
<div class="level-left"> <div class="level-left">
<div class="level-item"> <div class="level-item">
<tailbone-datepicker v-model="startDate" <tailbone-datepicker v-model="startDate"
ref="startDate" ref="startDate"
@input="startDateChanged"> @${'update:model-value' if request.use_oruga else 'input'}="startDateChanged">
</tailbone-datepicker> </tailbone-datepicker>
</div> </div>
<div v-show="dateRange" <div v-show="dateRange"
@ -112,7 +114,7 @@
class="level-item"> class="level-item">
<tailbone-datepicker v-model="endDate" <tailbone-datepicker v-model="endDate"
ref="endDate" ref="endDate"
@input="endDateChanged"> @${'update:model-value' if request.use_oruga else 'input'}="endDateChanged">
</tailbone-datepicker> </tailbone-datepicker>
</div> </div>
</div> </div>
@ -123,25 +125,26 @@
const GridFilterDateValue = { const GridFilterDateValue = {
template: '#grid-filter-date-value-template', template: '#grid-filter-date-value-template',
props: { props: {
value: String, ${'modelValue' if request.use_oruga else 'value'}: String,
dateRange: Boolean, dateRange: Boolean,
}, },
data() { data() {
let startDate = null let startDate = null
let endDate = null let endDate = null
if (this.value) { let value = this.${'modelValue' if request.use_oruga else 'value'}
if (value) {
if (this.dateRange) { if (this.dateRange) {
let values = this.value.split('|') let values = value.split('|')
if (values.length == 2) { if (values.length == 2) {
startDate = this.parseDate(values[0]) startDate = this.parseDate(values[0])
endDate = this.parseDate(values[1]) endDate = this.parseDate(values[1])
} else { // no end date specified? } else { // no end date specified?
startDate = this.parseDate(this.value) startDate = this.parseDate(value)
} }
} else { // not a range, so start date only } else { // not a range, so start date only
startDate = this.parseDate(this.value) startDate = this.parseDate(value)
} }
} }
@ -179,11 +182,11 @@
if (this.dateRange) { if (this.dateRange) {
value += '|' + this.formatDate(this.endDate) value += '|' + this.formatDate(this.endDate)
} }
this.$emit('input', value) this.$emit("${'update:modelValue' if request.use_oruga else 'input'}", value)
}, },
endDateChanged(value) { endDateChanged(value) {
value = this.formatDate(this.startDate) + '|' + this.formatDate(value) value = this.formatDate(this.startDate) + '|' + this.formatDate(value)
this.$emit('input', value) this.$emit("${'update:modelValue' if request.use_oruga else 'input'}", value)
}, },
}, },
} }
@ -194,6 +197,7 @@
</%def> </%def>
<%def name="make_grid_filter_component()"> <%def name="make_grid_filter_component()">
<% request.register_component('grid-filter', 'GridFilter') %>
<script type="text/x-template" id="grid-filter-template"> <script type="text/x-template" id="grid-filter-template">
<div class="filter" <div class="filter"
v-show="filter.visible" v-show="filter.visible"

View file

@ -6,54 +6,58 @@
<h3 class="is-size-3">Designated Handlers</h3> <h3 class="is-size-3">Designated Handlers</h3>
<b-table :data="handlersData" <${b}-table :data="handlersData"
narrowed narrowed
icon-pack="fas" icon-pack="fas"
:default-sort="['host_title', 'asc']"> :default-sort="['host_title', 'asc']">
<b-table-column field="host_title" <${b}-table-column field="host_title"
label="Data Source" label="Data Source"
v-slot="props" v-slot="props"
sortable> sortable>
{{ props.row.host_title }} {{ props.row.host_title }}
</b-table-column> </${b}-table-column>
<b-table-column field="local_title" <${b}-table-column field="local_title"
label="Data Target" label="Data Target"
v-slot="props" v-slot="props"
sortable> sortable>
{{ props.row.local_title }} {{ props.row.local_title }}
</b-table-column> </${b}-table-column>
<b-table-column field="direction" <${b}-table-column field="direction"
label="Direction" label="Direction"
v-slot="props" v-slot="props"
sortable> sortable>
{{ props.row.direction_display }} {{ props.row.direction_display }}
</b-table-column> </${b}-table-column>
<b-table-column field="handler_spec" <${b}-table-column field="handler_spec"
label="Handler Spec" label="Handler Spec"
v-slot="props" v-slot="props"
sortable> sortable>
{{ props.row.handler_spec }} {{ props.row.handler_spec }}
</b-table-column> </${b}-table-column>
<b-table-column field="cmd" <${b}-table-column field="cmd"
label="Command" label="Command"
v-slot="props" v-slot="props"
sortable> sortable>
{{ props.row.command }} {{ props.row.subcommand }} {{ props.row.command }} {{ props.row.subcommand }}
</b-table-column> </${b}-table-column>
<b-table-column field="runas" <${b}-table-column field="runas"
label="Default Runas" label="Default Runas"
v-slot="props" v-slot="props"
sortable> sortable>
{{ props.row.default_runas }} {{ props.row.default_runas }}
</b-table-column> </${b}-table-column>
<b-table-column label="Actions" <${b}-table-column label="Actions"
v-slot="props"> v-slot="props">
<a href="#" class="grid-action" <a href="#" class="grid-action"
@click.prevent="editHandler(props.row)"> @click.prevent="editHandler(props.row)">
% if request.use_oruga:
<o-icon icon="edit" />
% else:
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
% endif
Edit Edit
</a> </a>
</b-table-column> </${b}-table-column>
<template slot="empty"> <template slot="empty">
<section class="section"> <section class="section">
<div class="content has-text-grey has-text-centered"> <div class="content has-text-grey has-text-centered">
@ -68,7 +72,7 @@
</div> </div>
</section> </section>
</template> </template>
</b-table> </${b}-table>
<b-modal :active.sync="editHandlerShowDialog"> <b-modal :active.sync="editHandlerShowDialog">
<div class="card"> <div class="card">

View file

@ -22,48 +22,56 @@
</div> </div>
<div class="block" style="padding-left: 2rem; display: flex;"> <div class="block" style="padding-left: 2rem; display: flex;">
<b-table :data="overnightTasks"> <${b}-table :data="overnightTasks">
<!-- <b-table-column field="key" --> <!-- <${b}-table-column field="key" -->
<!-- label="Key" --> <!-- label="Key" -->
<!-- sortable> --> <!-- sortable> -->
<!-- {{ props.row.key }} --> <!-- {{ props.row.key }} -->
<!-- </b-table-column> --> <!-- </${b}-table-column> -->
<b-table-column field="key" <${b}-table-column field="key"
label="Key" label="Key"
v-slot="props"> v-slot="props">
{{ props.row.key }} {{ props.row.key }}
</b-table-column> </${b}-table-column>
<b-table-column field="description" <${b}-table-column field="description"
label="Description" label="Description"
v-slot="props"> v-slot="props">
{{ props.row.description }} {{ props.row.description }}
</b-table-column> </${b}-table-column>
<b-table-column field="class_name" <${b}-table-column field="class_name"
label="Class Name" label="Class Name"
v-slot="props"> v-slot="props">
{{ props.row.class_name }} {{ props.row.class_name }}
</b-table-column> </${b}-table-column>
<b-table-column field="script" <${b}-table-column field="script"
label="Script" label="Script"
v-slot="props"> v-slot="props">
{{ props.row.script }} {{ props.row.script }}
</b-table-column> </${b}-table-column>
<b-table-column label="Actions" <${b}-table-column label="Actions"
v-slot="props"> v-slot="props">
<a href="#" <a href="#"
@click.prevent="overnightTaskEdit(props.row)"> @click.prevent="overnightTaskEdit(props.row)">
% if request.use_oruga:
<o-icon icon="edit" />
% else:
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
% endif
Edit Edit
</a> </a>
&nbsp; &nbsp;
<a href="#" <a href="#"
class="has-text-danger" class="has-text-danger"
@click.prevent="overnightTaskDelete(props.row)"> @click.prevent="overnightTaskDelete(props.row)">
% if request.use_oruga:
<o-icon icon="trash" />
% else:
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
% endif
Delete Delete
</a> </a>
</b-table-column> </${b}-table-column>
</b-table> </${b}-table>
<b-modal has-modal-card <b-modal has-modal-card
:active.sync="overnightTaskShowDialog"> :active.sync="overnightTaskShowDialog">
@ -139,48 +147,56 @@
</div> </div>
<div class="block" style="padding-left: 2rem; display: flex;"> <div class="block" style="padding-left: 2rem; display: flex;">
<b-table :data="backfillTasks"> <${b}-table :data="backfillTasks">
<b-table-column field="key" <${b}-table-column field="key"
label="Key" label="Key"
v-slot="props"> v-slot="props">
{{ props.row.key }} {{ props.row.key }}
</b-table-column> </${b}-table-column>
<b-table-column field="description" <${b}-table-column field="description"
label="Description" label="Description"
v-slot="props"> v-slot="props">
{{ props.row.description }} {{ props.row.description }}
</b-table-column> </${b}-table-column>
<b-table-column field="script" <${b}-table-column field="script"
label="Script" label="Script"
v-slot="props"> v-slot="props">
{{ props.row.script }} {{ props.row.script }}
</b-table-column> </${b}-table-column>
<b-table-column field="forward" <${b}-table-column field="forward"
label="Orientation" label="Orientation"
v-slot="props"> v-slot="props">
{{ props.row.forward ? "Forward" : "Backward" }} {{ props.row.forward ? "Forward" : "Backward" }}
</b-table-column> </${b}-table-column>
<b-table-column field="target_date" <${b}-table-column field="target_date"
label="Target Date" label="Target Date"
v-slot="props"> v-slot="props">
{{ props.row.target_date }} {{ props.row.target_date }}
</b-table-column> </${b}-table-column>
<b-table-column label="Actions" <${b}-table-column label="Actions"
v-slot="props"> v-slot="props">
<a href="#" <a href="#"
@click.prevent="backfillTaskEdit(props.row)"> @click.prevent="backfillTaskEdit(props.row)">
% if request.use_oruga:
<o-icon icon="edit" />
% else:
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
% endif
Edit Edit
</a> </a>
&nbsp; &nbsp;
<a href="#" <a href="#"
class="has-text-danger" class="has-text-danger"
@click.prevent="backfillTaskDelete(props.row)"> @click.prevent="backfillTaskDelete(props.row)">
% if request.use_oruga:
<o-icon icon="trash" />
% else:
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
% endif
Delete Delete
</a> </a>
</b-table-column> </${b}-table-column>
</b-table> </${b}-table>
<b-modal has-modal-card <b-modal has-modal-card
:active.sync="backfillTaskShowDialog"> :active.sync="backfillTaskShowDialog">

View file

@ -53,25 +53,25 @@
<h3 class="block is-size-3">Overnight Tasks</h3> <h3 class="block is-size-3">Overnight Tasks</h3>
<b-table :data="overnightTasks" hoverable> <${b}-table :data="overnightTasks" hoverable>
<b-table-column field="description" <${b}-table-column field="description"
label="Description" label="Description"
v-slot="props"> v-slot="props">
{{ props.row.description }} {{ props.row.description }}
</b-table-column> </${b}-table-column>
<b-table-column field="script" <${b}-table-column field="script"
label="Command" label="Command"
v-slot="props"> v-slot="props">
{{ props.row.script || props.row.class_name }} {{ props.row.script || props.row.class_name }}
</b-table-column> </${b}-table-column>
<b-table-column field="last_date" <${b}-table-column field="last_date"
label="Last Date" label="Last Date"
v-slot="props"> v-slot="props">
<span :class="overnightTextClass(props.row)"> <span :class="overnightTextClass(props.row)">
{{ props.row.last_date || "never!" }} {{ props.row.last_date || "never!" }}
</span> </span>
</b-table-column> </${b}-table-column>
<b-table-column label="Actions" <${b}-table-column label="Actions"
v-slot="props"> v-slot="props">
<b-button type="is-primary" <b-button type="is-primary"
icon-pack="fas" icon-pack="fas"
@ -128,11 +128,11 @@
</footer> </footer>
</div> </div>
</b-modal> </b-modal>
</b-table-column> </${b}-table-column>
<template #empty> <template #empty>
<p class="block">No tasks defined.</p> <p class="block">No tasks defined.</p>
</template> </template>
</b-table> </${b}-table>
% endif % endif
@ -140,35 +140,35 @@
<h3 class="block is-size-3">Backfill Tasks</h3> <h3 class="block is-size-3">Backfill Tasks</h3>
<b-table :data="backfillTasks" hoverable> <${b}-table :data="backfillTasks" hoverable>
<b-table-column field="description" <${b}-table-column field="description"
label="Description" label="Description"
v-slot="props"> v-slot="props">
{{ props.row.description }} {{ props.row.description }}
</b-table-column> </${b}-table-column>
<b-table-column field="script" <${b}-table-column field="script"
label="Script" label="Script"
v-slot="props"> v-slot="props">
{{ props.row.script }} {{ props.row.script }}
</b-table-column> </${b}-table-column>
<b-table-column field="forward" <${b}-table-column field="forward"
label="Orientation" label="Orientation"
v-slot="props"> v-slot="props">
{{ props.row.forward ? "Forward" : "Backward" }} {{ props.row.forward ? "Forward" : "Backward" }}
</b-table-column> </${b}-table-column>
<b-table-column field="last_date" <${b}-table-column field="last_date"
label="Last Date" label="Last Date"
v-slot="props"> v-slot="props">
<span :class="backfillTextClass(props.row)"> <span :class="backfillTextClass(props.row)">
{{ props.row.last_date }} {{ props.row.last_date }}
</span> </span>
</b-table-column> </${b}-table-column>
<b-table-column field="target_date" <${b}-table-column field="target_date"
label="Target Date" label="Target Date"
v-slot="props"> v-slot="props">
{{ props.row.target_date }} {{ props.row.target_date }}
</b-table-column> </${b}-table-column>
<b-table-column label="Actions" <${b}-table-column label="Actions"
v-slot="props"> v-slot="props">
<b-button type="is-primary" <b-button type="is-primary"
icon-pack="fas" icon-pack="fas"
@ -176,11 +176,11 @@
@click="backfillTaskLaunch(props.row)"> @click="backfillTaskLaunch(props.row)">
Launch Launch
</b-button> </b-button>
</b-table-column> </${b}-table-column>
<template #empty> <template #empty>
<p class="block">No tasks defined.</p> <p class="block">No tasks defined.</p>
</template> </template>
</b-table> </${b}-table>
<b-modal has-modal-card <b-modal has-modal-card
:active.sync="backfillTaskShowLaunchDialog"> :active.sync="backfillTaskShowLaunchDialog">

View file

@ -177,6 +177,8 @@
Vue.component('merge-buttons', MergeButtons) Vue.component('merge-buttons', MergeButtons)
<% request.register_component('merge-buttons', 'MergeButtons') %>
</script> </script>
</%def> </%def>

View file

@ -12,7 +12,11 @@
<b-button title="&quot;Touch&quot; this record to trigger sync" <b-button title="&quot;Touch&quot; this record to trigger sync"
@click="touchRecord()" @click="touchRecord()"
:disabled="touchSubmitting"> :disabled="touchSubmitting">
% if request.use_oruga:
<o-icon icon="hand-pointer" />
% else:
<span><i class="fa fa-hand-pointer"></i></span> <span><i class="fa fa-hand-pointer"></i></span>
% endif
</b-button> </b-button>
% endif % endif
% if expose_versions: % if expose_versions:
@ -112,7 +116,11 @@
<p class="block"> <p class="block">
<a href="${master.get_action_url('versions', instance)}" <a href="${master.get_action_url('versions', instance)}"
target="_blank"> target="_blank">
% if request.use_oruga:
<o-icon icon="external-link-alt" />
% else:
<i class="fas fa-external-link-alt"></i> <i class="fas fa-external-link-alt"></i>
% endif
View as separate page View as separate page
</a> </a>
</p> </p>
@ -122,7 +130,13 @@
@view-revision="viewRevision"> @view-revision="viewRevision">
</versions-grid> </versions-grid>
<b-modal :active.sync="viewVersionShowDialog" :width="1200"> <${b}-modal :width="1200"
% if request.use_oruga:
v-model:active="viewVersionShowDialog"
% else:
:active.sync="viewVersionShowDialog"
% endif
>
<div class="card"> <div class="card">
<div class="card-content"> <div class="card-content">
<div style="display: flex; flex-direction: column; gap: 1.5rem;"> <div style="display: flex; flex-direction: column; gap: 1.5rem;">
@ -169,7 +183,11 @@
<div> <div>
<a :href="viewVersionData.url" <a :href="viewVersionData.url"
target="_blank"> target="_blank">
% if request.use_oruga:
<o-icon icon="external-link-alt" />
% else:
<i class="fas fa-external-link-alt"></i> <i class="fas fa-external-link-alt"></i>
% endif
View as separate page View as separate page
</a> </a>
</div> </div>
@ -212,10 +230,14 @@
</div> </div>
</div> </div>
% if request.use_oruga:
<o-loading v-model:active="viewVersionLoading" :is-full-page="false" />
% else:
<b-loading :active.sync="viewVersionLoading" :is-full-page="false"></b-loading> <b-loading :active.sync="viewVersionLoading" :is-full-page="false"></b-loading>
% endif
</div> </div>
</div> </div>
</b-modal> </${b}-modal>
</div> </div>
% endif % endif
</%def> </%def>

View file

@ -72,6 +72,7 @@
ThisPage.data = function() { return ThisPageData } ThisPage.data = function() { return ThisPageData }
Vue.component('this-page', ThisPage) Vue.component('this-page', ThisPage)
<% request.register_component('this-page', 'ThisPage') %>
</script> </script>
</%def> </%def>

View file

@ -6,6 +6,37 @@
% if help_url or help_markdown: % if help_url or help_markdown:
% if request.use_oruga:
<o-button icon-left="question-circle"
% if help_markdown:
@click="displayInit()"
% elif help_url:
tag="a" href="${help_url}"
target="_blank"
% endif
>
Help
</o-button>
% if can_edit_help:
## TODO: this dropdown is duplicated, below
<o-dropdown position="bottom-left"
## TODO: why does click not work here?!
:triggers="['click', 'hover']">
<template #trigger>
<o-button>
<o-icon icon="cog" />
</o-button>
</template>
<o-dropdown-item label="Edit Page Help"
@click="configureInit()" />
<o-dropdown-item label="Edit Fields Help"
@click="configureFieldsInit()" />
</o-dropdown>
% endif
% else:
## buefy
<b-field> <b-field>
<p class="control"> <p class="control">
<b-button icon-pack="fas" <b-button icon-pack="fas"
@ -39,17 +70,39 @@
</b-dropdown> </b-dropdown>
% endif % endif
</b-field> </b-field>
% endif:
% elif can_edit_help: % elif can_edit_help:
## TODO: this dropdown is duplicated, above
% if request.use_oruga:
<o-dropdown position="bottom-left"
## TODO: why does click not work here?!
:triggers="['click', 'hover']">
<template #trigger>
<o-button>
<o-icon icon="question-circle" />
<o-icon icon="cog" />
</o-button>
</template>
<o-dropdown-item label="Edit Page Help"
@click="configureInit()" />
<o-dropdown-item label="Edit Fields Help"
@click="configureFieldsInit()" />
</o-dropdown>
% else:
<b-field> <b-field>
<p class="control"> <p class="control">
## TODO: this dropdown is duplicated, above
<b-dropdown aria-role="list" position="is-bottom-left"> <b-dropdown aria-role="list" position="is-bottom-left">
<template #trigger="{ active }"> <template #trigger>
<b-button> <b-button>
% if request.use_oruga:
<o-icon icon="question-circle" />
<o-icon icon="cog" />
% else:
<span><i class="fa fa-question-circle"></i></span> <span><i class="fa fa-question-circle"></i></span>
<span><i class="fa fa-cog"></i></span> <span><i class="fa fa-cog"></i></span>
% endif
</b-button> </b-button>
</template> </template>
<b-dropdown-item aria-role="listitem" <b-dropdown-item aria-role="listitem"
@ -63,12 +116,17 @@
</b-dropdown> </b-dropdown>
</p> </p>
</b-field> </b-field>
% endif
% endif % endif
% if help_markdown: % if help_markdown:
<b-modal has-modal-card <${b}-modal has-modal-card
:active.sync="displayShowDialog"> % if request.use_oruga:
v-model:active="displayShowDialog"
% else:
:active.sync="displayShowDialog"
% endif
>
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head"> <header class="modal-card-head">
@ -94,14 +152,23 @@
</b-button> </b-button>
</footer> </footer>
</div> </div>
</b-modal> </${b}-modal>
% endif % endif
% if can_edit_help: % if can_edit_help:
<b-modal has-modal-card <${b}-modal has-modal-card
:active.sync="configureShowDialog"> % if request.use_oruga:
<div class="modal-card"> v-model:active="configureShowDialog"
% else:
:active.sync="configureShowDialog"
% endif
>
<div class="modal-card"
% if request.use_oruga:
style="margin: auto;"
% endif
>
<header class="modal-card-head"> <header class="modal-card-head">
<p class="modal-card-title">Configure Help</p> <p class="modal-card-title">Configure Help</p>
@ -155,7 +222,7 @@
</b-button> </b-button>
</footer> </footer>
</div> </div>
</b-modal> </${b}-modal>
% endif % endif
@ -237,6 +304,7 @@
PageHelp.data = function() { return PageHelpData } PageHelp.data = function() { return PageHelpData }
Vue.component('page-help', PageHelp) Vue.component('page-help', PageHelp)
<% request.register_component('page-help', 'PageHelp') %>
</script> </script>
</%def> </%def>

View file

@ -242,6 +242,8 @@
Vue.component('find-principals', FindPrincipals) Vue.component('find-principals', FindPrincipals)
<% request.register_component('find-principals', 'FindPrincipals') %>
</script> </script>
</%def> </%def>

View file

@ -102,6 +102,8 @@
Vue.component('email-preview-tools', EmailPreviewTools) Vue.component('email-preview-tools', EmailPreviewTools)
<% request.register_component('email-preview-tools', 'EmailPreviewTools') %>
</script> </script>
</%def> </%def>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,679 @@
<%def name="make_buefy_components()">
${self.make_b_autocomplete_component()}
${self.make_b_button_component()}
${self.make_b_checkbox_component()}
${self.make_b_collapse_component()}
${self.make_b_datepicker_component()}
${self.make_b_dropdown_component()}
${self.make_b_dropdown_item_component()}
${self.make_b_field_component()}
${self.make_b_icon_component()}
${self.make_b_input_component()}
${self.make_b_loading_component()}
${self.make_b_modal_component()}
${self.make_b_notification_component()}
${self.make_b_select_component()}
${self.make_b_steps_component()}
${self.make_b_step_item_component()}
${self.make_b_table_component()}
${self.make_b_table_column_component()}
${self.make_once_button_component()}
</%def>
<%def name="make_b_autocomplete_component()">
<script type="text/x-template" id="b-autocomplete-template">
<o-autocomplete v-model="buefyValue"
:data="data"
:field="field"
:open-on-focus="openOnFocus"
:keep-first="keepFirst"
:clearable="clearable"
:clear-on-select="clearOnSelect"
:formatter="customFormatter"
:placeholder="placeholder"
@update:model-value="buefyValueUpdated"
ref="autocomplete">
</o-autocomplete>
</script>
<script>
const BAutocomplete = {
template: '#b-autocomplete-template',
props: {
modelValue: String,
data: Array,
field: String,
openOnFocus: Boolean,
keepFirst: Boolean,
clearable: Boolean,
clearOnSelect: Boolean,
customFormatter: null,
placeholder: String,
},
data() {
return {
buefyValue: this.modelValue,
}
},
watch: {
modelValue(to, from) {
if (this.buefyValue != to) {
this.buefyValue = to
}
},
},
methods: {
focus() {
const input = this.$refs.autocomplete.$el.querySelector('input')
input.focus()
},
buefyValueUpdated(value) {
this.$emit('update:modelValue', value)
},
},
}
</script>
<% request.register_component('b-autocomplete', 'BAutocomplete') %>
</%def>
<%def name="make_b_button_component()">
<script type="text/x-template" id="b-button-template">
<o-button :variant="variant"
:size="orugaSize"
:native-type="nativeType"
:tag="tag"
:href="href"
:icon-left="iconLeft">
<slot />
</o-button>
</script>
<script>
const BButton = {
template: '#b-button-template',
props: {
type: String,
nativeType: String,
tag: String,
href: String,
size: String,
iconPack: String, // ignored
iconLeft: String,
},
computed: {
orugaSize() {
if (this.size) {
return this.size.replace(/^is-/, '')
}
},
variant() {
if (this.type) {
return this.type.replace(/^is-/, '')
}
},
},
}
</script>
<% request.register_component('b-button', 'BButton') %>
</%def>
<%def name="make_b_checkbox_component()">
<script type="text/x-template" id="b-checkbox-template">
<o-checkbox v-model="orugaValue"
@update:model-value="orugaValueUpdated"
:name="name"
:native-value="nativeValue">
<slot />
</o-checkbox>
</script>
<script>
const BCheckbox = {
template: '#b-checkbox-template',
props: {
modelValue: null,
name: String,
nativeValue: null,
},
data() {
return {
orugaValue: this.modelValue,
}
},
watch: {
modelValue(to, from) {
this.orugaValue = to
},
},
methods: {
orugaValueUpdated(value) {
this.$emit('update:modelValue', value)
},
},
}
</script>
<% request.register_component('b-checkbox', 'BCheckbox') %>
</%def>
<%def name="make_b_collapse_component()">
<script type="text/x-template" id="b-collapse-template">
<o-collapse :open="open">
<slot name="trigger" />
<slot />
</o-collapse>
</script>
<script>
const BCollapse = {
template: '#b-collapse-template',
props: {
open: Boolean,
},
}
</script>
<% request.register_component('b-collapse', 'BCollapse') %>
</%def>
<%def name="make_b_datepicker_component()">
<script type="text/x-template" id="b-datepicker-template">
<o-datepicker :name="name"
v-model="buefyValue"
@update:model-value="buefyValueUpdated"
:value="value"
:placeholder="placeholder"
:date-formatter="dateFormatter"
:date-parser="dateParser"
:disabled="disabled"
:editable="editable"
:icon="icon"
:close-on-click="false">
</o-datepicker>
</script>
<script>
const BDatepicker = {
template: '#b-datepicker-template',
props: {
dateFormatter: null,
dateParser: null,
disabled: Boolean,
editable: Boolean,
icon: String,
// iconPack: String, // ignored
modelValue: Date,
name: String,
placeholder: String,
value: null,
},
data() {
return {
buefyValue: this.modelValue,
}
},
watch: {
modelValue(to, from) {
if (this.buefyValue != to) {
this.buefyValue = to
}
},
},
methods: {
buefyValueUpdated(value) {
if (this.modelValue != value) {
this.$emit('update:modelValue', value)
}
},
},
}
</script>
<% request.register_component('b-datepicker', 'BDatepicker') %>
</%def>
<%def name="make_b_dropdown_component()">
<script type="text/x-template" id="b-dropdown-template">
<o-dropdown :position="buefyPosition"
:triggers="triggers">
<slot name="trigger" />
<slot />
</o-dropdown>
</script>
<script>
const BDropdown = {
template: '#b-dropdown-template',
props: {
position: String,
triggers: Array,
},
computed: {
buefyPosition() {
if (this.position) {
return this.position.replace(/^is-/, '')
}
},
},
}
</script>
<% request.register_component('b-dropdown', 'BDropdown') %>
</%def>
<%def name="make_b_dropdown_item_component()">
<script type="text/x-template" id="b-dropdown-item-template">
<o-dropdown-item :label="label">
<slot />
</o-dropdown-item>
</script>
<script>
const BDropdownItem = {
template: '#b-dropdown-item-template',
props: {
label: String,
},
}
</script>
<% request.register_component('b-dropdown-item', 'BDropdownItem') %>
</%def>
<%def name="make_b_field_component()">
<script type="text/x-template" id="b-field-template">
<o-field :grouped="grouped"
:label="label"
:horizontal="horizontal"
:expanded="expanded"
:variant="variant">
<slot />
</o-field>
</script>
<script>
const BField = {
template: '#b-field-template',
props: {
expanded: Boolean,
grouped: Boolean,
horizontal: Boolean,
label: String,
type: String,
},
computed: {
variant() {
if (this.type) {
return this.type.replace(/^is-/, '')
}
},
},
}
</script>
<% request.register_component('b-field', 'BField') %>
</%def>
<%def name="make_b_icon_component()">
<script type="text/x-template" id="b-icon-template">
<o-icon :icon="icon"
:size="orugaSize" />
</script>
<script>
const BIcon = {
template: '#b-icon-template',
props: {
icon: String,
size: String,
},
computed: {
orugaSize() {
if (this.size) {
return this.size.replace(/^is-/, '')
}
},
},
}
</script>
<% request.register_component('b-icon', 'BIcon') %>
</%def>
<%def name="make_b_input_component()">
<script type="text/x-template" id="b-input-template">
<o-input :type="type"
:disabled="disabled"
v-model="buefyValue"
@update:modelValue="val => $emit('update:modelValue', val)"
autocomplete="off"
ref="input">
<slot />
</o-input>
</script>
<script>
const BInput = {
template: '#b-input-template',
props: {
modelValue: null,
type: String,
disabled: Boolean,
},
data() {
return {
buefyValue: this.modelValue
}
},
watch: {
modelValue(to, from) {
if (this.buefyValue != to) {
this.buefyValue = to
}
},
},
methods: {
focus() {
if (this.type == 'textarea') {
// TODO: this does not work right
this.$refs.input.$el.querySelector('textarea').focus()
}
},
},
}
</script>
<% request.register_component('b-input', 'BInput') %>
</%def>
<%def name="make_b_loading_component()">
<script type="text/x-template" id="b-loading-template">
<o-loading>
<slot />
</o-loading>
</script>
<script>
const BLoading = {
template: '#b-loading-template',
}
</script>
<% request.register_component('b-loading', 'BLoading') %>
</%def>
<%def name="make_b_modal_component()">
<script type="text/x-template" id="b-modal-template">
<o-modal v-model:active="trueActive"
@update:active="activeChanged">
<slot />
</o-modal>
</script>
<script>
const BModal = {
template: '#b-modal-template',
props: {
active: Boolean,
hasModalCard: Boolean, // nb. this is ignored
},
data() {
return {
trueActive: this.active,
}
},
watch: {
active(to, from) {
this.trueActive = to
},
trueActive(to, from) {
if (this.active != to) {
this.tellParent(to)
}
},
},
methods: {
tellParent(active) {
// TODO: this does not work properly
this.$emit('update:active', active)
},
activeChanged(active) {
this.tellParent(active)
},
},
}
</script>
<% request.register_component('b-modal', 'BModal') %>
</%def>
<%def name="make_b_notification_component()">
<script type="text/x-template" id="b-notification-template">
<o-notification :variant="variant"
:closable="closable">
<slot />
</o-notification>
</script>
<script>
const BNotification = {
template: '#b-notification-template',
props: {
type: String,
closable: {
type: Boolean,
default: true,
},
},
computed: {
variant() {
if (this.type) {
return this.type.replace(/^is-/, '')
}
},
},
}
</script>
<% request.register_component('b-notification', 'BNotification') %>
</%def>
<%def name="make_b_select_component()">
<script type="text/x-template" id="b-select-template">
<o-select :name="name"
v-model="orugaValue"
@update:model-value="orugaValueUpdated"
:expanded="expanded"
:multiple="multiple"
:size="orugaSize"
:native-size="nativeSize">
<slot />
</o-select>
</script>
<script>
const BSelect = {
template: '#b-select-template',
props: {
expanded: Boolean,
modelValue: null,
multiple: Boolean,
name: String,
nativeSize: null,
size: null,
},
data() {
return {
orugaValue: this.modelValue,
}
},
watch: {
modelValue(to, from) {
this.orugaValue = to
},
},
computed: {
orugaSize() {
if (this.size) {
return this.size.replace(/^is-/, '')
}
},
},
methods: {
orugaValueUpdated(value) {
this.$emit('update:modelValue', value)
this.$emit('input', value)
},
},
}
</script>
<% request.register_component('b-select', 'BSelect') %>
</%def>
<%def name="make_b_steps_component()">
<script type="text/x-template" id="b-steps-template">
<o-steps v-model="orugaValue"
@update:model-value="orugaValueUpdated"
:animated="animated"
:rounded="rounded"
:has-navigation="hasNavigation"
:vertical="vertical">
<slot />
</o-steps>
</script>
<script>
const BSteps = {
template: '#b-steps-template',
props: {
modelValue: null,
animated: Boolean,
rounded: Boolean,
hasNavigation: Boolean,
vertical: Boolean,
},
data() {
return {
orugaValue: this.modelValue,
}
},
watch: {
modelValue(to, from) {
this.orugaValue = to
},
},
methods: {
orugaValueUpdated(value) {
this.$emit('update:modelValue', value)
this.$emit('input', value)
},
},
}
</script>
<% request.register_component('b-steps', 'BSteps') %>
</%def>
<%def name="make_b_step_item_component()">
<script type="text/x-template" id="b-step-item-template">
<o-step-item :step="step"
:value="value"
:label="label"
:clickable="clickable">
<slot />
</o-step-item>
</script>
<script>
const BStepItem = {
template: '#b-step-item-template',
props: {
step: null,
value: null,
label: String,
clickable: Boolean,
},
}
</script>
<% request.register_component('b-step-item', 'BStepItem') %>
</%def>
<%def name="make_b_table_component()">
<script type="text/x-template" id="b-table-template">
<o-table :data="data">
<slot />
</o-table>
</script>
<script>
const BTable = {
template: '#b-table-template',
props: {
data: Array,
},
}
</script>
<% request.register_component('b-table', 'BTable') %>
</%def>
<%def name="make_b_table_column_component()">
<script type="text/x-template" id="b-table-column-template">
<o-table-column :field="field"
:label="label"
v-slot="props">
## TODO: this does not seem to really work for us...
<slot :props="props" />
</o-table-column>
</script>
<script>
const BTableColumn = {
template: '#b-table-column-template',
props: {
field: String,
label: String,
},
}
</script>
<% request.register_component('b-table-column', 'BTableColumn') %>
</%def>
<%def name="make_once_button_component()">
<script type="text/x-template" id="once-button-template">
<b-button :type="type"
:native-type="nativeType"
:tag="tag"
:href="href"
:title="title"
:disabled="buttonDisabled"
@click="clicked"
icon-pack="fas"
:icon-left="iconLeft">
{{ buttonText }}
</b-button>
</script>
<script>
const OnceButton = {
template: '#once-button-template',
props: {
type: String,
nativeType: String,
tag: String,
href: String,
text: String,
title: String,
iconLeft: String,
working: String,
workingText: String,
disabled: Boolean,
},
data() {
return {
currentText: null,
currentDisabled: null,
}
},
computed: {
buttonText: function() {
return this.currentText || this.text
},
buttonDisabled: function() {
if (this.currentDisabled !== null) {
return this.currentDisabled
}
return this.disabled
},
},
methods: {
clicked(event) {
this.currentDisabled = true
if (this.workingText) {
this.currentText = this.workingText
} else if (this.working) {
this.currentText = this.working + ", please wait..."
} else {
this.currentText = "Working, please wait..."
}
// this.$nextTick(function() {
// this.$emit('click', event)
// })
}
},
}
</script>
<% request.register_component('once-button', 'OnceButton') %>
</%def>

View file

@ -0,0 +1,32 @@
<%def name="make_buefy_plugin()">
<script>
const BuefyPlugin = {
install(app, options) {
app.config.globalProperties.$buefy = {
toast: {
open(options) {
let variant = null
if (options.type) {
variant = options.type.replace(/^is-/, '')
}
const opts = {
duration: options.duration,
message: options.message,
position: 'top',
variant,
}
const oruga = app.config.globalProperties.$oruga
oruga.notification.open(opts)
},
},
}
},
}
</script>
</%def>

View file

@ -0,0 +1,382 @@
## -*- coding: utf-8; -*-
<%def name="make_field_components()">
${self.make_tailbone_autocomplete_component()}
${self.make_tailbone_datepicker_component()}
</%def>
<%def name="make_tailbone_autocomplete_component()">
<% request.register_component('tailbone-autocomplete', 'TailboneAutocomplete') %>
<script type="text/x-template" id="tailbone-autocomplete-template">
<div>
<o-button v-if="modelValue"
style="width: 100%; justify-content: left;"
@click="clearSelection(true)"
expanded>
{{ internalLabel }} (click to change #1)
</o-button>
<o-autocomplete ref="autocompletex"
v-show="!modelValue"
v-model="orugaValue"
:placeholder="placeholder"
:data="filteredData"
:field="field"
:formatter="customFormatter"
@input="inputChanged"
@select="optionSelected"
keep-first
open-on-focus
expanded
:clearable="clearable"
:clear-on-select="clearOnSelect">
<template #default="{ option }">
{{ option.label }}
</template>
</o-autocomplete>
<input type="hidden" :name="name" :value="modelValue" />
</div>
</script>
<script>
const TailboneAutocomplete = {
template: '#tailbone-autocomplete-template',
props: {
// this is the "input" field name essentially. primarily
// is useful for "traditional" tailbone forms; it normally
// is not used otherwise. it is passed as-is to the oruga
// autocomplete component `name` prop
name: String,
// static data set; used when serviceUrl is not provided
data: Array,
// the url from which search results are to be obtained. the
// url should expect a GET request with a query string with a
// single `term` parameter, and return results as a JSON array
// containing objects with `value` and `label` properties.
serviceUrl: String,
// callers do not specify this directly but rather by way of
// the `v-model` directive. this component will emit `input`
// events when the value changes
modelValue: String,
// callers may set an initial label if needed. this is useful
// in cases where the autocomplete needs to "already have a
// value" on page load. for instance when a user fills out
// the autocomplete field, but leaves other required fields
// blank and submits the form; page will re-load showing
// errors but the autocomplete field should remain "set" -
// normally it is only given a "value" (e.g. uuid) but this
// allows for the "label" to display correctly as well
initialLabel: String,
// while the `initialLabel` above is useful for setting the
// *initial* label (of course), it cannot be used to
// arbitrarily update the label during the component's life.
// if you do need to *update* the label after initial page
// load, then you should set `assignedLabel` instead. one
// place this happens is in /custorders/create page, where
// product autocomplete shows some results, and user clicks
// one, but then handler logic can forcibly "swap" the
// selection, causing *different* product data to come back
// from the server, and autocomplete label should be updated
// to match. this feels a bit awkward still but does work..
assignedLabel: String,
// simple placeholder text for the input box
placeholder: String,
// these are passed as-is to <o-autocomplete>
clearable: Boolean,
clearOnSelect: Boolean,
customFormatter: null,
expanded: Boolean,
field: String,
},
data() {
const internalLabel = this.assignedLabel || this.initialLabel
// we want to track the "currently selected option" - which
// should normally be `null` to begin with, unless we were
// given a value, in which case we use `initialLabel` to
// complete the option
let selected = null
if (this.modelValue) {
selected = {
value: this.modelValue,
label: internalLabel,
}
}
return {
// this contains the search results; its contents may
// change over time as new searches happen. the
// "currently selected option" should be one of these,
// unless it is null
fetchedData: [],
// this tracks our "currently selected option" - per above
selected,
// since we are wrapping a component which also makes
// use of the "value" paradigm, we must separate the
// concerns. so we use our own `modelValue` prop to
// interact with the caller, but then we use this
// `orugaValue` data point to communicate with the
// oruga autocomplete component. note that
// `this.modelValue` will always be either a uuid or
// null, whereas `this.orugaValue` may be raw text as
// entered by the user.
// orugaValue: this.modelValue,
orugaValue: null,
// this stores the "internal" label for the button
internalLabel,
}
},
computed: {
filteredData() {
// do not filter if data comes from backend
if (this.serviceUrl) {
return this.fetchedData
}
if (!this.orugaValue || !this.orugaValue.length) {
return this.data
}
const terms = []
for (let term of this.orugaValue.toLowerCase().split(' ')) {
term = term.trim()
if (term) {
terms.push(term)
}
}
if (!terms.length) {
return this.data
}
// all terms must match
return this.data.filter((option) => {
const label = option.label.toLowerCase()
for (const term of terms) {
if (label.indexOf(term) < 0) {
return false
}
}
return true
})
},
},
watch: {
assignedLabel(to, from) {
// update button label when caller changes it
this.internalLabel = to
},
},
methods: {
inputChanged(entry) {
if (this.serviceUrl) {
this.getAsyncData(entry)
}
},
// fetch new search results from the server. this is
// invoked via the `@input` event from oruga autocomplete
// component.
getAsyncData(entry) {
// since the `@input` event from oruga component does
// not "self-regulate" in any way (?), we skip the
// search unless we have at least 3 characters of
// input from user
if (entry.length < 3) {
this.fetchedData = []
return
}
// and perform the search
this.$http.get(this.serviceUrl + '?term=' + encodeURIComponent(entry))
.then(({ data }) => {
this.fetchedData = data
})
.catch((error) => {
this.fetchedData = []
throw error
})
},
// this method is invoked via the `@select` event of the
// oruga autocomplete component. the `option` received
// will be one of:
// - object with (at least) `value` and `label` keys
// - simple string (e.g. when data set is static)
// - null
optionSelected(option) {
this.selected = option
this.internalLabel = option?.label || option
// reset the internal value for oruga autocomplete
// component. note that this value will normally hold
// either the raw text entered by the user, or a uuid.
// we will not be needing either of those b/c they are
// not visible to user once selection is made, and if
// the selection is cleared we want user to start over
// anyway
this.orugaValue = null
// here is where we alert callers to the new value
if (option) {
this.$emit('newLabel', option.label)
}
const value = option?.[this.field || 'value'] || option
this.$emit('update:modelValue', value)
// this.$emit('select', option)
// this.$emit('input', value)
},
## // set selection to the given option, which should a simple
## // object with (at least) `value` and `label` properties
## setSelection(option) {
## this.$refs.autocomplete.setSelected(option)
## },
// clear the field of any value, i.e. set the "currently
// selected option" to null. this is invoked when you click
// the button, which is visible while the field has a value.
// but callers can invoke it directly as well.
clearSelection(focus) {
this.$emit('update:modelValue', null)
this.$emit('input', null)
this.$emit('newLabel', null)
this.internalLabel = null
this.selected = null
this.orugaValue = null
## // clear selection for the oruga autocomplete component
## this.$refs.autocomplete.setSelected(null)
// maybe set focus to our (autocomplete) component
if (focus) {
this.$nextTick(function() {
this.focus()
})
}
},
// set focus to this component, which will just set focus
// to the oruga autocomplete component
focus() {
// TODO: why is this ref null?!
if (this.$refs.autocompletex) {
this.$refs.autocompletex.focus()
}
},
// returns the "raw" user input from the underlying oruga
// autocomplete component
getUserInput() {
return this.orugaValue
},
},
}
</script>
</%def>
<%def name="make_tailbone_datepicker_component()">
<% request.register_component('tailbone-datepicker', 'TailboneDatepicker') %>
<script type="text/x-template" id="tailbone-datepicker-template">
<o-datepicker placeholder="Click to select ..."
icon="calendar-alt"
:date-formatter="formatDate"
:date-parser="parseDate"
v-model="orugaValue"
@update:model-value="orugaValueUpdated"
:disabled="disabled"
ref="trueDatePicker">
</o-datepicker>
</script>
<script>
const TailboneDatepicker = {
template: '#tailbone-datepicker-template',
props: {
modelValue: Date,
disabled: Boolean,
},
data() {
return {
orugaValue: this.parseDate(this.modelValue),
}
},
watch: {
modelValue(to, from) {
if (this.orugaValue != to) {
this.orugaValue = to
}
},
},
methods: {
formatDate(date) {
if (date === null) {
return null
}
// just need to convert to simple ISO date format here, seems
// like there should be a more obvious way to do that?
var year = date.getFullYear()
var month = date.getMonth() + 1
var day = date.getDate()
month = month < 10 ? '0' + month : month
day = day < 10 ? '0' + day : day
return year + '-' + month + '-' + day
},
parseDate(value) {
if (typeof(value) == 'object') {
// nb. we are assuming it is a Date here
return value
}
if (value) {
// note, this assumes classic YYYY-MM-DD (i.e. ISO?) format
const parts = value.split('-')
return new Date(parts[0], parseInt(parts[1]) - 1, parts[2])
}
return null
},
orugaValueUpdated(date) {
this.$emit('update:modelValue', date)
},
focus() {
this.$refs.trueDatePicker.focus()
},
},
}
</script>
</%def>

View file

@ -0,0 +1,100 @@
<%def name="make_http_plugin()">
<script>
const HttpPlugin = {
install(app, options) {
app.config.globalProperties.$http = {
get(url, options) {
if (options === undefined) {
options = {}
}
if (options.params) {
// convert params to query string
const data = new URLSearchParams()
for (let [key, value] of Object.entries(options.params)) {
// nb. all values get converted to string here, so
// fallback to empty string to avoid null value
// from being interpreted as "null" string
if (value === null) {
value = ''
}
data.append(key, value)
}
// TODO: this should be smarter in case query string already exists
url += '?' + data.toString()
// params is not a valid arg for options to fetch()
delete options.params
}
return new Promise((resolve, reject) => {
fetch(url, options).then(response => {
// original response does not contain 'data'
// attribute, so must use a "mock" response
// which does contain everything
response.json().then(json => {
resolve({
data: json,
headers: response.headers,
ok: response.ok,
redirected: response.redirected,
status: response.status,
statusText: response.statusText,
type: response.type,
url: response.url,
})
}, json => {
reject(response)
})
}, response => {
reject(response)
})
})
},
post(url, params, options) {
if (params) {
// attach params as json
options.body = JSON.stringify(params)
// and declare content-type
options.headers = new Headers(options.headers)
options.headers.append('Content-Type', 'application/json')
}
options.method = 'POST'
return new Promise((resolve, reject) => {
fetch(url, options).then(response => {
// original response does not contain 'data'
// attribute, so must use a "mock" response
// which does contain everything
response.json().then(json => {
resolve({
data: json,
headers: response.headers,
ok: response.ok,
redirected: response.redirected,
status: response.status,
statusText: response.statusText,
type: response.type,
url: response.url,
})
}, json => {
reject(response)
})
}, response => {
reject(response)
})
})
},
}
},
}
</script>
</%def>

View file

@ -0,0 +1,244 @@
## -*- coding: utf-8; -*-
<%namespace name="base_meta" file="/base_meta.mako" />
<%namespace file="/base.mako" import="core_javascript" />
<%namespace file="/base.mako" import="core_styles" />
<%namespace file="/http-plugin.mako" import="make_http_plugin" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
${base_meta.favicon()}
<title>${initial_msg or "Working"}...</title>
${core_javascript()}
${core_styles()}
${self.extra_styles()}
</head>
<body>
<div id="app" style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
<whole-page></whole-page>
</div>
${make_http_plugin()}
${self.make_whole_page_component()}
${self.modify_whole_page_vars()}
${self.make_whole_page_app()}
</body>
</html>
<%def name="extra_styles()"></%def>
<%def name="make_whole_page_component()">
<script type="text/x-template" id="whole-page-template">
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container">
<div style="display: flex; flex-direction: column; justify-content: center;">
<div style="margin: auto; display: flex; gap: 1rem; align-items: end;">
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div style="display: flex; gap: 3rem;">
<span>{{ progressMessage }} ... {{ totalDisplay }}</span>
<span>{{ percentageDisplay }}</span>
</div>
<div style="display: flex; gap: 1rem; align-items: center;">
<div>
<progress class="progress is-large"
style="width: 400px;"
:max="progressMax"
:value="progressValue" />
</div>
% if can_cancel:
<o-button v-show="canCancel"
@click="cancelProgress()"
:disabled="cancelingProgress"
icon-left="ban">
{{ cancelingProgress ? "Canceling, please wait..." : "Cancel" }}
</o-button>
% endif
</div>
</div>
</div>
</div>
${self.after_progress()}
</div>
</div>
</section>
</script>
<script>
const WholePage = {
template: '#whole-page-template',
computed: {
percentageDisplay() {
if (!this.progressMax) {
return
}
const percent = this.progressValue / this.progressMax
return percent.toLocaleString(undefined, {
style: 'percent',
minimumFractionDigits: 0})
},
totalDisplay() {
% if can_cancel:
if (!this.stillInProgress && !this.cancelingProgress) {
return "done!"
}
% else:
if (!this.stillInProgress) {
return "done!"
}
% endif
if (this.progressMaxDisplay) {
return `(${'$'}{this.progressMaxDisplay} total)`
}
},
},
mounted() {
// fetch first progress data, one second from now
setTimeout(() => {
this.updateProgress()
}, 1000)
// custom logic if applicable
this.mountedCustom()
},
methods: {
mountedCustom() {},
updateProgress() {
this.$http.get(this.progressURL).then(response => {
if (response.data.error) {
// errors stop the show, we redirect to "cancel" page
location.href = '${cancel_url}'
} else {
if (response.data.complete || response.data.maximum) {
this.progressMessage = response.data.message
this.progressMaxDisplay = response.data.maximum_display
if (response.data.complete) {
this.progressValue = this.progressMax
this.stillInProgress = false
% if can_cancel:
this.canCancel = false
% endif
location.href = response.data.success_url
} else {
this.progressValue = response.data.value
this.progressMax = response.data.maximum
}
}
// custom logic if applicable
this.updateProgressCustom(response)
if (this.stillInProgress) {
// fetch progress data again, in one second from now
setTimeout(() => {
this.updateProgress()
}, 1000)
}
}
})
},
updateProgressCustom(response) {},
% if can_cancel:
cancelProgress() {
if (confirm("Do you really wish to cancel this operation?")) {
this.cancelingProgress = true
this.stillInProgress = false
let params = {cancel_msg: ${json.dumps(cancel_msg)|n}}
this.$http.get(this.cancelURL, {params: params}).then(response => {
location.href = ${json.dumps(cancel_url)|n}
})
}
},
% endif
}
}
const WholePageData = {
progressURL: '${url('progress', key=progress.key, _query={'sessiontype': progress.session.type})}',
progressMessage: "${(initial_msg or "Working").replace('"', '\\"')} (please wait)",
progressMax: null,
progressMaxDisplay: null,
progressValue: null,
stillInProgress: true,
% if can_cancel:
canCancel: true,
cancelURL: '${url('progress.cancel', key=progress.key, _query={'sessiontype': progress.session.type})}',
cancelingProgress: false,
% endif
}
</script>
</%def>
<%def name="after_progress()"></%def>
<%def name="modify_whole_page_vars()"></%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)
WholePage.data = () => { return WholePageData }
app.component('whole-page', WholePage)
app.use(Oruga, {
...bulmaConfig,
iconComponent: 'vue-fontawesome',
iconPack: 'fas',
})
app.use(HttpPlugin)
app.mount('#app')
</script>
</%def>

View file

@ -7,31 +7,35 @@
<h3 class="is-size-3">Upgradable Systems</h3> <h3 class="is-size-3">Upgradable Systems</h3>
<div class="block" style="padding-left: 2rem; display: flex;"> <div class="block" style="padding-left: 2rem; display: flex;">
<b-table :data="upgradeSystems" <${b}-table :data="upgradeSystems"
sortable> sortable>
<b-table-column field="key" <${b}-table-column field="key"
label="Key" label="Key"
v-slot="props" v-slot="props"
sortable> sortable>
{{ props.row.key }} {{ props.row.key }}
</b-table-column> </${b}-table-column>
<b-table-column field="label" <${b}-table-column field="label"
label="Label" label="Label"
v-slot="props" v-slot="props"
sortable> sortable>
{{ props.row.label }} {{ props.row.label }}
</b-table-column> </${b}-table-column>
<b-table-column field="command" <${b}-table-column field="command"
label="Command" label="Command"
v-slot="props" v-slot="props"
sortable> sortable>
{{ props.row.command }} {{ props.row.command }}
</b-table-column> </${b}-table-column>
<b-table-column label="Actions" <${b}-table-column label="Actions"
v-slot="props"> v-slot="props">
<a href="#" <a href="#"
@click.prevent="upgradeSystemEdit(props.row)"> @click.prevent="upgradeSystemEdit(props.row)">
% if request.use_oruga:
<o-icon icon="edit" />
% else:
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
% endif
Edit Edit
</a> </a>
&nbsp; &nbsp;
@ -39,11 +43,15 @@
v-if="props.row.key != 'rattail'" v-if="props.row.key != 'rattail'"
class="has-text-danger" class="has-text-danger"
@click.prevent="updateSystemDelete(props.row)"> @click.prevent="updateSystemDelete(props.row)">
% if request.use_oruga:
<o-icon icon="trash" />
% else:
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
% endif
Delete Delete
</a> </a>
</b-table-column> </${b}-table-column>
</b-table> </${b}-table>
<div style="margin-left: 1rem;"> <div style="margin-left: 1rem;">
<b-button type="is-primary" <b-button type="is-primary"

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar # Copyright © 2010-2024 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -31,7 +31,6 @@ import warnings
import humanize import humanize
import markdown import markdown
from rattail.time import timezone, make_utc
from rattail.files import resource_path from rattail.files import resource_path
import colander import colander
@ -161,6 +160,25 @@ def get_libver(request, key, fallback=True, default_only=False):
elif key == 'fontawesome': elif key == 'fontawesome':
return '5.3.1' return '5.3.1'
elif key == 'bb_vue':
# TODO: iiuc vue 3.4 does not work with oruga yet
return '3.3.11'
elif key == 'bb_oruga':
return '0.8.8'
elif key in ('bb_oruga_bulma', 'bb_oruga_bulma_css'):
return '0.3.0'
elif key == 'bb_fontawesome_svg_core':
return '6.5.2'
elif key == 'bb_free_solid_svg_icons':
return '6.5.2'
elif key == 'bb_vue_fontawesome':
return '3.0.6'
def get_liburl(request, key, fallback=True): def get_liburl(request, key, fallback=True):
""" """
@ -192,6 +210,27 @@ def get_liburl(request, key, fallback=True):
elif key == 'fontawesome': elif key == 'fontawesome':
return 'https://use.fontawesome.com/releases/v{}/js/all.js'.format(version) return 'https://use.fontawesome.com/releases/v{}/js/all.js'.format(version)
elif key == 'bb_vue':
return f'https://unpkg.com/vue@{version}/dist/vue.esm-browser.js'
elif key == 'bb_oruga':
return f'https://unpkg.com/@oruga-ui/oruga-next@{version}/dist/esm/index.mjs'
elif key == 'bb_oruga_bulma':
return f'https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.mjs'
elif key == 'bb_oruga_bulma_css':
return f'https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.css'
elif key == 'bb_fontawesome_svg_core':
return f'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-svg-core@{version}/+esm'
elif key == 'bb_free_solid_svg_icons':
return f'https://cdn.jsdelivr.net/npm/@fortawesome/free-solid-svg-icons@{version}/+esm'
elif key == 'bb_vue_fontawesome':
return f'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm'
def pretty_datetime(config, value): def pretty_datetime(config, value):
""" """
@ -214,10 +253,10 @@ def pretty_datetime(config, value):
value = app.make_utc(value, tzinfo=True) value = app.make_utc(value, tzinfo=True)
# Calculate time diff using UTC. # Calculate time diff using UTC.
time_ago = datetime.datetime.utcnow() - make_utc(value) time_ago = datetime.datetime.utcnow() - app.make_utc(value)
# Convert value to local timezone. # Convert value to local timezone.
local = timezone(config) local = app.get_timezone()
value = local.normalize(value.astimezone(local)) value = local.normalize(value.astimezone(local))
return HTML.tag('span', return HTML.tag('span',
@ -246,10 +285,10 @@ def raw_datetime(config, value, verbose=False, as_date=False):
value = app.make_utc(value, tzinfo=True) value = app.make_utc(value, tzinfo=True)
# Calculate time diff using UTC. # Calculate time diff using UTC.
time_ago = datetime.datetime.utcnow() - make_utc(value) time_ago = datetime.datetime.utcnow() - app.make_utc(value)
# Convert value to local timezone. # Convert value to local timezone.
local = timezone(config) local = app.get_timezone()
value = local.normalize(value.astimezone(local)) value = local.normalize(value.astimezone(local))
kwargs = {} kwargs = {}
@ -378,6 +417,18 @@ def get_effective_theme(rattail_config, theme=None, session=None):
return theme return theme
def should_use_oruga(request):
"""
Returns a flag indicating whether or not the current theme
supports (and therefore should use) Oruga + Vue 3 as opposed to
the default of Buefy + Vue 2.
"""
theme = request.registry.settings['tailbone.theme']
if theme == 'butterball':
return True
return False
def validate_email_address(address): def validate_email_address(address):
""" """
Perform basic validation on the given email address. This leverages the Perform basic validation on the given email address. This leverages the

View file

@ -107,6 +107,13 @@ class AppInfoView(MasterView):
('buefy', "Buefy"), ('buefy', "Buefy"),
('buefy.css', "Buefy CSS"), ('buefy.css', "Buefy CSS"),
('fontawesome', "FontAwesome"), ('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"),
]) ])
for key in weblibs: for key in weblibs:
@ -181,6 +188,41 @@ class AppInfoView(MasterView):
{'section': 'tailbone', {'section': 'tailbone',
'option': 'liburl.fontawesome'}, 'option': 'liburl.fontawesome'},
{'section': 'tailbone',
'option': 'libver.bb_vue'},
{'section': 'tailbone',
'option': 'liburl.bb_vue'},
{'section': 'tailbone',
'option': 'libver.bb_oruga'},
{'section': 'tailbone',
'option': 'liburl.bb_oruga'},
{'section': 'tailbone',
'option': 'libver.bb_oruga_bulma'},
{'section': 'tailbone',
'option': 'liburl.bb_oruga_bulma'},
{'section': 'tailbone',
'option': 'libver.bb_oruga_bulma_css'},
{'section': 'tailbone',
'option': 'liburl.bb_oruga_bulma_css'},
{'section': 'tailbone',
'option': 'libver.bb_fontawesome_svg_core'},
{'section': 'tailbone',
'option': 'liburl.bb_fontawesome_svg_core'},
{'section': 'tailbone',
'option': 'libver.bb_free_solid_svg_icons'},
{'section': 'tailbone',
'option': 'liburl.bb_free_solid_svg_icons'},
{'section': 'tailbone',
'option': 'libver.bb_vue_fontawesome'},
{'section': 'tailbone',
'option': 'liburl.bb_vue_fontawesome'},
# 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', {'section': 'tailbone',