From 86695c9dc782623ba71645dba237833d1afa84a4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 4 Nov 2019 17:30:00 -0600 Subject: [PATCH] Refactor "send new message" form, esp. recipients field, per Vue.js --- .../js/tailbone.buefy.message_recipients.js | 108 ++++++++++++++++++ .../deform/message_recipients_buefy.pt | 13 +++ tailbone/templates/deform/textinput.pt | 9 +- tailbone/templates/messages/create.mako | 45 +++++++- tailbone/templates/messages/recipients.mako | 36 ++++++ tailbone/templates/messages/view.mako | 1 + tailbone/templates/themes/falafel/base.mako | 4 +- tailbone/views/messages.py | 78 ++++++++++--- 8 files changed, 274 insertions(+), 20 deletions(-) create mode 100644 tailbone/static/js/tailbone.buefy.message_recipients.js create mode 100644 tailbone/templates/deform/message_recipients_buefy.pt create mode 100644 tailbone/templates/messages/recipients.mako 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 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 name="before_tag_added()"> @@ -133,4 +154,20 @@ % endif +<%def name="render_this_page_template()"> + ${parent.render_this_page_template()} + ${message_recipients_template()} + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + ${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()"> + + 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")