diff --git a/tailbone/static/js/tailbone.buefy.message_recipients.js b/tailbone/static/js/tailbone.buefy.message_recipients.js
new file mode 100644
index 00000000..161ad404
--- /dev/null
+++ b/tailbone/static/js/tailbone.buefy.message_recipients.js
@@ -0,0 +1,108 @@
+
+const MessageRecipients = {
+ template: '#message-recipients-template',
+
+ props: {
+ name: String,
+ value: Array,
+ possibleRecipients: Array,
+ recipientDisplayMap: Object,
+ },
+
+ data() {
+ return {
+ autocompleteValue: null,
+ actualValue: this.value,
+ }
+ },
+
+ computed: {
+
+ filteredData() {
+ // this is the logic responsible for "matching" user's autocomplete
+ // input, with possible recipients. we return all matches as list.
+ let filtered = []
+ if (this.autocompleteValue) {
+ let term = this.autocompleteValue.toLowerCase()
+ this.possibleRecipients.forEach(function(value, key, map) {
+
+ // first check to see if value is simple string, if so then
+ // will attempt to match it directly
+ if (value.toLowerCase !== undefined) {
+ if (value.toLowerCase().indexOf(term) >= 0) {
+ filtered.push({value: key, label: value})
+ }
+
+ } else {
+ // value is not a string, which means it must be a
+ // grouping object, which must have a name property
+ if (value.name.toLowerCase().indexOf(term) >= 0) {
+ filtered.push({
+ value: key,
+ label: value.name,
+ moreValues: value.uuids,
+ })
+ }
+ }
+ })
+ }
+ return filtered
+ },
+ },
+
+ methods: {
+
+ addRecipient(uuid) {
+
+ // add selected user to "actual" value
+ if (!this.actualValue.includes(uuid)) {
+ this.actualValue.push(uuid)
+ }
+ },
+
+ removeRecipient(uuid) {
+
+ // locate and remove user uuid from "actual" value
+ for (let i = 0; i < this.actualValue.length; i++) {
+ if (this.actualValue[i] == uuid) {
+ this.actualValue.splice(i, 1)
+ break
+ }
+ }
+ },
+
+ selectionMade(option) {
+
+ // apparently option can be null sometimes..?
+ if (option) {
+
+ // add all newly-selected users to "actual" value
+ if (option.moreValues) {
+ // grouping object; add all its "contained" values
+ option.moreValues.forEach(function(uuid) {
+ this.addRecipient(uuid)
+ }, this)
+ } else {
+ // normal object, just add its value
+ this.addRecipient(option.value)
+ }
+
+ // let parent know we changed value
+ this.$emit('input', this.actualValue)
+ }
+
+ // clear out the *visible* autocomplete value
+ this.$nextTick(function() {
+ this.autocompleteValue = null
+
+ // TODO: wtf, sometimes we have to clear this out twice?!
+ this.$nextTick(function() {
+ this.autocompleteValue = null
+ })
+ })
+ },
+ },
+}
+
+
+Vue.component('message-recipients', MessageRecipients)
diff --git a/tailbone/templates/deform/message_recipients_buefy.pt b/tailbone/templates/deform/message_recipients_buefy.pt
new file mode 100644
index 00000000..1ea5e153
--- /dev/null
+++ b/tailbone/templates/deform/message_recipients_buefy.pt
@@ -0,0 +1,13 @@
+
diff --git a/tailbone/templates/deform/textinput.pt b/tailbone/templates/deform/textinput.pt
index 48d4c360..2e1c32ef 100644
--- a/tailbone/templates/deform/textinput.pt
+++ b/tailbone/templates/deform/textinput.pt
@@ -4,13 +4,16 @@
mask mask|field.widget.mask;
mask_placeholder mask_placeholder|field.widget.mask_placeholder;
style style|field.widget.style;
- use_buefy use_buefy|0;"
+ use_buefy use_buefy|0;
+ placeholder placeholder|getattr(field.widget, 'placeholder', '');
+ autocomplete autocomplete|getattr(field.widget, 'autocomplete', 'on');"
tal:omit-tag="">
${self.validate_message_js()}
+ % endif
%def>
<%def name="validate_message_js()">
@@ -95,6 +102,19 @@
<%def name="extra_styles()">
${parent.extra_styles()}
+ % if use_buefy:
+
+ % else:
${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.tagit.css'))}
+ % endif
%def>
<%def name="before_tag_added()">
@@ -133,4 +154,20 @@
% endif
%def>
+<%def name="render_this_page_template()">
+ ${parent.render_this_page_template()}
+ ${message_recipients_template()}
+%def>
+
+<%def name="modify_this_page_vars()">
+ ${parent.modify_this_page_vars()}
+
+%def>
+
+
${parent.body()}
diff --git a/tailbone/templates/messages/recipients.mako b/tailbone/templates/messages/recipients.mako
new file mode 100644
index 00000000..c16ddaf2
--- /dev/null
+++ b/tailbone/templates/messages/recipients.mako
@@ -0,0 +1,36 @@
+## -*- coding: utf-8; -*-
+
+<%def name="message_recipients_template()">
+
+%def>
diff --git a/tailbone/templates/messages/view.mako b/tailbone/templates/messages/view.mako
index a7bf449c..78caab93 100644
--- a/tailbone/templates/messages/view.mako
+++ b/tailbone/templates/messages/view.mako
@@ -34,6 +34,7 @@
}
.tailbone-message-body {
margin: 1rem auto;
+ min-height: 10rem;
}
.tailbone-message-body p {
margin-bottom: 1rem;
diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako
index 01395ea7..b3ca7d54 100644
--- a/tailbone/templates/themes/falafel/base.mako
+++ b/tailbone/templates/themes/falafel/base.mako
@@ -246,7 +246,7 @@
% if master:
% if master.listing:
${index_title}
- % else:
+ % elif index_url:
${h.link_to(index_title, index_url)}
% if parent_url is not Undefined:
»
@@ -258,6 +258,8 @@
% if master.viewing and grid_index:
${grid_index_nav()}
% endif
+ % else:
+ ${index_title}
% endif
% elif index_title:
${index_title}
diff --git a/tailbone/views/messages.py b/tailbone/views/messages.py
index 0d356d70..5f814569 100644
--- a/tailbone/views/messages.py
+++ b/tailbone/views/messages.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2018 Lance Edgar
+# Copyright © 2010-2019 Lance Edgar
#
# This file is part of Rattail.
#
@@ -36,6 +36,7 @@ from deform import widget as dfwidget
from pyramid import httpexceptions
from webhelpers2.html import tags, HTML
+# from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.util import raw_datetime
@@ -140,6 +141,8 @@ class MessagesView(MasterView):
return six.text_type(sender)
def render_subject_bold(self, message, field):
+ if not message.subject:
+ return ""
return HTML.tag('span', c=message.subject, style='font-weight: bold;')
def render_recipients(self, message, column_name):
@@ -212,9 +215,12 @@ class MessagesView(MasterView):
super(MessagesView, self).configure_form(f)
use_buefy = self.get_use_buefy()
- # we have custom logic to disable submit button
- f.auto_disable = False
- f.auto_disable_save = False
+ f.submit_label = "Send Message"
+
+ if not use_buefy:
+ # we have custom logic to disable submit button
+ f.auto_disable = False
+ f.auto_disable_save = False
# TODO: A fair amount of this still seems hacky...
@@ -223,22 +229,34 @@ class MessagesView(MasterView):
f.set_type('sent', 'datetime')
+ # recipients
f.set_renderer('recipients', self.render_recipients_full)
f.set_label('recipients', "To")
+ # subject
if use_buefy:
f.set_renderer('subject', self.render_subject_bold)
+ if self.creating:
+ f.set_widget('subject', dfwidget.TextInputWidget(
+ placeholder="please enter a subject",
+ autocomplete='off'))
+ f.set_required('subject')
+ # body
f.set_widget('body', dfwidget.TextAreaWidget(cols=50, rows=15))
if self.creating:
f.remove('sender', 'sent')
- f.insert_after('recipients', 'recipients_')
+ # recipients
+ f.insert_after('recipients', 'set_recipients')
f.remove('recipients')
- f.set_node('recipients_', colander.SchemaNode(colander.Set()))
- f.set_widget('recipients_', RecipientsWidget())
- f.set_label('recipients_', "To")
+ f.set_node('set_recipients', colander.SchemaNode(colander.Set()))
+ if use_buefy:
+ f.set_widget('set_recipients', RecipientsWidgetBuefy())
+ else:
+ f.set_widget('set_recipients', RecipientsWidget())
+ f.set_label('set_recipients', "To")
if self.replying:
old_message = self.get_instance()
@@ -259,11 +277,11 @@ class MessagesView(MasterView):
value = [r[0] for r in value]
if old_message.sender is not self.request.user and old_message.sender.active:
value.insert(0, old_message.sender_uuid)
- f.set_default('recipients_', ','.join(value))
+ f.set_default('set_recipients', ','.join(value))
# Just a normal reply, to sender only.
elif self.filter_reply_recipient(old_message.sender):
- f.set_default('recipients_', old_message.sender.uuid)
+ f.set_default('set_recipients', old_message.sender.uuid)
# TODO?
# # Set focus to message body instead of recipients, when replying.
@@ -281,7 +299,7 @@ class MessagesView(MasterView):
if self.request.user:
message.sender = self.request.user
- for uuid in data['recipients_']:
+ for uuid in data['set_recipients']:
user = self.Session.query(model.User).get(uuid)
if user:
message.add_recipient(user, status=self.enum.MESSAGE_STATUS_INBOX)
@@ -322,11 +340,21 @@ class MessagesView(MasterView):
return recipient
def template_kwargs_create(self, **kwargs):
- recips = list(self.get_available_recipients().items())
+ use_buefy = self.get_use_buefy()
+
+ recips = self.get_available_recipients()
+ if use_buefy:
+ kwargs['recipient_display_map'] = recips
+ recips = list(recips.items())
recips.sort(key=self.recipient_sortkey)
kwargs['available_recipients'] = recips
+
if self.replying:
kwargs['original_message'] = self.get_instance()
+
+ if use_buefy:
+ kwargs['index_url'] = None
+ kwargs['index_title'] = "New Message"
return kwargs
def recipient_sortkey(self, recip):
@@ -355,7 +383,7 @@ class MessagesView(MasterView):
kwargs['message'] = message
kwargs['recipient'] = recipient
- if recipient.status == self.enum.MESSAGE_STATUS_ARCHIVE:
+ if recipient and recipient.status == self.enum.MESSAGE_STATUS_ARCHIVE:
kwargs['index_url'] = self.request.route_url('messages.archive')
return kwargs
@@ -514,6 +542,30 @@ class RecipientsWidget(dfwidget.TextInputWidget):
return pstruct.split(',')
+class RecipientsWidgetBuefy(dfwidget.Widget):
+ """
+ Custom "message recipients" widget, for use with Buefy / Vue.js themes.
+ """
+ template = 'message_recipients_buefy'
+
+ def deserialize(self, field, pstruct):
+ if pstruct is colander.null:
+ return colander.null
+ if not isinstance(pstruct, six.string_types):
+ raise colander.Invalid(field.schema, "Pstruct is not a string")
+ if not pstruct:
+ return colander.null
+ pstruct = pstruct.split(',')
+ return pstruct
+
+ def serialize(self, field, cstruct, **kw):
+ if cstruct in (colander.null, None):
+ cstruct = ""
+ template = self.template
+ values = self.get_template_values(field, cstruct, kw)
+ return field.renderer(template, **values)
+
+
def includeme(config):
config.add_tailbone_permission('messages', 'messages.list', "List/Search Messages")