Added settings and messages to manage sending web inquiries to one or more email addresses specified via the web management interface.

This commit is contained in:
Wynne Crisman
2020-01-16 09:32:59 -08:00
parent 3b1c57e47e
commit 9d243850db
15 changed files with 364 additions and 30 deletions

View File

@@ -6,16 +6,16 @@
meteor-base@1.4.0 # Packages every Meteor app needs to have meteor-base@1.4.0 # Packages every Meteor app needs to have
mobile-experience@1.0.5 # Packages for a great mobile UX mobile-experience@1.0.5 # Packages for a great mobile UX
mongo@1.6.0 # The database Meteor supports right now mongo@1.6.2 # The database Meteor supports right now
blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
reactive-var@1.0.11 # Reactive variable for tracker reactive-var@1.0.11 # Reactive variable for tracker
reactive-dict@1.2.1 # ??? reactive-dict@1.3.0 # ???
tracker@1.2.0 # Meteor's client-side reactive programming library tracker@1.2.0 # Meteor's client-side reactive programming library
tomwasd:history-polyfill # Adds IE 8/9 support for HTML5 history. tomwasd:history-polyfill # Adds IE 8/9 support for HTML5 history.
email@1.2.3 # Adds the Meteor/Email package for sending lost password emails email@1.2.3 # Adds the Meteor/Email package for sending lost password emails
standard-minifier-css@1.5.2 # CSS minifier run for production mode standard-minifier-css@1.5.3 # CSS minifier run for production mode
standard-minifier-js@2.4.0 # JS minifier run for production mode standard-minifier-js@2.4.1 # JS minifier run for production mode
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers. es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers.
poorvavyas:es6-shim poorvavyas:es6-shim
ecmascript@0.12.4 # Enable ECMAScript2015+ syntax in app code ecmascript@0.12.4 # Enable ECMAScript2015+ syntax in app code
@@ -60,7 +60,7 @@ juliancwirko:s-alert # Client error/alert handling
jcbernack:reactive-aggregate # Allows us to create a new client collection (from the server) with the contents being an aggregate of server data. Note that aggregation can only be done on the server currently as mini-mongo does not support it. jcbernack:reactive-aggregate # Allows us to create a new client collection (from the server) with the contents being an aggregate of server data. Note that aggregation can only be done on the server currently as mini-mongo does not support it.
ostrio:logger ostrio:logger
ostrio:loggermongo ostrio:loggermongo
dynamic-import@0.5.0 dynamic-import@0.5.1
markdown@1.0.12 markdown@1.0.12
wcrisman:jquery-custom-scrollbar wcrisman:jquery-custom-scrollbar
wcrisman:server-side-seo # A custom plugin to take all HTML from the app, and place the templates in a map in a file in the private folder for use at runtime to generate search engine readable html. See code in the /packages folder for this project. wcrisman:server-side-seo # A custom plugin to take all HTML from the app, and place the templates in a map in a file in the private folder for use at runtime to generate search engine readable html. See code in the /packages folder for this project.

View File

@@ -1 +1 @@
METEOR@1.8.0.2 METEOR@1.8.1

View File

@@ -6,8 +6,8 @@ aldeed:schema-index@3.0.0
aldeed:template-extension@4.1.0 aldeed:template-extension@4.1.0
allow-deny@1.1.0 allow-deny@1.1.0
arillo:flow-router-helpers@0.5.2 arillo:flow-router-helpers@0.5.2
autoupdate@1.5.0 autoupdate@1.6.0
babel-compiler@7.2.4 babel-compiler@7.3.4
babel-runtime@1.3.0 babel-runtime@1.3.0
base64@1.0.11 base64@1.0.11
binary-heap@1.0.11 binary-heap@1.0.11
@@ -24,7 +24,7 @@ ddp@1.4.0
ddp-client@2.3.3 ddp-client@2.3.3
ddp-common@1.4.0 ddp-common@1.4.0
ddp-rate-limiter@1.0.7 ddp-rate-limiter@1.0.7
ddp-server@2.2.0 ddp-server@2.3.0
deps@1.0.12 deps@1.0.12
diff-sequence@1.1.1 diff-sequence@1.1.1
dynamic-import@0.5.1 dynamic-import@0.5.1
@@ -35,7 +35,7 @@ ecmascript-runtime-server@0.7.1
ejson@1.1.0 ejson@1.1.0
email@1.2.3 email@1.2.3
es5-shim@4.8.0 es5-shim@4.8.0
fetch@0.1.0 fetch@0.1.1
fortawesome:fontawesome@4.7.0 fortawesome:fontawesome@4.7.0
geojson-utils@1.0.10 geojson-utils@1.0.10
hot-code-push@1.0.4 hot-code-push@1.0.4
@@ -55,7 +55,7 @@ localstorage@1.2.0
logging@1.1.20 logging@1.1.20
manuel:reactivearray@1.0.9 manuel:reactivearray@1.0.9
markdown@1.0.12 markdown@1.0.12
meteor@1.9.2 meteor@1.9.3
meteor-base@1.4.0 meteor-base@1.4.0
meteorhacks:picker@1.0.3 meteorhacks:picker@1.0.3
meteorhacks:ssr@2.2.0 meteorhacks:ssr@2.2.0
@@ -76,24 +76,24 @@ meteortoys:sub@4.0.0
meteortoys:throttle@4.0.0 meteortoys:throttle@4.0.0
meteortoys:toggle@4.0.0 meteortoys:toggle@4.0.0
meteortoys:toykit@4.0.2 meteortoys:toykit@4.0.2
minifier-css@1.4.1 minifier-css@1.4.2
minifier-js@2.4.0 minifier-js@2.4.1
minimongo@1.4.5 minimongo@1.4.5
mizzao:bootboxjs@4.4.0 mizzao:bootboxjs@4.4.0
mobile-experience@1.0.5 mobile-experience@1.0.5
mobile-status-bar@1.0.14 mobile-status-bar@1.0.14
modern-browsers@0.1.3 modern-browsers@0.1.4
modules@0.13.0 modules@0.13.0
modules-runtime@0.10.3 modules-runtime@0.10.3
momentjs:moment@2.23.0 momentjs:moment@2.23.0
mongo@1.6.0 mongo@1.6.2
mongo-decimal@0.1.0 mongo-decimal@0.1.1
mongo-dev-server@1.1.0 mongo-dev-server@1.1.0
mongo-id@1.0.7 mongo-id@1.0.7
msavin:jetsetter@4.0.0 msavin:jetsetter@4.0.0
msavin:mongol@7.0.1 msavin:mongol@7.0.1
npm-bcrypt@0.9.3 npm-bcrypt@0.9.3
npm-mongo@3.1.1 npm-mongo@3.1.2
observe-sequence@1.0.16 observe-sequence@1.0.16
ordered-dict@1.1.0 ordered-dict@1.1.0
ostrio:cookies@2.3.0 ostrio:cookies@2.3.0
@@ -106,9 +106,9 @@ promise@0.11.2
raix:eventemitter@0.1.3 raix:eventemitter@0.1.3
random@1.1.0 random@1.1.0
rate-limit@1.0.9 rate-limit@1.0.9
reactive-dict@1.2.1 reactive-dict@1.3.0
reactive-var@1.0.11 reactive-var@1.0.11
reload@1.2.0 reload@1.3.0
retry@1.1.0 retry@1.1.0
routepolicy@1.1.0 routepolicy@1.1.0
service-configuration@1.0.11 service-configuration@1.0.11
@@ -120,8 +120,8 @@ softwarerero:accounts-t9n@1.3.11
spacebars@1.0.15 spacebars@1.0.15
spacebars-compiler@1.1.3 spacebars-compiler@1.1.3
srp@1.0.12 srp@1.0.12
standard-minifier-css@1.5.2 standard-minifier-css@1.5.3
standard-minifier-js@2.4.0 standard-minifier-js@2.4.1
stylus@2.513.14 stylus@2.513.14
templating@1.3.2 templating@1.3.2
templating-compiler@1.3.3 templating-compiler@1.3.3
@@ -138,6 +138,6 @@ useraccounts:flow-routing@1.14.2
useraccounts:unstyled@1.14.2 useraccounts:unstyled@1.14.2
wcrisman:jquery-custom-scrollbar@3.0.0 wcrisman:jquery-custom-scrollbar@3.0.0
wcrisman:server-side-seo@1.0.0 wcrisman:server-side-seo@1.0.0
webapp@1.7.2 webapp@1.7.3
webapp-hashing@1.0.9 webapp-hashing@1.0.9
zimme:active-route@2.3.2 zimme:active-route@2.3.2

View File

@@ -175,7 +175,7 @@ h3
z-index: 999 z-index: 999
@import "../imports/ui/styles/snapTable.import.styl"
@import "../imports/ui/styles/effects.import.styl" @import "../imports/ui/styles/effects.import.styl"
@import "../imports/ui/styles/buttons.import.styl" @import "../imports/ui/styles/buttons.import.styl"
@import "../imports/ui/styles/maxHeightLayout.import.styl" @import "../imports/ui/styles/maxHeightLayout.import.styl"
@@ -192,6 +192,7 @@ h3
@import "../imports/ui/Home.import.styl" @import "../imports/ui/Home.import.styl"
@import "../imports/ui/Admin/Messages.import.styl"
@import "../imports/ui/Admin/UserManagement.import.styl" @import "../imports/ui/Admin/UserManagement.import.styl"
@import "../imports/ui/Admin/InternshipEditor.import.styl" @import "../imports/ui/Admin/InternshipEditor.import.styl"
@import "../imports/ui/Admin/PageEditor.import.styl" @import "../imports/ui/Admin/PageEditor.import.styl"

View File

@@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo'; import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check'; import { check } from 'meteor/check';
import { Email } from 'meteor/email';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
let ContactUsMessages = new Mongo.Collection('ContactUsMessages'); let ContactUsMessages = new Mongo.Collection('ContactUsMessages');
@@ -32,7 +33,10 @@ ContactUsMessages.attachSchema(new SimpleSchema({
})); }));
if(Meteor.isServer) Meteor.publish('contactUsMessages', function() { if(Meteor.isServer) Meteor.publish('contactUsMessages', function() {
return ContactUsMessages.find({}); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
return ContactUsMessages.find({});
}
else throw new Meteor.Error(403, "Not authorized.");
}); });
if(Meteor.isServer) { if(Meteor.isServer) {
@@ -42,10 +46,18 @@ if(Meteor.isServer) {
check(email, String); check(email, String);
check(message, String); check(message, String);
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { ContactUsMessages.insert({name, email, message, createdAt: new Date()});
ContactUsMessages.insert({name, email, message, createdAt: new Date()});
try {
let settings = Meteor.collections.Settings.findOne();
if(settings && settings.forwardEmailsTo && settings.forwardEmailsTo.length > 0) {
Email.send({to: settings.forwardEmailsTo, from: "Do Not Reply <no-reply@andersonvalleyeducation.org>", subject: "Contact Us Message", text: "Email: " + email + "\n\n" + message});
}
}
catch(err) {
console.log(err);
} }
else throw new Meteor.Error(403, "Not authorized.");
} }
}); });
} }

52
imports/api/Settings.js Normal file
View File

@@ -0,0 +1,52 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
import SimpleSchema from 'simpl-schema';
let Settings = new Mongo.Collection('Settings');
let singletonId;
Settings.attachSchema(new SimpleSchema({
forwardEmailsTo: {
type: Array,
label: "Forward Emails To",
optional: true,
defaultValue: []
},
'forwardEmailsTo.$': {
type: String,
label: "Email",
regEx: SimpleSchema.RegEx.Email
}
}));
if(Meteor.isServer) Meteor.publish('Settings', function() {
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
return Settings.find({});
}
else throw new Meteor.Error(403, "Not authorized.");
});
if(Meteor.isServer) {
let singleton = Settings.findOne({});
if(!singleton) {
singletonId = Settings.insert({});
}
else {
singletonId = singleton._id;
}
Meteor.methods({
changeForwardEmailsTo: function(emails) {
if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) {
check(emails, [String]);
Settings.update({_id: singletonId}, {$set: {forwardEmailsTo: emails}});
}
else throw new Meteor.Error(403, "Not authorized.");
}
});
}
export default Settings;

View File

@@ -6,9 +6,10 @@ import ImageUploads from "./Images.js";
import Slideshow from "./Slideshow.js"; import Slideshow from "./Slideshow.js";
import Internship from "./Internship.js"; import Internship from "./Internship.js";
import ContactUsMessages from "./ContactUsMessages.js"; import ContactUsMessages from "./ContactUsMessages.js";
import Settings from "./Settings.js";
//Save the collections in the Meteor.collections property for easy access without name conflicts. //Save the collections in the Meteor.collections property for easy access without name conflicts.
Meteor.collections = {Users, UserRoles, Pages, Slideshow, Internship, ContactUsMessages, ImageUploads}; Meteor.collections = {Users, UserRoles, Pages, Slideshow, Internship, ContactUsMessages, ImageUploads, Settings};
//If this is the server then setup the default admin user if none exist. //If this is the server then setup the default admin user if none exist.
if(Meteor.isServer) { if(Meteor.isServer) {

View File

@@ -88,6 +88,13 @@ pri.route("/Admin/SlideshowPageEditor", {
BlazeLayout.render("Admin", {content: "PageEditor"}); BlazeLayout.render("Admin", {content: "PageEditor"});
} }
}); });
pri.route("/Admin/Messages", {
name: "Messages",
action: function(params, queryParams) {
require("/imports/ui/Admin/Messages.js");
BlazeLayout.render("Admin", {content: "Messages"});
}
});
//*** PUBLIC //*** PUBLIC
pub.route('/', { pub.route('/', {

View File

@@ -0,0 +1,58 @@
<template name="Messages">
<div id="messages" class="snapTable">
{{#if Template.subscriptionsReady}}
<div class="emailContainer"><label>Forward Messages To:</label>
<select class="emailEditor" multiple="multiple">
{{#each emails}}
<option value="{{this}}" selected>{{this}}</option>
{{/each}}
</select>
</div>
<div class="tableControls">
<div class="contentControls">
<a class="loadMoreLink {{#if disableLoadMore}}disabled{{/if}}" href="javascript:">Load More...</a>
</div>
</div>
<div class="separatedTableHeader">
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="email">Email</th>
<th class="message">Message</th>
<th class="date">Date</th>
<th class="actions">Actions</th>
</tr>
</thead>
</table>
</div>
<div class="listContianer">
<div class="listRow">
<div class="listCell">
<div class="tableContainer mCustomScrollbar" data-mcs-theme="dark">
<table class="dataTable table table-striped table-hover">
<tbody>
{{#each messages}}
{{> Message}}
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
</div>
{{else}}
{{/if}}
</div>
</template>
<template name="Message">
<tr class="messageDetails" data-id="{{_id}}">
<td class="email tdLarge noselect nonclickable">{{email}}</td>
<td class="message tdLarge noselect nonclickable">{{message}}</td>
<td class="date tdLarge noselect nonclickable">{{date}}</td>
<td class="actions center tdLarge"><i class="toggleHandled {{handledClass}} fa fa-mail fa-lg noselect clickable" aria-hidden="true"></i></td>
</tr>
<tr class="fullMessage">
<td colspan="4"><div>{{{fullMessage}}}</div></td>
</tr>
</template>

36
imports/ui/Admin/Messages.import.styl vendored Normal file
View File

@@ -0,0 +1,36 @@
#messages
.emailContainer
width 100
position relative
.emailEditor
min-width 200px
width 100%
tr
> .email
width 300px
min-width 100px
> .message
width 50%
min-width 100px
max-width 200px
overflow hidden
text-overflow ellipsis
white-space nowrap
> .date
width 260px
min-width 260px
> .actions
width 80px
min-width 80px
.toggleHandled
color red
.toggleHandled.handled
color green
tr.fullMessage
display none
background-color #b8c1dc
font-weight 800
div
padding-left 20px
tr.fullMessage.display
display table-row

View File

@@ -0,0 +1,95 @@
import './Messages.html';
import swal from "sweetalert2";
const PREFIX = "Messages_";
Tracker.autorun(function() {
Meteor.subscribe("contactUsMessages");
Meteor.subscribe("Settings");
});
Template.Messages.onCreated(function() {
Session.set(PREFIX + 'selected', null);
});
Template.Messages.onRendered(function() {
$(".tableContainer").mCustomScrollbar({
scrollButtons: {enable:true},
theme: "light-thick",
scrollbarPosition: "outside",
scrollEasing: "linear"
});
this.$(".emailEditor").select2({tags: true, tokenSeparators: [';']});
});
Template.Messages.helpers({
messages: function() {
return Meteor.collections.ContactUsMessages.find({}, {sort: {createdAt: 1}});
},
selected: function() {
return Session.get(PREFIX + "selected");
},
emails: function() {
let emails = [];
let settings = Meteor.collections.Settings.findOne({});
//Settings might be undefined if we are still loading.
if(settings) {
emails = settings.forwardEmailsTo;
//if(emails) {
// emails = emails.split(";");
//}
}
return emails;
}
});
Template.Messages.events({
'change .emailEditor': function(e, t) {
let emails = t.$(".emailEditor").select2('data');
emails = emails.map((n)=>n.id);
console.log(emails);
Meteor.call("changeForwardEmailsTo", emails, function(err, result) {
if(err) console.log(err);
});
},
'click table.dataTable tr.messageDetails': function(event, template) {
let $tr = template.$(event.currentTarget);
if(Session.get(PREFIX + "selected") && $tr.data('id') === Session.get(PREFIX + "selected")._id) {
$tr.removeClass('selected');
$tr.next().removeClass('display');
Session.set(PREFIX + "selected", undefined);
}
else {
$tr.siblings(".messageDetails").removeClass('selected');
$tr.siblings(".fullMessage").removeClass('display');
$tr.addClass('selected');
$tr.next().addClass('display');
Session.set(PREFIX + "selected", Meteor.collections.ContactUsMessages.findOne($tr.data('id')));
}
}
});
Template.Message.onCreated(function() {
});
Template.Message.helpers({
date: function() {
return moment(this.createdAt).format("MM/DD/YYYY");
},
selected: function() {
return Session.get(PREFIX + "selected");
},
handledClass: function() {
return "";
},
fullMessage: function() {
return this.message.replace(/[\n\r]/g, "<br/>");
}
});
Template.Message.events({
"click .toggleHandled": function(event, template) {
//TODO: Mark the message as handled.
}
});

View File

@@ -45,6 +45,11 @@
News & Notices News & Notices
</a> </a>
</li> </li>
<li class="{{isActiveRoute 'Messages'}}">
<a href="{{pathFor 'Messages'}}">
Messages
</a>
</li>
<li class="{{isActiveRoute 'UserManagement'}}"> <li class="{{isActiveRoute 'UserManagement'}}">
<a href="{{pathFor 'UserManagement'}}"> <a href="{{pathFor 'UserManagement'}}">
Users Users

67
imports/ui/styles/snapTable.import.styl vendored Normal file
View File

@@ -0,0 +1,67 @@
// A snap table is an abstraction of a full page table that I use repeatedly in projects. All of the table dialogs (editing, new, details, functions, etc) are embedded in the table its self.
// A basic table will have a load more link in the controls, and will load a fixed number of values. A scroll bar attached to the table contents will provide means of accessing all the data.
// The table header will be separated (in a separate HTML table) such that it is always visible.
// Often an action column will be in the mix to provide buttons related to the row, or if in the header will be an action on the table (such as a + button for adding new rows).
.snapTable
display table
content-box border-box
padding 10px 20px
height 100%
width 100%
text-align left
position relative
.tableControls
display table
width 100%
text-align right
margin-right 20px
a
font-size 12px
font-family "Arial", san-serif
font-weight 800
color #2d1b8c
text-decoration none
a:hover
text-decoration underline
a.disabled
visibility hidden
.table
table-layout fixed
min-width 100%
tr.selected
background-color #feff00
.separatedTableHeader
.actions
text-align center
.listContainer
position absolute
top 100px
left 0
right 0
bottom 0
.listRow
display table-row
.listCell
display table-cell
position relative
height 100%
width 100%
.tableContainer
position absolute
top 0
bottom 0
left 0
right 0
width auto
height auto
border 0
font-size 12.5px
overflow-y auto
table
table-layout fixed
width 100%
thead
display none
visibility hidden

2
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{ {
"name": "PetitTeton", "name": "AVEF",
"requires": true, "requires": true,
"lockfileVersion": 1, "lockfileVersion": 1,
"dependencies": { "dependencies": {

View File

@@ -1,5 +1,5 @@
{ {
"name": "PetitTeton", "name": "AVEF",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "meteor run", "start": "meteor run",