Refactor "send new message" form, esp. recipients field, per Vue.js
This commit is contained in:
parent
e153e530a8
commit
86695c9dc7
108
tailbone/static/js/tailbone.buefy.message_recipients.js
Normal file
108
tailbone/static/js/tailbone.buefy.message_recipients.js
Normal file
|
@ -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)
|
13
tailbone/templates/deform/message_recipients_buefy.pt
Normal file
13
tailbone/templates/deform/message_recipients_buefy.pt
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div tal:define="name name|field.name;
|
||||
possible_recipients possible_recipients|'possibleRecipients';
|
||||
recipient_display_map recipient_display_map|'recipientDisplayMap';"
|
||||
tal:omit-tag="">
|
||||
<div tal:define="vmodel vmodel|'field_model_' + name;"
|
||||
tal:omit-tag="">
|
||||
<message-recipients name="${name}"
|
||||
v-model="${vmodel}"
|
||||
tal:attributes=":possible-recipients possible_recipients;
|
||||
:recipient-display-map recipient_display_map;">
|
||||
</message-recipients>
|
||||
</div>
|
||||
</div>
|
|
@ -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="">
|
||||
<div tal:condition="not use_buefy" tal:omit-tag="">
|
||||
<input type="text" name="${name}" value="${cstruct}"
|
||||
tal:attributes="class string: form-control ${css_class or ''};
|
||||
style style;
|
||||
attributes|field.widget.attributes|{};"
|
||||
autocomplete="${autocomplete}"
|
||||
id="${oid}"/>
|
||||
<script tal:condition="mask" type="text/javascript">
|
||||
deform.addCallback(
|
||||
|
@ -26,7 +29,9 @@
|
|||
tal:define="vmodel vmodel|'field_model_' + name;"
|
||||
tal:omit-tag="">
|
||||
<b-input name="${name}"
|
||||
v-model="${vmodel}">
|
||||
v-model="${vmodel}"
|
||||
placeholder="${placeholder}"
|
||||
autocomplete="${autocomplete}">
|
||||
</b-input>
|
||||
</div>
|
||||
</span>
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/create.mako" />
|
||||
<%namespace file="/messages/recipients.mako" import="message_recipients_template" />
|
||||
|
||||
<%def name="content_title()">${parent.content_title() if not use_buefy else ''}</%def>
|
||||
|
||||
<%def name="extra_javascript()">
|
||||
${parent.extra_javascript()}
|
||||
% if use_buefy:
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.message_recipients.js'))}
|
||||
% else:
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/lib/tag-it.min.js'))}
|
||||
<script type="text/javascript">
|
||||
|
||||
|
@ -18,9 +24,9 @@
|
|||
function validate_message_form() {
|
||||
var form = $('#deform');
|
||||
|
||||
if (! form.find('input[name="recipients_"]').val()) {
|
||||
if (! form.find('input[name="set_recipients"]').val()) {
|
||||
alert("You must specify some recipient(s) for the message.");
|
||||
$('.recipients_ input').data('ui-tagit').tagInput.focus();
|
||||
$('.set_recipients input').data('ui-tagit').tagInput.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -35,7 +41,7 @@
|
|||
|
||||
$(function() {
|
||||
|
||||
var recipients = $('.recipients_ input');
|
||||
var recipients = $('.set_recipients input');
|
||||
|
||||
recipients.tagit({
|
||||
|
||||
|
@ -81,6 +87,7 @@
|
|||
|
||||
</script>
|
||||
${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:
|
||||
<style type="text/css">
|
||||
|
||||
.this-page-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.this-page-content .buttons {
|
||||
margin-left: 20rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
% else:
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.tagit.css'))}
|
||||
<style type="text/css">
|
||||
|
||||
|
@ -111,6 +131,7 @@
|
|||
}
|
||||
|
||||
</style>
|
||||
% 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()}
|
||||
<script type="text/javascript">
|
||||
|
||||
TailboneFormData.possibleRecipients = new Map(${json.dumps(available_recipients)|n})
|
||||
TailboneFormData.recipientDisplayMap = ${json.dumps(recipient_display_map)|n}
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
||||
|
|
36
tailbone/templates/messages/recipients.mako
Normal file
36
tailbone/templates/messages/recipients.mako
Normal file
|
@ -0,0 +1,36 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
|
||||
<%def name="message_recipients_template()">
|
||||
<script type="text/x-template" id="message-recipients-template">
|
||||
<div>
|
||||
|
||||
<input type="hidden" :name="name" v-model="actualValue" />
|
||||
|
||||
<b-field grouped group-multiline>
|
||||
<div v-for="uuid in actualValue"
|
||||
:key="uuid"
|
||||
class="control">
|
||||
<b-tag type="is-primary"
|
||||
attached
|
||||
aria-close-label="Remove recipient"
|
||||
closable
|
||||
@close="removeRecipient(uuid)">
|
||||
{{ recipientDisplayMap[uuid] }}
|
||||
</b-tag>
|
||||
</div>
|
||||
</b-field>
|
||||
|
||||
<b-autocomplete v-model="autocompleteValue"
|
||||
placeholder="add recipient"
|
||||
:data="filteredData"
|
||||
field="uuid"
|
||||
@select="selectionMade"
|
||||
keep-first>
|
||||
<template slot-scope="props">
|
||||
{{ props.option.label }}
|
||||
</template>
|
||||
<template slot="empty">No results found</template>
|
||||
</b-autocomplete>
|
||||
</div>
|
||||
</script>
|
||||
</%def>
|
|
@ -34,6 +34,7 @@
|
|||
}
|
||||
.tailbone-message-body {
|
||||
margin: 1rem auto;
|
||||
min-height: 10rem;
|
||||
}
|
||||
.tailbone-message-body p {
|
||||
margin-bottom: 1rem;
|
||||
|
|
|
@ -246,7 +246,7 @@
|
|||
% if master:
|
||||
% if master.listing:
|
||||
<span>${index_title}</span>
|
||||
% else:
|
||||
% elif index_url:
|
||||
${h.link_to(index_title, index_url)}
|
||||
% if parent_url is not Undefined:
|
||||
<span>»</span>
|
||||
|
@ -258,6 +258,8 @@
|
|||
% if master.viewing and grid_index:
|
||||
${grid_index_nav()}
|
||||
% endif
|
||||
% else:
|
||||
<span>${index_title}</span>
|
||||
% endif
|
||||
% elif index_title:
|
||||
<span>${index_title}</span>
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue