From ad1df0ebeb88cc05668fe854b79114edf3011acb Mon Sep 17 00:00:00 2001 From: Joshua Pan Date: Wed, 15 Aug 2018 16:26:55 -0700 Subject: [PATCH] settings: Add support for customizing the top-left logo. This adds a new realm_logo field, which is a horizontal-format logo to be displayed in the top-left corner of the webapp, and any other places where we might want a wide-format branding of the organization. Tweaked significantly by tabbott to rebase, fix styling, etc. Fixing the styling of this feature's loading indicator caused me to notice the loading indicator for the realm_icon feature was also ugly, so I fixed that too. Fixes #7995. --- .eslintrc.json | 1 + frontend_tests/node_tests/settings_org.js | 5 + static/js/admin.js | 2 + static/js/bundles/app.js | 1 + static/js/realm_logo.js | 53 ++++++ static/js/server_events_dispatch.js | 4 + static/js/settings_org.js | 43 +++++ static/styles/settings.scss | 35 +++- static/styles/zulip.scss | 21 +++ .../organization-profile-admin.handlebars | 30 +++- templates/zerver/api/server-settings.md | 7 +- templates/zerver/app/navbar.html | 2 +- zerver/context_processors.py | 4 + zerver/lib/actions.py | 17 ++ zerver/lib/events.py | 4 + zerver/lib/realm_logo.py | 13 ++ zerver/lib/upload.py | 68 ++++++++ .../migrations/0196_add_realm_logo_fields.py | 25 +++ zerver/models.py | 13 ++ zerver/openapi/zulip.yaml | 9 +- zerver/tests/test_auth_backends.py | 1 + zerver/tests/test_home.py | 3 + zerver/tests/test_upload.py | 165 ++++++++++++++++++ zerver/views/auth.py | 1 + zerver/views/realm_logo.py | 58 ++++++ zproject/settings.py | 2 + zproject/urls.py | 6 + 27 files changed, 585 insertions(+), 8 deletions(-) create mode 100644 static/js/realm_logo.js create mode 100644 zerver/lib/realm_logo.py create mode 100644 zerver/migrations/0196_add_realm_logo_fields.py create mode 100644 zerver/views/realm_logo.py diff --git a/.eslintrc.json b/.eslintrc.json index 0e81209754..6f9f13e849 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -117,6 +117,7 @@ "pygments_data": false, "reactions": false, "realm_icon": false, + "realm_logo": false, "recent_senders": false, "reload": false, "reload_state": false, diff --git a/frontend_tests/node_tests/settings_org.js b/frontend_tests/node_tests/settings_org.js index 1cf0ee43b2..bcc31aa6c4 100644 --- a/frontend_tests/node_tests/settings_org.js +++ b/frontend_tests/node_tests/settings_org.js @@ -55,6 +55,10 @@ const _ui_report = { }, }; +const _realm_logo = { + build_realm_logo_widget: noop, +}; + set_global('channel', _channel); set_global('csrf_token', 'token-stub'); set_global('FormData', _FormData); @@ -63,6 +67,7 @@ set_global('loading', _loading); set_global('overlays', _overlays); set_global('page_params', _page_params); set_global('realm_icon', _realm_icon); +set_global('realm_logo', _realm_logo); set_global('templates', _templates); set_global('ui_report', _ui_report); diff --git a/static/js/admin.js b/static/js/admin.js index 0804e2a081..56f4d89aa3 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -49,6 +49,8 @@ exports.build_page = function () { is_guest: page_params.is_guest, realm_icon_source: page_params.realm_icon_source, realm_icon_url: page_params.realm_icon_url, + realm_logo_source: page_params.realm_logo_source, + realm_logo_url: page_params.realm_logo_url, realm_mandatory_topics: page_params.realm_mandatory_topics, realm_send_welcome_emails: page_params.realm_send_welcome_emails, realm_default_twenty_four_hour_time: page_params.realm_default_twenty_four_hour_time, diff --git a/static/js/bundles/app.js b/static/js/bundles/app.js index 0eac14b8c5..5bce15910f 100644 --- a/static/js/bundles/app.js +++ b/static/js/bundles/app.js @@ -166,6 +166,7 @@ import "js/templates.js"; import "js/upload_widget.js"; import "js/avatar.js"; import "js/realm_icon.js"; +import "js/realm_logo.js"; import 'js/reminder.js'; import 'js/confirm_dialog.js'; import "js/settings_account.js"; diff --git a/static/js/realm_logo.js b/static/js/realm_logo.js new file mode 100644 index 0000000000..906e4e83b0 --- /dev/null +++ b/static/js/realm_logo.js @@ -0,0 +1,53 @@ +/* eslint indent: "off" */ +var realm_logo = (function () { + + var exports = {}; + + exports.build_realm_logo_widget = function (upload_function) { + var get_file_input = function () { + return $('#realm_logo_file_input').expectOne(); + }; + + if (page_params.realm_logo_source === 'D') { + $("#realm_logo_delete_button").hide(); + } else { + $("#realm_logo_delete_button").show(); + } + $("#realm_logo_delete_button").on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + channel.del({ + url: '/json/realm/logo', + }); + }); + + return upload_widget.build_direct_upload_widget( + get_file_input, + $("#realm_logo_file_input_error").expectOne(), + $("#realm_logo_upload_button").expectOne(), + upload_function, + page_params.max_logo_file_size + ); + }; + + exports.rerender = function () { + $("#realm-settings-logo").attr("src", page_params.realm_logo_url); + $("#realm-logo").attr("src", page_params.realm_logo_url); + if (page_params.realm_logo_source === 'U') { + $("#realm_logo_delete_button").show(); + } else { + $("#realm_logo_delete_button").hide(); + // Need to clear input because of a small edge case + // where you try to upload the same image you just deleted. + var file_input = $("#realm_logo_file_input"); + file_input.val(''); + } + }; + + return exports; +}()); + +if (typeof module !== 'undefined') { + module.exports = realm_logo; +} +window.realm_logo = realm_logo; diff --git a/static/js/server_events_dispatch.js b/static/js/server_events_dispatch.js index c239828a79..de466d3eae 100644 --- a/static/js/server_events_dispatch.js +++ b/static/js/server_events_dispatch.js @@ -160,6 +160,10 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) { if (electron_bridge !== undefined) { electron_bridge.send_event('realm_icon_url', event.data.icon_url); } + } else if (event.op === 'update_dict' && event.property === 'logo') { + page_params.realm_logo_url = event.data.logo_url; + page_params.realm_logo_source = event.data.logo_source; + realm_logo.rerender(); } else if (event.op === 'deactivated') { window.location.href = "/accounts/deactivated/"; } diff --git a/static/js/settings_org.js b/static/js/settings_org.js index af3b439108..09a158b75f 100644 --- a/static/js/settings_org.js +++ b/static/js/settings_org.js @@ -1054,8 +1054,11 @@ exports.build_page = function () { form_data.append('file-' + i, file); }); + var error_field = $("#realm_icon_file_input_error"); + error_field.hide(); var spinner = $("#upload_icon_spinner").expectOne(); loading.make_indicator(spinner, {text: i18n.t("Uploading icon.")}); + $("#upload_icon_button_text").expectOne().hide(); channel.post({ url: '/json/realm/icon', @@ -1065,12 +1068,52 @@ exports.build_page = function () { contentType: false, success: function () { loading.destroy_indicator($("#upload_icon_spinner")); + $("#upload_icon_button_text").expectOne().show(); + }, + error: function (xhr) { + loading.destroy_indicator($("#upload_logo_spinner")); + $("#upload_logo_button_text").expectOne().show(); + ui_report.error("", xhr, error_field); }, }); } realm_icon.build_realm_icon_widget(upload_realm_icon); + function upload_realm_logo(file_input) { + var form_data = new FormData(); + + form_data.append('csrfmiddlewaretoken', csrf_token); + jQuery.each(file_input[0].files, function (i, file) { + form_data.append('file-' + i, file); + }); + + var error_field = $("#realm_logo_file_input_error"); + error_field.hide(); + var spinner = $("#upload_logo_spinner").expectOne(); + loading.make_indicator(spinner, {text: i18n.t("Uploading logo.")}); + $("#upload_logo_button_text").expectOne().hide(); + + channel.post({ + url: '/json/realm/logo', + data: form_data, + cache: false, + processData: false, + contentType: false, + success: function () { + loading.destroy_indicator($("#upload_logo_spinner")); + $("#upload_logo_button_text").expectOne().show(); + }, + error: function (xhr) { + loading.destroy_indicator($("#upload_logo_spinner")); + $("#upload_logo_button_text").expectOne().show(); + ui_report.error("", xhr, error_field); + }, + }); + + } + realm_logo.build_realm_logo_widget(upload_realm_logo); + $('#deactivate_realm_button').on('click', function (e) { if (!overlays.is_modal_open()) { e.preventDefault(); diff --git a/static/styles/settings.scss b/static/styles/settings.scss index 6801c83f90..25c45d087b 100644 --- a/static/styles/settings.scss +++ b/static/styles/settings.scss @@ -81,10 +81,15 @@ label { } .user-avatar-section, +.realm-logo-section, .realm-icon-section { position: relative; } +.realm-logo-block { + margin-bottom: 10px; +} + .user-avatar-section { float: right; } @@ -101,8 +106,9 @@ label { } .user-avatar-section .inline-block, +.realm-logo-section .inline-block, .realm-icon-section .inline-block { - margin: 10px 20px 0px 0px; + margin: 0px 20px 0px 0px; vertical-align: top; border-radius: 4px; @@ -458,6 +464,11 @@ input[type=checkbox] + .inline-block { margin-top: 0px; } +.realm-logo-section { + margin-top: 10px; + margin-bottom: 20px; +} + .realm-icon-section { margin-bottom: 20px; } @@ -937,6 +948,7 @@ input[type=checkbox].inline-block { } #upload_avatar_spinner, +#upload_logo_spinner, #upload_icon_spinner { font-size: 14px; margin: auto; @@ -1071,6 +1083,15 @@ input[type=checkbox].inline-block { height: 100px; } +#realm-settings-logo { + border-radius: 5px; + box-shadow: 0px 0px 10px hsla(0, 0%, 0%, 0.2); + /* We allow actual images up to 800x100 in the main display, but the + settings UI looks bad beyond ~730px, so we limit the width here */ + height: 100px; + max-width: 720px; +} + .invite-user-link i { text-decoration: none; margin-right: 5px; @@ -1636,6 +1657,13 @@ input[type=text]#settings_search { margin: 0 0 0 5px; } +@media (max-width: 1023px) { + #realm-settings-logo { + max-width: 600px; + height: 75px; + } +} + @media (max-width: 953px) { .user-avatar-section, .realm-icon-section { @@ -1655,6 +1683,11 @@ input[type=text]#settings_search { .subsection-failed-status p { margin: 5px 0 0 0; } + + #realm-settings-logo { + max-width: 400px; + height: 50px; + } } @media (max-width: 786px) { diff --git a/static/styles/zulip.scss b/static/styles/zulip.scss index d3e8662352..84c61b0a3a 100644 --- a/static/styles/zulip.scss +++ b/static/styles/zulip.scss @@ -2157,6 +2157,27 @@ div.floating_recipient { display: inline-block; } +.settings-section .realm-icon-section .loading_indicator_text, +.settings-section .realm-logo-section .loading_indicator_text { + font-size: 14px; + font-weight: 400; + vertical-align: middle; + line-height: 20px; + display: inline-block; + float: none; + margin-top: 0px; + margin-left: -12px; +} + +.settings-section .realm-logo-section .loading_indicator_spinner, +.settings-section .realm-icon-section .loading_indicator_spinner { + width: 10%; + height: 16px; + margin-top: 2px; + vertical-align: middle; + display: inline-block; +} + .settings-section #default_language { text-decoration: none; } diff --git a/static/templates/settings/organization-profile-admin.handlebars b/static/templates/settings/organization-profile-admin.handlebars index bd22ff67d4..0f2d4228d2 100644 --- a/static/templates/settings/organization-profile-admin.handlebars +++ b/static/templates/settings/organization-profile-admin.handlebars @@ -23,23 +23,47 @@

{{t "Organization avatar" }}

+

{{t "A square logo used to brand your Zulip organization." }}

-
+
-
+ id="realm_icon_upload_button"> + {{t 'Upload new icon' }} + +
+

{{t "Organization logo" }}

+

{{t "A wide image, replacing the Zulip logo in the upper left corner of the Zulip apps." }}

+ +
+
+ + +
+
+
+ + +
+
+

{{t "Deactivate organization" }}

diff --git a/templates/zerver/api/server-settings.md b/templates/zerver/api/server-settings.md index 0912c79a81..2a2f986999 100644 --- a/templates/zerver/api/server-settings.md +++ b/templates/zerver/api/server-settings.md @@ -50,7 +50,12 @@ curl {{ api_url }}/v1/server_settings \ enabled with a username and password combination. * `realm_uri`: the organization's canonical URI. * `realm_name`: the organization's name (for display purposes). -* `realm_icon`: the URI of the organization's icon (usually a logo). +* `realm_icon`: the URI of the organization's logo as a square image, + used for identifying the organization in small locations in the + mobile and desktop apps. +* `realm_logo`: the URI of the organization's logo as a horizontal + format image (displayed in the top-left corner of the logged-in + webapp). * `realm_description`: HTML description of the organization, as configured by the [organization profile](/help/create-your-organization-profile). diff --git a/templates/zerver/app/navbar.html b/templates/zerver/app/navbar.html index fe03922a17..160ac2dd0e 100644 --- a/templates/zerver/app/navbar.html +++ b/templates/zerver/app/navbar.html @@ -31,7 +31,7 @@