diff --git a/app/assets/stylesheets/member-facing/global.scss b/app/assets/stylesheets/member-facing/global.scss
new file mode 100644
index 0000000000..16705b0d48
--- /dev/null
+++ b/app/assets/stylesheets/member-facing/global.scss
@@ -0,0 +1,10 @@
+.container {
+ section {
+ padding: 2em 4%;
+ clear: both;
+ }
+
+ .alternate-bg {
+ background-color: #ececec;
+ }
+}
diff --git a/app/javascript/async/email-ukparliament.js b/app/javascript/async/email-ukparliament.js
new file mode 100644
index 0000000000..bd1982db91
--- /dev/null
+++ b/app/javascript/async/email-ukparliament.js
@@ -0,0 +1,16 @@
+// @flow
+import React from 'react';
+import { render } from 'react-dom';
+import { EmailPluginConfig } from './interfaces';
+import EmailParliament from '../modules/EmailParliament';
+
+type Options = {
+ el: HTMLElement,
+ config: EmailPluginConfig,
+};
+export const init = (options: Options) => {
+ if (!options.config.active) return;
+ if (options.el) {
+ render(, options.el);
+ }
+};
diff --git a/app/javascript/async/email-your-mp.js b/app/javascript/async/email-your-mp.js
deleted file mode 100644
index 7e3cd50211..0000000000
--- a/app/javascript/async/email-your-mp.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react';
-
-PluginProps = {};
-const PluginView = (props: PluginProps) => {
- return
;
-};
diff --git a/app/javascript/async/index.js b/app/javascript/async/index.js
index 3049d5bdc4..03d15090f5 100644
--- a/app/javascript/async/index.js
+++ b/app/javascript/async/index.js
@@ -1,17 +1,53 @@
// @flow
-import PluginError from '../util/plugin-error';
-const MODULES = {
- 'email-your-mp': () => import('./email-your-mp'),
+type PluginErrorOptions = {
+ name?: string,
+ message: string,
};
-export default async (name: string, options: any) => {
+
+export class PluginError {
+ name: string;
+ message: string;
+ constructor(options: PluginErrorOptions) {
+ this.name = options.name || 'PluginError';
+ this.message = options.message;
+ }
+
+ toString() {
+ return `${this.name}: ${this.message}`;
+ }
+}
+
+export const MODULES = {
+ 'email-ukparliament': () => import('./email-ukparliament'),
+};
+
+// Lists supported async modules
+export const list = (): string[] => Object.keys(MODULES);
+
+// Loads an async module.
+export const load = async (name: string, options: any) => {
const loader = MODULES[name];
if (!loader) {
throw new PluginError({
message: `Plugin "${name}" is not available or does not support dynamic loading.`,
});
}
+ const plugin = await loader();
+ plugin.init(options);
+};
- const plugin = await loader(options);
- plugin.load(options);
+export const modules = {
+ // Attaches the async feature to the champaign global
+ // object.
+ setup(champaign: any) {
+ Object.assign(champaign, {
+ modules: {
+ load,
+ list,
+ },
+ });
+ },
};
+
+export default modules;
diff --git a/app/javascript/plugins/interfaces.js b/app/javascript/async/interfaces.js
similarity index 60%
rename from app/javascript/plugins/interfaces.js
rename to app/javascript/async/interfaces.js
index e054dc16fb..9b0bc14332 100644
--- a/app/javascript/plugins/interfaces.js
+++ b/app/javascript/async/interfaces.js
@@ -8,3 +8,8 @@ export interface PluginConfig {
pluginId: number;
title: string;
}
+
+export interface EmailPluginConfig extends PluginConfig {
+ subject: string;
+ template: string;
+}
diff --git a/app/javascript/components/ProfilePicture/ProfilePicture.css b/app/javascript/components/ProfilePicture/ProfilePicture.css
new file mode 100644
index 0000000000..8b8e69d009
--- /dev/null
+++ b/app/javascript/components/ProfilePicture/ProfilePicture.css
@@ -0,0 +1,6 @@
+.ProfilePicture {
+ border-radius: 50%;
+ width: 70px;
+ height: 70px;
+ border: 2px solid papayawhip;
+}
diff --git a/app/javascript/components/ProfilePicture/index.js b/app/javascript/components/ProfilePicture/index.js
new file mode 100644
index 0000000000..bb7388190f
--- /dev/null
+++ b/app/javascript/components/ProfilePicture/index.js
@@ -0,0 +1,17 @@
+// @flow
+import React from 'react';
+import classnames from 'classnames';
+import './ProfilePicture.css';
+
+type Props = {
+ src: string,
+ alt: string,
+ className?: string,
+};
+
+export default (props: Props) => {
+ if (!props.src) return null;
+ const classNames = classnames('ProfilePicture', props.className);
+
+ return ;
+};
diff --git a/app/javascript/member-facing/index.js b/app/javascript/member-facing/index.js
new file mode 100644
index 0000000000..292155caf8
--- /dev/null
+++ b/app/javascript/member-facing/index.js
@@ -0,0 +1,28 @@
+// @flow
+import Petition from './backbone/petition';
+import PetitionAndScrollToConsent from './backbone/petition_and_scroll_to_consent';
+import DoubleOptIn from './double_opt_in';
+import Survey from './backbone/survey';
+import ActionForm from './backbone/action_form';
+import OverlayToggle from './backbone/overlay_toggle';
+import Thermometer from './backbone/thermometer';
+import Sidebar from './backbone/sidebar';
+import Notification from './backbone/notification';
+import SweetPlaceholder from './backbone/sweet_placeholder';
+import CampaignerOverlay from './backbone/campaigner_overlay';
+
+export const setup = (champaign: any) => {
+ Object.assign(champaign, {
+ ActionForm,
+ CampaignerOverlay,
+ DoubleOptIn,
+ Notification,
+ OverlayToggle,
+ Petition,
+ PetitionAndScrollToConsent,
+ Sidebar,
+ Survey,
+ SweetPlaceholder,
+ Thermometer,
+ });
+};
diff --git a/app/javascript/modules/EmailParliament/EmailComposer.css b/app/javascript/modules/EmailParliament/EmailComposer.css
new file mode 100644
index 0000000000..e1a4535eb2
--- /dev/null
+++ b/app/javascript/modules/EmailParliament/EmailComposer.css
@@ -0,0 +1,5 @@
+.EmailComposer {
+ max-width: 800px;
+ margin-left: auto;
+ margin-right: auto;
+}
diff --git a/app/javascript/modules/EmailParliament/EmailComposer.js b/app/javascript/modules/EmailParliament/EmailComposer.js
new file mode 100644
index 0000000000..9fec0353d0
--- /dev/null
+++ b/app/javascript/modules/EmailParliament/EmailComposer.js
@@ -0,0 +1,50 @@
+// @flow
+// $FlowIgnore
+import React, { useState } from 'react';
+import SweetInput from '../../components/SweetInput/SweetInput';
+import FormGroup from '../../components/Form/FormGroup';
+import Editor from '../../components/EmailEditor/EmailEditor';
+import type { Target } from './index';
+import './EmailComposer.css';
+
+type Props = {
+ postcode: string,
+ title: string,
+ subject: string,
+ template: string,
+ target: Target,
+ body?: string,
+};
+
+export default (props: Props) => {
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+
+ return (
+
+ {props.title}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/app/javascript/modules/EmailParliament/RepresentativeCard.css b/app/javascript/modules/EmailParliament/RepresentativeCard.css
new file mode 100644
index 0000000000..232945bca8
--- /dev/null
+++ b/app/javascript/modules/EmailParliament/RepresentativeCard.css
@@ -0,0 +1,26 @@
+.RepresentativeCard {
+ display: flex;
+ margin-left: -4%;
+ margin-right: -4%;
+ padding: 8%;
+ background: white;
+ margin-top: 2em;
+ margin-bottom: 2em;
+}
+
+.RepresentativeCard-copy {
+ margin: auto 1em;
+ font-size: 1.3em;
+}
+.RepresentativeCard-name {
+ font-weight: bold;
+}
+.RepresentativeCard-party {
+ color: darkgrey;
+}
+.RepresentativeCard-email {
+ font-size: 0.8em;
+ color: grey;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+
+}
diff --git a/app/javascript/modules/EmailParliament/RepresentativeCard.js b/app/javascript/modules/EmailParliament/RepresentativeCard.js
new file mode 100644
index 0000000000..2038a2f281
--- /dev/null
+++ b/app/javascript/modules/EmailParliament/RepresentativeCard.js
@@ -0,0 +1,42 @@
+// @flow
+import React from 'react';
+import ProfilePicture from '../../components/ProfilePicture';
+import type { Target } from './index';
+import './RepresentativeCard.css';
+
+type Props = {
+ target?: Target,
+};
+
+export default (props: Props) => {
+ let target = props.target;
+ if (!target) target = test;
+ // const { target } = props;
+
+ return (
+
+
+
+ {target.displayAs}
+ {target.party}
+
+ email: {target.email}
+
+
+ );
+};
+
+const test = {
+ email: 'stella.creasy.mp@parliament.uk',
+ name: 'Stella Creasy',
+ displayAs: 'Stella Creasy',
+ listAs: 'Creasy, Stella',
+ party: 'Labour (Co-op)',
+ picture:
+ 'https://data.parliament.uk/membersdataplatform/services//images/MemberPhoto/4088/',
+ gender: 'F',
+ id: '4088',
+};
diff --git a/app/javascript/modules/EmailParliament/SearchByPostcode.css b/app/javascript/modules/EmailParliament/SearchByPostcode.css
new file mode 100644
index 0000000000..cb5e0a43a1
--- /dev/null
+++ b/app/javascript/modules/EmailParliament/SearchByPostcode.css
@@ -0,0 +1,8 @@
+.SearchByPostcode {
+ max-width: 450px;
+ margin: auto;
+}
+.SearchByPostcode .title {
+ margin: 1em 0;
+}
+
diff --git a/app/javascript/modules/EmailParliament/SearchByPostcode.js b/app/javascript/modules/EmailParliament/SearchByPostcode.js
new file mode 100644
index 0000000000..9204ebdc13
--- /dev/null
+++ b/app/javascript/modules/EmailParliament/SearchByPostcode.js
@@ -0,0 +1,74 @@
+// @flow
+// $FlowIgnore
+import React, { useState, useEffect } from 'react';
+import Button from '../../components/Button/Button';
+import SweetInput from '../../components/SweetInput/SweetInput';
+import FormGroup from '../../components/Form/FormGroup';
+import SearchError from './SearchError';
+import { search } from './api';
+import type { Target } from './index';
+
+import './SearchByPostcode.css';
+
+type Props = {
+ onChange: (target: Target) => void,
+};
+
+const SearchByPostcode = (props: Props) => {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(false);
+ const [postcode, setPostcode] = useState(null);
+ const [input, setInput] = useState('');
+
+ const reset = () => {
+ setLoading();
+ };
+ useEffect(
+ () => {
+ (async () => {
+ if (!postcode) return;
+
+ setLoading(true);
+ setError(false);
+
+ try {
+ const target = await search(postcode);
+ props.onChange(target);
+ } catch (e) {
+ setError(true);
+ } finally {
+ setLoading(false);
+ }
+ })();
+ },
+ [postcode]
+ );
+
+ const submit = (e: SyntheticEvent) => {
+ e.preventDefault();
+ setPostcode(input);
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default SearchByPostcode;
diff --git a/app/javascript/modules/EmailParliament/SearchError.css b/app/javascript/modules/EmailParliament/SearchError.css
new file mode 100644
index 0000000000..440f6b0efe
--- /dev/null
+++ b/app/javascript/modules/EmailParliament/SearchError.css
@@ -0,0 +1,7 @@
+.SearchError {
+ background: #fff;
+ color: orangered;
+ border: 2px solid crimson;
+ border-radius: 2px;
+ padding: 0.5em 1em;
+}
diff --git a/app/javascript/modules/EmailParliament/SearchError.js b/app/javascript/modules/EmailParliament/SearchError.js
new file mode 100644
index 0000000000..a6e427c90e
--- /dev/null
+++ b/app/javascript/modules/EmailParliament/SearchError.js
@@ -0,0 +1,19 @@
+// @flow
+import React from 'react';
+import './SearchError.css';
+
+type Props = {
+ error: boolean,
+};
+
+export default ({ error }: Props) => {
+ if (!error) return null;
+ return (
+
+
+ We couldn't find your MP. Check that your postcode is correct, or get in
+ touch with us if you think there might be an issue.
+
+
+ );
+};
diff --git a/app/javascript/modules/EmailParliament/api.js b/app/javascript/modules/EmailParliament/api.js
new file mode 100644
index 0000000000..295f811b4e
--- /dev/null
+++ b/app/javascript/modules/EmailParliament/api.js
@@ -0,0 +1,22 @@
+// @flow
+import type { Target } from './index';
+
+export const search = async (postcode: ?string) => {
+ const result: Target = await fetch(
+ 'https://sls.sumofus.org/parliament-data/mps',
+ {
+ method: 'post',
+ headers: {
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({ postcode }),
+ }
+ ).then(r => r.json());
+
+ console.log('result:', result);
+
+ return result;
+};
+
+type SendEmailParams = {};
+export const sendEmail = async (params: SendEmailParams) => {};
diff --git a/app/javascript/modules/EmailParliament/index.js b/app/javascript/modules/EmailParliament/index.js
new file mode 100644
index 0000000000..fffb5fc0f2
--- /dev/null
+++ b/app/javascript/modules/EmailParliament/index.js
@@ -0,0 +1,45 @@
+// @flow
+// $FlowIgnore
+import React, { useState } from 'react';
+import { search } from './api';
+import SearchByPostcode from './SearchByPostcode';
+import RepresentativeCard from './RepresentativeCard';
+import EmailComposer from './EmailComposer';
+import ComponentWrapper from '../../components/ComponentWrapper';
+import type { EmailPluginConfig } from '../../async/interfaces';
+
+export type Target = {
+ displayAs: string,
+ email: string,
+ gender: string,
+ id: string,
+ listAs: string,
+ name: string,
+ party: string,
+ picture: string,
+};
+
+type Props = {
+ config: EmailPluginConfig,
+};
+
+const EmailParliament = (props: Props) => {
+ const [target, setTarget] = useState(null);
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default EmailParliament;
diff --git a/app/javascript/packs/member_facing.js b/app/javascript/packs/member_facing.js
index 28011bf0b7..4954292880 100644
--- a/app/javascript/packs/member_facing.js
+++ b/app/javascript/packs/member_facing.js
@@ -1,4 +1,4 @@
-// @flow
+import I18n from 'champaign-i18n';
import 'whatwg-fetch';
import '../shared/show_errors';
import '../member-facing/registration';
@@ -7,55 +7,40 @@ import '../recommend_pages/recommend_pages';
import '../util/event_tracking';
import { mapValues, pick } from 'lodash';
+import flatten from 'flat';
import URI from 'urijs';
import configureStore from '../state';
-import Petition from '../member-facing/backbone/petition';
-import PetitionAndScrollToConsent from '../member-facing/backbone/petition_and_scroll_to_consent';
-import DoubleOptIn from '../member-facing/double_opt_in';
-import Survey from '../member-facing/backbone/survey';
-import ActionForm from '../member-facing/backbone/action_form';
-import OverlayToggle from '../member-facing/backbone/overlay_toggle';
-import Thermometer from '../member-facing/backbone/thermometer';
-import Sidebar from '../member-facing/backbone/sidebar';
-import Notification from '../member-facing/backbone/notification';
-import SweetPlaceholder from '../member-facing/backbone/sweet_placeholder';
-import CampaignerOverlay from '../member-facing/backbone/campaigner_overlay';
import redirectors from '../member-facing/redirectors';
import { FeaturesHelper } from '../state/features';
import DonationsThermometer from '../plugins/donations-thermometer';
import ProgressTracker from '../plugins/progress-tracker';
-
+import asyncModules from '../async/';
+import * as legacy from '../member-facing/';
window.URI = URI;
if (process.env.EXTERNAL_ASSETS_JS_PATH) {
- import(process.env.EXTERNAL_ASSETS_JS_PATH);
+ // $FlowIgnore
+ require(process.env.EXTERNAL_ASSETS_JS_PATH);
}
window.champaign = window.champaign || {};
+// Legacy components (Backbone)
+legacy.setup(window.champaign);
+
+// Async modules (module splitting)
+asyncModules.setup(window.champaign);
+
const store = configureStore(window.champaign);
Object.assign(window.champaign, {
- ActionForm,
- CampaignerOverlay,
DonationsThermometer,
- DoubleOptIn,
- Notification,
- OverlayToggle,
- Petition,
- PetitionAndScrollToConsent,
ProgressTracker,
- Sidebar,
- Survey,
- SweetPlaceholder,
- Thermometer,
redirectors,
store,
features: new FeaturesHelper(store),
});
-import flatten from 'flat';
-
I18n.flatTranslations = mapValues(I18n.translations, (value, key) =>
flatten(
pick(value, [
diff --git a/app/javascript/util/plugin-error.js b/app/javascript/util/plugin-error.js
deleted file mode 100644
index 3ae4575f33..0000000000
--- a/app/javascript/util/plugin-error.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// @flow
-
-type PluginErrorOptions = {
- name?: string,
- message: string,
-};
-
-export default class PluginError {
- name: string;
- message: string;
- constructor(options: PluginErrorOptions) {
- this.name = options.name || 'PluginError';
- this.message = options.message;
- }
-
- toString() {
- return `${this.name}: ${this.message}`;
- }
-}
diff --git a/app/models/plugins/email.rb b/app/models/plugins/email.rb
index fc5504d6a6..4300ca15e4 100644
--- a/app/models/plugins/email.rb
+++ b/app/models/plugins/email.rb
@@ -1,3 +1,28 @@
+# == Schema Information
+#
+# Table name: plugins_emails
+#
+# id :bigint(8) not null, primary key
+# active :boolean default(FALSE)
+# from :string
+# instructions :text
+# ref :string
+# spoof_member_email :boolean default(FALSE)
+# subjects :string default([]), is an Array
+# template :text
+# test_email_address :string
+# title :string default("")
+# created_at :datetime not null
+# updated_at :datetime not null
+# page_id :bigint(8)
+# registered_email_address_id :bigint(8)
+#
+# Indexes
+#
+# index_plugins_emails_on_page_id (page_id)
+# index_plugins_emails_on_registered_email_address_id (registered_email_address_id)
+#
+
class Plugins::Email < ApplicationRecord
DEFAULTS = {}.freeze
@@ -14,6 +39,7 @@ def liquid_data(_supplemental_data = {})
page_id: page_id,
plugin_id: id,
locale: page.language_code,
+ title: title,
active: active,
subject: subjects.sample,
template: template
diff --git a/app/views/pages/_packs.slim b/app/views/pages/_packs.slim
index c91cdb0a67..34052489a7 100644
--- a/app/views/pages/_packs.slim
+++ b/app/views/pages/_packs.slim
@@ -1,5 +1,4 @@
- plugins.each do |plugin|
- name = plugin.name.underscore
- - puts "Plugin name: #{name}"
= stylesheet_pack_tag(name) if pack_exists?(name, :stylesheet)
= javascript_pack_tag(name) if pack_exists?(name, :javascript)
diff --git a/app/views/plugins/emails/_email.liquid b/app/views/plugins/emails/_email.liquid
index 5a390971e1..7f10c6628f 100644
--- a/app/views/plugins/emails/_email.liquid
+++ b/app/views/plugins/emails/_email.liquid
@@ -1,11 +1,12 @@
{% if plugins.email[ref].active %}
- hello
+
{% endif %}
diff --git a/app/views/plugins/emails/_form.slim b/app/views/plugins/emails/_form.slim
index 483ed78171..0795979a0d 100644
--- a/app/views/plugins/emails/_form.slim
+++ b/app/views/plugins/emails/_form.slim
@@ -25,7 +25,7 @@
.has-error
strong = t('plugins.email_tool.test_email_warning')
- / = render 'plugins/shared/subject_editor', name: name, f: f, plugin: plugin, tooltip: t('plugins.email_tool.tooltips.email_content')
+ = render 'plugins/emails/subject_editor', name: name, f: f, plugin: plugin, tooltip: t('plugins.email_tool.tooltips.email_content')
.email-template-section
.form-group
diff --git a/app/views/plugins/emails/_subject_editor.slim b/app/views/plugins/emails/_subject_editor.slim
new file mode 100644
index 0000000000..d9eceb0d49
--- /dev/null
+++ b/app/views/plugins/emails/_subject_editor.slim
@@ -0,0 +1,17 @@
+- list_editor_id = "#{name}_subjects"
+.form-group class=list_editor_id
+ = label_with_tooltip f, :subjects, t('plugins.email_tool.subject'), tooltip
+ .form-element__choice-fields
+ - plugin.subjects << '' if plugin.subjects.empty?
+ - plugin.subjects.each_with_index do |subject, i|
+ .form-element__choice-field
+ = f.text_field :subjects, value: subject, multiple: true, class: 'form-control', autocomplete: "subject-#{i}"
+ a.form-element__remove-choice
+ small
+ span.glyphicon.glyphicon-remove
+ a.form-element__add-choice = t('form_elements.add_choice')
+
+ javascript:
+ $(document).ready(function(){
+ new window.ListEditor({el: '.#{list_editor_id}' });
+ });
diff --git a/db/migrate/20190417165312_create_plugins_emails.rb b/db/migrate/20190417165312_create_plugins_emails.rb
index 120ebc1945..35dd0f93bc 100644
--- a/db/migrate/20190417165312_create_plugins_emails.rb
+++ b/db/migrate/20190417165312_create_plugins_emails.rb
@@ -1,5 +1,18 @@
class CreatePluginsEmails < ActiveRecord::Migration[5.2]
def change
- create_table :plugins_emails, &:timestamps
+ create_table :plugins_emails do |t|
+ t.boolean :active, default: false
+ t.boolean :spoof_member_email, default: false
+ t.string :from
+ t.string :ref
+ t.string :subjects, array: true, default: []
+ t.string :test_email_address
+ t.string :title, default: ''
+ t.text :template
+ t.text :instructions
+ t.references :page, index: true
+ t.references :registered_email_address
+ t.timestamps
+ end
end
end
diff --git a/db/schema.rb b/db/schema.rb
index 29514d0227..55cf1283e5 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_04_04_164508) do
+ActiveRecord::Schema.define(version: 2019_04_17_165312) do
# These are extensions that must be enabled in order to support this database
enable_extension "intarray"
@@ -457,6 +457,21 @@
t.string "target_by_attributes", default: [], array: true
end
+ create_table "plugins_email_by_lookups", force: :cascade do |t|
+ t.string "ref", default: "default"
+ t.bigint "page_id"
+ t.boolean "active", default: false
+ t.string "from"
+ t.string "subjects", default: [], array: true
+ t.text "body"
+ t.text "body_header"
+ t.text "body_footer"
+ t.string "test_email"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["page_id"], name: "index_plugins_email_by_lookups_on_page_id"
+ end
+
create_table "plugins_email_pensions", id: :serial, force: :cascade do |t|
t.string "ref"
t.integer "page_id"
@@ -511,6 +526,24 @@
t.index ["page_id"], name: "index_plugins_email_tools_on_page_id"
end
+ create_table "plugins_emails", force: :cascade do |t|
+ t.boolean "active", default: false
+ t.boolean "spoof_member_email", default: false
+ t.string "from"
+ t.string "ref"
+ t.string "subjects", default: [], array: true
+ t.string "test_email_address"
+ t.string "title", default: ""
+ t.text "template"
+ t.text "instructions"
+ t.bigint "page_id"
+ t.bigint "registered_email_address_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["page_id"], name: "index_plugins_emails_on_page_id"
+ t.index ["registered_email_address_id"], name: "index_plugins_emails_on_registered_email_address_id"
+ end
+
create_table "plugins_fundraisers", id: :serial, force: :cascade do |t|
t.string "title"
t.string "ref"
diff --git a/flow-typed/npm/react-redux_v5.x.x.js b/flow-typed/npm/react-redux_v5.x.x.js
index 759ad5a56e..a3ed094f28 100644
--- a/flow-typed/npm/react-redux_v5.x.x.js
+++ b/flow-typed/npm/react-redux_v5.x.x.js
@@ -1,5 +1,5 @@
-// flow-typed signature: 502cfd4f5e95c6308f747cdf16dc93ce
-// flow-typed version: 1751d5bf0a/react-redux_v5.x.x/flow_>=v0.68.0
+// flow-typed signature: b872fdc4da6b0c1021c55df6fab87e73
+// flow-typed version: f8afc4cfdd/react-redux_v5.x.x/flow_>=v0.68.0 <=v0.84.x
declare module "react-redux" {
import type { ComponentType, ElementConfig } from 'react';
diff --git a/spec/factories/plugins_emails.rb b/spec/factories/plugins_emails.rb
index 73b4297907..49edea40bb 100644
--- a/spec/factories/plugins_emails.rb
+++ b/spec/factories/plugins_emails.rb
@@ -1,3 +1,28 @@
+# == Schema Information
+#
+# Table name: plugins_emails
+#
+# id :bigint(8) not null, primary key
+# active :boolean default(FALSE)
+# from :string
+# instructions :text
+# ref :string
+# spoof_member_email :boolean default(FALSE)
+# subjects :string default([]), is an Array
+# template :text
+# test_email_address :string
+# title :string default("")
+# created_at :datetime not null
+# updated_at :datetime not null
+# page_id :bigint(8)
+# registered_email_address_id :bigint(8)
+#
+# Indexes
+#
+# index_plugins_emails_on_page_id (page_id)
+# index_plugins_emails_on_registered_email_address_id (registered_email_address_id)
+#
+
FactoryBot.define do
factory :plugins_email, class: 'Plugins::Email' do
end
diff --git a/spec/models/plugins/email_spec.rb b/spec/models/plugins/email_spec.rb
index 416d47ec3e..2616ee89f9 100644
--- a/spec/models/plugins/email_spec.rb
+++ b/spec/models/plugins/email_spec.rb
@@ -1,3 +1,28 @@
+# == Schema Information
+#
+# Table name: plugins_emails
+#
+# id :bigint(8) not null, primary key
+# active :boolean default(FALSE)
+# from :string
+# instructions :text
+# ref :string
+# spoof_member_email :boolean default(FALSE)
+# subjects :string default([]), is an Array
+# template :text
+# test_email_address :string
+# title :string default("")
+# created_at :datetime not null
+# updated_at :datetime not null
+# page_id :bigint(8)
+# registered_email_address_id :bigint(8)
+#
+# Indexes
+#
+# index_plugins_emails_on_page_id (page_id)
+# index_plugins_emails_on_registered_email_address_id (registered_email_address_id)
+#
+
require 'rails_helper'
RSpec.describe Plugins::Email, type: :model do