From 7b955ffad2f05a31a89b48accaf86a9dfecff949 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Wed, 1 Aug 2018 14:28:45 +0100 Subject: [PATCH 1/8] Add 'self' to 'frame-src' header Allows iframes to contain pages from the same domain as the parent page. --- app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index de4cc583c..37781f2ef 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -514,7 +514,7 @@ def useful_headers_after_request(response): "object-src 'self';" "font-src 'self' data:;" "img-src 'self' *.google-analytics.com *.notifications.service.gov.uk {} data:;" - "frame-src www.youtube.com;".format(get_cdn_domain()) + "frame-src 'self' www.youtube.com;".format(get_cdn_domain()) )) if 'Cache-Control' in response.headers: del response.headers['Cache-Control'] From a2a02a5185f3ea5f6d36764e918b72d842624e89 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Thu, 2 Aug 2018 14:46:01 +0100 Subject: [PATCH 2/8] Add preview step to branding selection flow Gives platform admins a chance to preview the combination of brand type and custom brand (coloured banner and logo) set for service before saving. --- app/main/forms.py | 6 ++++ app/main/views/service_settings.py | 34 ++++++++++++++++--- .../preview-email-branding.html | 24 +++++++++++++ .../service-settings/set-email-branding.html | 18 ++++++---- 4 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 app/templates/views/service-settings/preview-email-branding.html diff --git a/app/main/forms.py b/app/main/forms.py index 988afb8b8..c99a93319 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -717,6 +717,12 @@ class ServiceSetBranding(StripWhitespaceForm): ) +class ServicePreviewBranding(StripWhitespaceForm): + + branding_type = HiddenField('branding_type') + branding_style = HiddenField('branding_style') + + class ServiceSelectEmailBranding(StripWhitespaceForm): def __init__(self, email_brandings=[], *args, **kwargs): diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index fc1351cef..11b79cf49 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -41,6 +41,7 @@ from app.main.forms import ( ServiceEditInboundNumberForm, ServiceInboundNumberForm, ServiceLetterContactBlockForm, + ServicePreviewBranding, ServiceReplyToEmailForm, ServiceSetBranding, ServiceSmsSenderForm, @@ -895,12 +896,36 @@ def set_free_sms_allowance(service_id): @user_is_platform_admin def service_set_email_branding(service_id): email_branding = email_branding_client.get_all_email_branding() + branding_type = current_service.get('branding') - form = ServiceSetBranding(branding_type=current_service.get('branding')) + form = ServiceSetBranding(branding_type=branding_type) # dynamically create org choices, including the null option form.branding_style.choices = [('None', 'None')] + get_branding_as_value_and_label(email_branding) + if form.validate_on_submit(): + branding_style = None if form.branding_style.data == 'None' else form.branding_style.data + return redirect(url_for('.service_preview_email_branding', service_id=service_id, + branding_type=form.branding_type.data, branding_style=branding_style)) + + form.branding_style.data = current_service['email_branding'] or 'None' + + return render_template( + 'views/service-settings/set-email-branding.html', + form=form, + branding_dict=get_branding_as_dict(email_branding) + ) + + +@main.route("/services//service-settings/preview-email-branding", methods=['GET', 'POST']) +@login_required +@user_is_platform_admin +def service_preview_email_branding(service_id): + branding_type = request.args.get('branding_type', None) + branding_style = request.args.get('branding_style', None) + + form = ServicePreviewBranding(branding_type=branding_type, branding_style=branding_style) + if form.validate_on_submit(): branding_style = None if form.branding_style.data == 'None' else form.branding_style.data service_api_client.update_service( @@ -910,12 +935,11 @@ def service_set_email_branding(service_id): ) return redirect(url_for('.service_settings', service_id=service_id)) - form.branding_style.data = current_service.email_branding or 'None' - return render_template( - 'views/service-settings/set-email-branding.html', + 'views/service-settings/preview-email-branding.html', form=form, - branding_dict=get_branding_as_dict(email_branding) + service_id=service_id, + action=url_for('main.service_preview_email_branding', service_id=service_id), ) diff --git a/app/templates/views/service-settings/preview-email-branding.html b/app/templates/views/service-settings/preview-email-branding.html new file mode 100644 index 000000000..51ecdb7e2 --- /dev/null +++ b/app/templates/views/service-settings/preview-email-branding.html @@ -0,0 +1,24 @@ +{% extends "views/platform-admin/_base_template.html" %} + +{% block service_page_title %} + Preview email branding +{% endblock %} + +{% block platform_admin_content %} + +

Preview email branding

+
+
+ +
+
+ {{ form.hidden_tag() }} + +
+
+
+
+{% endblock %} diff --git a/app/templates/views/service-settings/set-email-branding.html b/app/templates/views/service-settings/set-email-branding.html index 6aef3a71e..194d138a7 100644 --- a/app/templates/views/service-settings/set-email-branding.html +++ b/app/templates/views/service-settings/set-email-branding.html @@ -9,18 +9,24 @@ {% block maincolumn_content %}

Set email branding

-
-
-
+ +
+
{{ radios(form.branding_type) }} +
+
{{ branding_radios(form.branding_style, branding_dict=branding_dict) }} +
+
+
+
{{ page_footer( - 'Save', + 'Preview', back_link=url_for('.service_settings', service_id=current_service.id), back_link_text='Back to settings' ) }} - +
-
+ {% endblock %} From c7610f359a060fd6c052dbdf326147d12f748f3d Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Fri, 3 Aug 2018 12:43:35 +0100 Subject: [PATCH 3/8] improve range of params email page can take Can now deal with no query params and has a default format of the GOV.UK branding. --- app/main/views/index.py | 125 ++++++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 42 deletions(-) diff --git a/app/main/views/index.py b/app/main/views/index.py index 77ba91b94..f0ee6f746 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -1,15 +1,22 @@ -from flask import abort, redirect, render_template, request, url_for +from flask import ( + abort, + make_response, + redirect, + render_template, + request, + url_for, +) from flask_login import current_user, login_required from notifications_utils.international_billing_rates import ( INTERNATIONAL_BILLING_RATES, ) from notifications_utils.template import HTMLEmailTemplate -from app import convert_to_boolean +from app import email_branding_client from app.main import main from app.main.forms import SearchTemplatesForm from app.main.views.sub_navigation_dictionaries import features_nav -from app.utils import AgreementInfo +from app.utils import AgreementInfo, get_cdn_domain @main.route('/') @@ -73,45 +80,79 @@ def design_content(): @main.route('/_email') def email_template(): - return str(HTMLEmailTemplate({'subject': 'foo', 'content': ( - 'Lorem Ipsum is simply dummy text of the printing and typesetting ' - 'industry.\n\nLorem Ipsum has been the industry’s standard dummy ' - 'text ever since the 1500s, when an unknown printer took a galley ' - 'of type and scrambled it to make a type specimen book. ' - '\n\n' - '# History' - '\n\n' - 'It has ' - 'survived not only' - '\n\n' - '* five centuries' - '\n' - '* but also the leap into electronic typesetting' - '\n\n' - 'It was ' - 'popularised in the 1960s with the release of Letraset sheets ' - 'containing Lorem Ipsum passages, and more recently with desktop ' - 'publishing software like Aldus PageMaker including versions of ' - 'Lorem Ipsum.' - '\n\n' - '^ It is a long established fact that a reader will be distracted ' - 'by the readable content of a page when looking at its layout.' - '\n\n' - 'The point of using Lorem Ipsum is that it has a more-or-less ' - 'normal distribution of letters, as opposed to using ‘Content ' - 'here, content here’, making it look like readable English.' - '\n\n\n' - '1. One' - '\n' - '2. Two' - '\n' - '10. Three' - '\n\n' - 'This is an example of an email sent using GOV.UK Notify.' - '\n\n' - 'https://www.notifications.service.gov.uk' - )}, govuk_banner=convert_to_boolean(request.args.get('govuk_banner', True)) - )) + branding_type = request.args.get('branding_type', 'govuk') + branding_style = request.args.get('branding_style', 'None') + + if branding_type == 'govuk' or branding_style == 'None': + brand_name = None + brand_colour = None + brand_logo = None + govuk_banner = True + brand_banner = False + else: + email_branding = email_branding_client.get_email_branding(branding_style)['email_branding'] + brand_name = email_branding['name'] + brand_colour = email_branding['colour'] + brand_logo = 'https://{}/{}'.format(get_cdn_domain(), email_branding['logo']) + govuk_banner = branding_type in ['govuk', 'both'] + brand_banner = branding_type == 'org_banner' + + template = { + 'subject': 'foo', + 'content': ( + 'Lorem Ipsum is simply dummy text of the printing and typesetting ' + 'industry.\n\nLorem Ipsum has been the industry’s standard dummy ' + 'text ever since the 1500s, when an unknown printer took a galley ' + 'of type and scrambled it to make a type specimen book. ' + '\n\n' + '# History' + '\n\n' + 'It has ' + 'survived not only' + '\n\n' + '* five centuries' + '\n' + '* but also the leap into electronic typesetting' + '\n\n' + 'It was ' + 'popularised in the 1960s with the release of Letraset sheets ' + 'containing Lorem Ipsum passages, and more recently with desktop ' + 'publishing software like Aldus PageMaker including versions of ' + 'Lorem Ipsum.' + '\n\n' + '^ It is a long established fact that a reader will be distracted ' + 'by the readable content of a page when looking at its layout.' + '\n\n' + 'The point of using Lorem Ipsum is that it has a more-or-less ' + 'normal distribution of letters, as opposed to using ‘Content ' + 'here, content here’, making it look like readable English.' + '\n\n\n' + '1. One' + '\n' + '2. Two' + '\n' + '10. Three' + '\n\n' + 'This is an example of an email sent using GOV.UK Notify.' + '\n\n' + 'https://www.notifications.service.gov.uk' + ) + } + + if not bool(request.args): + resp = make_response(str(HTMLEmailTemplate(template))) + else: + resp = make_response(str(HTMLEmailTemplate( + template, + govuk_banner=govuk_banner, + brand_name=brand_name, + brand_colour=brand_colour, + brand_logo=brand_logo, + brand_banner=brand_banner, + ))) + + resp.headers['X-Frame-Options'] = 'SAMEORIGIN' + return resp @main.route('/documentation') From 386105954e5e0e0662eba23b25009b6cc27221a0 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Fri, 3 Aug 2018 16:22:13 +0100 Subject: [PATCH 4/8] Add JS to embed email preview into page --- app/assets/javascripts/emailPreviewPane.js | 43 ++++++++++++++++++++++ gulpfile.babel.js | 1 + 2 files changed, 44 insertions(+) create mode 100644 app/assets/javascripts/emailPreviewPane.js diff --git a/app/assets/javascripts/emailPreviewPane.js b/app/assets/javascripts/emailPreviewPane.js new file mode 100644 index 000000000..7e42e99ba --- /dev/null +++ b/app/assets/javascripts/emailPreviewPane.js @@ -0,0 +1,43 @@ +(function () { + + 'use strict'; + + const root = this, + $ = this.jQuery; + + let branding_type = $('.multiple-choice input[name="branding_type"]:checked'); + let branding_style = $('.multiple-choice input[name="branding_style"]:checked'); + + if (!branding_type.length || !branding_style.length) { return; } + + branding_type = branding_type.val(); + branding_style = branding_style.val(); + + const $paneWrapper = $('
'); + const $form = $('form'); + const $previewPane = $(''); + + function buildQueryString () { + return $.map(arguments, (val, idx) => encodeURI(val[0]) + '=' + encodeURI(val[1])).join('&'); + } + + function setPreviewPane (e) { + const $target = $(e.target); + if ($target.attr('name') == 'branding_type') { + branding_type = $target.val(); + } + if ($target.attr('name') == 'branding_style') { + branding_style = $target.val(); + } + $previewPane.attr('src', '/_email?' + buildQueryString(['branding_type', branding_type], ['branding_style', branding_style])); + } + + $paneWrapper.append($previewPane); + $form.find('.grid-row').eq(0).prepend($paneWrapper); + $form.attr('action', location.pathname.replace(/set-email-branding$/, 'preview-email-branding')); + $form.find('button[type="submit"]').text('Save'); + + $('fieldset').on('change', 'input[name="branding_type"], input[name="branding_style"]', setPreviewPane); +})(); diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 60b66f28a..a27093059 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -74,6 +74,7 @@ gulp.task('javascripts', () => gulp paths.src + 'javascripts/preventDuplicateFormSubmissions.js', paths.src + 'javascripts/fullscreenTable.js', paths.src + 'javascripts/conditionalRadios.js', + paths.src + 'javascripts/emailPreviewPane.js', paths.src + 'javascripts/main.js' ]) .pipe(plugins.prettyerror()) From eb72f43b3f125769796f4cee5ee0a5af4bdcf17d Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Fri, 3 Aug 2018 17:39:58 +0100 Subject: [PATCH 5/8] Fixes for tests broken by changes - corrects target page for set_email_branding to new preview step instead of itself - removed check for helper method being called in email page test - updates expected result for test of global headers to include changes to `frame-src` - updates navigation config with brand preview page --- app/navigation.py | 4 ++++ tests/app/main/views/test_email_preview.py | 2 -- tests/app/main/views/test_headers.py | 4 ++-- tests/app/main/views/test_service_settings.py | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/navigation.py b/app/navigation.py index ebc917a93..b4467cdcf 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -218,6 +218,7 @@ class HeaderNavigation(Navigation): 'service_letter_contact_details', 'service_name_change', 'service_name_change_confirm', + 'service_preview_email_branding', 'service_set_auth_type', 'service_set_basic_view', 'service_set_contact_link', @@ -339,6 +340,7 @@ class MainNavigation(Navigation): 'service_letter_contact_details', 'service_name_change', 'service_name_change_confirm', + 'service_preview_email_branding', 'service_set_auth_type', 'service_set_basic_view', 'service_set_contact_link', @@ -661,6 +663,7 @@ class CaseworkNavigation(Navigation): 'service_letter_contact_details', 'service_name_change', 'service_name_change_confirm', + 'service_preview_email_branding', 'service_set_auth_type', 'service_set_basic_view', 'service_set_contact_link', @@ -887,6 +890,7 @@ class OrgNavigation(Navigation): 'service_letter_contact_details', 'service_name_change', 'service_name_change_confirm', + 'service_preview_email_branding', 'service_set_auth_type', 'service_set_basic_view', 'service_set_contact_link', diff --git a/tests/app/main/views/test_email_preview.py b/tests/app/main/views/test_email_preview.py index 5b7923ce6..05fb3f32b 100644 --- a/tests/app/main/views/test_email_preview.py +++ b/tests/app/main/views/test_email_preview.py @@ -10,11 +10,9 @@ from flask import url_for ) def test_renders(client, mocker, query_args, result): - mock_convert_to_boolean = mocker.patch('app.main.views.index.convert_to_boolean') mocker.patch('app.main.views.index.HTMLEmailTemplate.__str__', return_value='rendered') response = client.get(url_for('main.email_template', **query_args)) assert response.status_code == 200 assert response.get_data(as_text=True) == 'rendered' - mock_convert_to_boolean.assert_called_once_with(result) diff --git a/tests/app/main/views/test_headers.py b/tests/app/main/views/test_headers.py index 94558d431..1ef2a794c 100644 --- a/tests/app/main/views/test_headers.py +++ b/tests/app/main/views/test_headers.py @@ -14,7 +14,7 @@ def test_owasp_useful_headers_set(client, mocker): "object-src 'self';" "font-src 'self' data:;" "img-src 'self' *.google-analytics.com *.notifications.service.gov.uk static-logos.test.com data:;" - "frame-src www.youtube.com;" + "frame-src 'self' www.youtube.com;" ) @@ -31,5 +31,5 @@ def test_headers_non_ascii_characters_are_replaced(client, mocker): "object-src 'self';" "font-src 'self' data:;" "img-src 'self' *.google-analytics.com *.notifications.service.gov.uk static-logos??.test.com data:;" - "frame-src www.youtube.com;" + "frame-src 'self' www.youtube.com;" ) diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 812891bb5..87feb5b66 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -1785,7 +1785,8 @@ def test_should_set_branding_and_organisations( } ) assert response.status_code == 302 - assert response.location == url_for('main.service_settings', service_id=service_one['id'], _external=True) + assert response.location == url_for('main.service_preview_email_branding', + service_id=service_one['id'], _external=True) mock_get_all_email_branding.assert_called_once_with() mock_update_service.assert_called_once_with( From d8d48ea8012a25ceb4908f60cb840fe4bed6364f Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 6 Aug 2018 15:32:50 +0100 Subject: [PATCH 6/8] Add tests for preview step Includes moving code that tests an API call to update the service with the new branding to the preview step submission. Also includes a change to the HTTP params sent by the set-email-branding step. Think it was missed out of this PR: https://github.com/alphagov/notifications-admin/pull/1843 ...so was never changed to 'branding_style'. --- tests/app/main/views/test_service_settings.py | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 87feb5b66..45cacf614 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -1769,7 +1769,7 @@ def test_should_show_organisations( app.service_api_client.get_service.assert_called_once_with(service_one['id']) -def test_should_set_branding_and_organisations( +def test_should_send_branding_and_organisations_to_preview( logged_in_platform_admin_client, service_one, mock_get_all_email_branding, @@ -1781,18 +1781,58 @@ def test_should_set_branding_and_organisations( ), data={ 'branding_type': 'org', - 'organisation': '1' + 'branding_style': '1' } ) assert response.status_code == 302 assert response.location == url_for('main.service_preview_email_branding', - service_id=service_one['id'], _external=True) + service_id=service_one['id'], branding_type='org', + branding_style='1', _external=True) mock_get_all_email_branding.assert_called_once_with() + + +def test_should_preview_email_branding( + logged_in_platform_admin_client, + service_one, +): + response = logged_in_platform_admin_client.get(url_for( + 'main.service_preview_email_branding', service_id=service_one['id'], + branding_type='org', branding_style='1' + )) + assert response.status_code == 200 + page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') + iframe = page.find('iframe', attrs={"class": "email-branding-preview"}) + + assert page.find('input', attrs={"id": "branding_type"})['value'] == 'org' + assert page.find('input', attrs={"id": "branding_style"})['value'] == '1' + assert iframe and iframe['src'] == '/_email?branding_type=org&branding_style=1' + + app.service_api_client.get_service.assert_called_once_with(service_one['id']) + + +def test_should_set_branding_and_organisations( + logged_in_platform_admin_client, + service_one, + mock_update_service, +): + response = logged_in_platform_admin_client.post( + url_for( + 'main.service_preview_email_branding', service_id=service_one['id'] + ), + data={ + 'branding_type': 'org', + 'branding_style': '1' + } + ) + assert response.status_code == 302 + assert response.location == url_for('main.service_settings', + service_id=service_one['id'], _external=True) + mock_update_service.assert_called_once_with( service_one['id'], branding='org', - email_branding=None + email_branding='1' ) From e7ead052ce53f00cabb1e3576d61208a497007ea Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Tue, 7 Aug 2018 16:34:04 +0100 Subject: [PATCH 7/8] Add tests for /_email page --- tests/app/main/views/test_email_preview.py | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/app/main/views/test_email_preview.py b/tests/app/main/views/test_email_preview.py index 05fb3f32b..3d8fc34f7 100644 --- a/tests/app/main/views/test_email_preview.py +++ b/tests/app/main/views/test_email_preview.py @@ -1,4 +1,7 @@ +import re + import pytest +from bs4 import BeautifulSoup from flask import url_for @@ -16,3 +19,67 @@ def test_renders(client, mocker, query_args, result): assert response.status_code == 200 assert response.get_data(as_text=True) == 'rendered' + + +def test_displays_govuk_branding_by_default(client): + + response = client.get(url_for('main.email_template')) + + page = BeautifulSoup(response.data.decode("utf-8"), "html.parser") + + assert response.status_code == 200 + + assert page.find("a", attrs={"href": "https://www.gov.uk"}) + + +def test_displays_govuk_branding(client): + + response = client.get(url_for('main.email_template', branding_type="govuk", branding_style="1")) + + page = BeautifulSoup(response.data.decode("utf-8"), "html.parser") + + assert response.status_code == 200 + + assert page.find("a", attrs={"href": "https://www.gov.uk"}) + + +def test_displays_both_branding(client, mock_get_email_branding): + + response = client.get(url_for('main.email_template', branding_type="both", branding_style="1")) + + page = BeautifulSoup(response.data.decode("utf-8"), "html.parser") + + assert response.status_code == 200 + mock_get_email_branding.assert_called_once_with('1') + + assert page.find("a", attrs={"href": "https://www.gov.uk"}) + assert page.find("img", attrs={"src": re.compile("example.png$")}) + + +def test_displays_org_branding(client, mock_get_email_branding): + + response = client.get(url_for('main.email_template', branding_type="org", branding_style="1")) + + page = BeautifulSoup(response.data.decode("utf-8"), "html.parser") + + assert response.status_code == 200 + mock_get_email_branding.assert_called_once_with('1') + + assert not page.find("a", attrs={"href": "https://www.gov.uk"}) + assert page.find("img", attrs={"src": re.compile("example.png")}) + assert not page.select("body > table > tr > td[bgcolor='#f00']") + + +def test_displays_org_branding_with_banner(client, mock_get_email_branding): + + response = client.get(url_for('main.email_template', branding_type="org_banner", + branding_style="1")) + + page = BeautifulSoup(response.data.decode("utf-8"), "html.parser") + + assert response.status_code == 200 + mock_get_email_branding.assert_called_once_with('1') + + assert not page.find("a", attrs={"href": "https://www.gov.uk"}) + assert page.find("img", attrs={"src": re.compile("example.png")}) + assert page.select("body > table > tr > td[bgcolor='#f00']") From d931966e5a333ed50e78fa1c8687cf1a0a4fd738 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Wed, 8 Aug 2018 13:38:59 +0100 Subject: [PATCH 8/8] Add CSS for email preview iframe --- app/assets/stylesheets/components/email-preview-pane.scss | 7 +++++++ app/assets/stylesheets/main.scss | 1 + 2 files changed, 8 insertions(+) create mode 100644 app/assets/stylesheets/components/email-preview-pane.scss diff --git a/app/assets/stylesheets/components/email-preview-pane.scss b/app/assets/stylesheets/components/email-preview-pane.scss new file mode 100644 index 000000000..807069631 --- /dev/null +++ b/app/assets/stylesheets/components/email-preview-pane.scss @@ -0,0 +1,7 @@ +.email-branding-preview { + width: 100%; + box-sizing: border-box; + border: solid 1px $border-colour; + min-height: 200px; + margin-bottom: $gutter +} diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index 43edbaed5..9f5955cae 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -61,6 +61,7 @@ $path: '/static/images/'; @import 'components/conditional-radios'; @import 'components/vendor/breadcrumbs'; @import 'components/vendor/responsive-embed'; +@import 'components/email-preview-pane'; @import 'views/dashboard'; @import 'views/users';