diff --git a/.eslintrc.json b/.eslintrc.json index a8276d147f..325c8618be 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -61,6 +61,7 @@ "settings_filters": false, "settings_invites": false, "settings_user_groups": false, + "settings_profile_fields": false, "settings": false, "resize": false, "loading": false, diff --git a/frontend_tests/casper_tests/10-admin.js b/frontend_tests/casper_tests/10-admin.js index 5d06969568..e46b09f01a 100644 --- a/frontend_tests/casper_tests/10-admin.js +++ b/frontend_tests/casper_tests/10-admin.js @@ -184,6 +184,55 @@ casper.then(function () { }); }); +// Test custom profile fields +casper.test.info("Testing custom profile fields"); +casper.thenClick("li[data-section='profile-field-settings']"); +casper.then(function () { + casper.waitUntilVisible('.admin-profile-field-form', function () { + casper.fill('form.admin-profile-field-form', { + name: 'Teams', + field_type: '3', + }); + casper.click('form.admin-profile-field-form button.button'); + }); +}); + +casper.then(function () { + casper.waitUntilVisible('div#admin-profile-field-status', function () { + casper.test.assertSelectorHasText('div#admin-profile-field-status', + 'Custom profile field added!'); + casper.test.assertSelectorHasText('.profile-field-row span.profile_field_name', 'Teams'); + casper.test.assertSelectorHasText('.profile-field-row span.profile_field_type', 'Short Text'); + casper.click('.profile-field-row button.open-edit-form'); + }); +}); + +casper.then(function () { + casper.waitUntilVisible('tr.profile-field-form form', function () { + casper.fill('tr.profile-field-form form.name-setting', { + name: 'team', + }); + casper.click('tr.profile-field-form button.submit'); + }); +}); + +casper.then(function () { + casper.waitUntilVisible('div#admin-profile-field-status', function () { + casper.test.assertSelectorHasText('div#admin-profile-field-status', + 'Custom profile field updated!'); + casper.test.assertSelectorHasText('.profile-field-row span.profile_field_name', 'team'); + casper.test.assertSelectorHasText('.profile-field-row span.profile_field_type', 'Short Text'); + casper.click('.profile-field-row button.delete'); + }); +}); + +casper.then(function () { + casper.waitUntilVisible('div#admin-profile-field-status', function () { + casper.test.assertSelectorHasText('div#admin-profile-field-status', + 'Custom profile field deleted!'); + }); +}); + // Test custom realm filters casper.then(function () { casper.click("li[data-section='filter-settings']"); diff --git a/frontend_tests/node_tests/dispatch.js b/frontend_tests/node_tests/dispatch.js index 6102c868c5..056e459166 100644 --- a/frontend_tests/node_tests/dispatch.js +++ b/frontend_tests/node_tests/dispatch.js @@ -476,6 +476,14 @@ var event_fixtures = { type: 'delete_message', message_id: 1337, }, + + custom_profile_fields: { + type: 'custom_profile_fields', + fields: [ + {id: 1, name: 'teams', type: 1}, + {id: 2, name: 'hobbies', type: 1}, + ], + }, }; function assert_same(actual, expected) { @@ -496,6 +504,16 @@ with_overrides(function (override) { }); +with_overrides(function (override) { + // custom profile fields + var event = event_fixtures.custom_profile_fields; + override('settings_profile_fields.populate_profile_fields', noop); + override('settings_profile_fields.report_success', noop); + dispatch(event); + assert_same(global.page_params.custom_profile_fields, event.fields); + +}); + with_overrides(function (override) { // default_streams var event = event_fixtures.default_streams; diff --git a/frontend_tests/node_tests/templates.js b/frontend_tests/node_tests/templates.js index 2ff2fd9cf3..fd754c052b 100644 --- a/frontend_tests/node_tests/templates.js +++ b/frontend_tests/node_tests/templates.js @@ -195,6 +195,55 @@ function render(template_name, args) { assert.equal(emoji_url.attr('src'), 'http://emojipedia-us.s3.amazonaws.com/cache/46/7f/467fe69069c408e07517621f263ea9b5.png'); }()); +(function admin_profile_field_list() { + + // When the logged in user is admin + var args = { + profile_field: { + name: "teams", + type: "Long Text", + }, + can_modify: true, + }; + + var html = ''; + html += ''; + html += render('admin_profile_field_list', args); + html += ''; + + var field_name = $(html).find('tr.profile-field-row:first span.profile_field_name'); + var field_type = $(html).find('tr.profile-field-row:first span.profile_field_type'); + var td = $(html).find('tr.profile-field-row:first td'); + + assert.equal(field_name.text(), 'teams'); + assert.equal(field_type.text(), 'Long Text'); + assert.equal(td.length, 3); + + // When the logged in user is not admin + args = { + profile_field: { + name: "teams", + type: "Long Text", + }, + can_modify: false, + }; + + html = ''; + html += ''; + html += render('admin_profile_field_list', args); + html += ''; + + global.write_test_output('admin_profile_field_list', html); + + field_name = $(html).find('tr.profile-field-row:first span.profile_field_name'); + field_type = $(html).find('tr.profile-field-row:first span.profile_field_type'); + td = $(html).find('tr.profile-field-row:first td'); + + assert.equal(field_name.text(), 'teams'); + assert.equal(field_type.text(), 'Long Text'); + assert.equal(td.length, 2); +}()); + (function admin_filter_list() { // When the logged in user is admin diff --git a/static/js/admin.js b/static/js/admin.js index 4a1caa6ffd..9a4f0ba3b2 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -20,12 +20,15 @@ exports.show_or_hide_menu_item = function () { .find("input:not(.search), button, select").attr("disabled", true); $(".organization-box [data-name='filter-settings']") .find("input, button, select").attr("disabled", true); + $(".organization-box [data-name='profile-field-settings']") + .find("input, button, select").attr("disabled", true); $(".control-label-disabled").css("color", "#333333"); } }; function _setup_page() { var options = { + custom_profile_field_types: page_params.custom_profile_field_types, realm_name: page_params.realm_name, realm_description: page_params.realm_description, realm_restricted_to_domain: page_params.realm_restricted_to_domain, diff --git a/static/js/admin_sections.js b/static/js/admin_sections.js index 240e332682..0a507621c3 100644 --- a/static/js/admin_sections.js +++ b/static/js/admin_sections.js @@ -35,6 +35,9 @@ exports.load_admin_section = function (name) { case 'user-groups-admin': section = 'user-groups'; break; + case 'profile-field-settings': + section = 'profile-fields'; + break; default: blueslip.error('Unknown admin id ' + name); return; @@ -68,6 +71,9 @@ exports.load_admin_section = function (name) { case 'user-groups': settings_user_groups.set_up(); break; + case 'profile-fields': + settings_profile_fields.set_up(); + break; default: blueslip.error('programming error for section ' + section); return; @@ -85,6 +91,7 @@ exports.reset_sections = function () { settings_filters.reset(); settings_invites.reset(); settings_user_groups.reset(); + settings_profile_fields.reset(); }; return exports; diff --git a/static/js/server_events_dispatch.js b/static/js/server_events_dispatch.js index 9f21a8f023..49c711a073 100644 --- a/static/js/server_events_dispatch.js +++ b/static/js/server_events_dispatch.js @@ -160,6 +160,12 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) { settings_filters.populate_filters(page_params.realm_filters); break; + case 'custom_profile_fields': + page_params.custom_profile_fields = event.fields; + settings_profile_fields.populate_profile_fields(page_params.custom_profile_fields); + settings_profile_fields.report_success(event.op); + break; + case 'realm_domains': var i; if (event.op === 'add') { diff --git a/static/js/settings.js b/static/js/settings.js index 21430877e8..acca3b3376 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -108,6 +108,7 @@ function _setup_page() { "filter-settings": i18n.t("Filter settings"), "invites-list-admin": i18n.t("Invitations"), "user-groups-admin": i18n.t("User groups"), + "profile-field-settings": i18n.t("Profile field settings"), }; } diff --git a/static/js/settings_profile_fields.js b/static/js/settings_profile_fields.js new file mode 100644 index 0000000000..27eb21bcbd --- /dev/null +++ b/static/js/settings_profile_fields.js @@ -0,0 +1,159 @@ +var settings_profile_fields = (function () { + +var exports = {}; + +var meta = { + loaded: false, +}; + +function field_type_id_to_string(type_id) { + var name = _.find(page_params.custom_profile_field_types, function (type) { + return type[0] === type_id; + })[1]; + return name; +} + +function delete_profile_field(e) { + e.preventDefault(); + e.stopPropagation(); + var btn = $(this); + + channel.del({ + url: '/json/realm/profile_fields/' + encodeURIComponent(btn.attr('data-profile-field-id')), + error: function (xhr) { + if (xhr.status.toString().charAt(0) === "4") { + btn.closest("td").html( + $("

").addClass("text-error").text(JSON.parse(xhr.responseText).msg) + ); + } else { + btn.text(i18n.t("Failed!")); + } + }, + }); +} + +function create_profile_field(e) { + e.preventDefault(); + e.stopPropagation(); + + var name_status = $('#admin-profile-field-name-status'); + name_status.hide(); + + channel.post({ + url: "/json/realm/profile_fields", + data: $(this).serialize(), + error: function (xhr) { + var response = JSON.parse(xhr.responseText); + xhr.responseText = JSON.stringify({msg: response.msg}); + ui_report.error(i18n.t("Failed"), xhr, name_status); + }, + }); +} + +function get_profile_field_info(id) { + var info = {}; + info.row = $("tr.profile-field-row[data-profile-field-id='" + id + "']"); + info.form = $("tr.profile-field-form[data-profile-field-id='" + id + "']"); + return info; +} + +function open_edit_form(e) { + var field_id = $(e.currentTarget).attr("data-profile-field-id"); + var profile_field = get_profile_field_info(field_id); + + profile_field.row.hide(); + profile_field.form.show(); + + profile_field.form.find('.reset').on("click", function () { + profile_field.form.hide(); + profile_field.row.show(); + }); + + profile_field.form.find('.submit').on("click", function () { + e.preventDefault(); + e.stopPropagation(); + + var profile_field_status = $('#admin-profile-field-status'); + profile_field_status.hide(); + + // For some reason jQuery's serialize() is not working with + // channel.patch even though it is supported by $.ajax. + var data = {}; + data.name = profile_field.form.find('input[name=name]').val(); + + channel.patch({ + url: "/json/realm/profile_fields/" + field_id, + data: data, + error: function (xhr) { + var response = JSON.parse(xhr.responseText); + xhr.responseText = JSON.stringify({msg: response.msg}); + ui_report.error(i18n.t("Failed"), xhr, profile_field_status); + }, + }); + }); +} + +exports.reset = function () { + meta.loaded = false; +}; + +exports.populate_profile_fields = function (profile_fields_data) { + if (!meta.loaded) { + return; + } + + var profile_fields_table = $("#admin_profile_fields_table").expectOne(); + profile_fields_table.find("tr.profile-field-row").remove(); // Clear all rows. + profile_fields_table.find("tr.profile-field-form").remove(); // Clear all rows. + _.each(profile_fields_data, function (profile_field) { + profile_fields_table.append( + templates.render( + "admin_profile_field_list", { + profile_field: { + id: profile_field.id, + name: profile_field.name, + type: field_type_id_to_string(profile_field.type), + }, + can_modify: page_params.is_admin, + } + ) + ); + }); + loading.destroy_indicator($('#admin_page_profile_fields_loading_indicator')); +}; + +exports.set_up = function () { + meta.loaded = true; + + // create loading indicators + loading.make_indicator($('#admin_page_profile_fields_loading_indicator')); + // Populate profile_fields table + exports.populate_profile_fields(page_params.custom_profile_fields); + + $('#admin_profile_fields_table').on('click', '.delete', delete_profile_field); + $(".organization").on("submit", "form.admin-profile-field-form", create_profile_field); + $("#admin_profile_fields_table").on("click", ".open-edit-form", open_edit_form); +}; + +exports.report_success = function (operation) { + var profile_field_status = $('#admin-profile-field-status'); + profile_field_status.hide(); + var msg; + + if (operation === 'add') { + msg = i18n.t('Custom profile field added!'); + } else if (operation === 'delete') { + msg = i18n.t('Custom profile field deleted!'); + } else if (operation === 'update') { + msg = i18n.t('Custom profile field updated!'); + } + + ui_report.success(msg, profile_field_status); +}; + +return exports; +}()); + +if (typeof module !== 'undefined') { + module.exports = settings_profile_fields; +} diff --git a/static/styles/settings.css b/static/styles/settings.css index 499deaf20d..c6396d2221 100644 --- a/static/styles/settings.css +++ b/static/styles/settings.css @@ -423,6 +423,7 @@ input[type=checkbox].inline-block { #settings .settings-section .new-bot-form, #settings .settings-section .new-alert-word-form, #filter-settings .new-filter-form, + #profile-field-settings .new-profile-field-form, #settings .settings-section .notification-settings-form, #settings .settings-section .display-settings-form, #settings .settings-section .edit-bot-form-box { @@ -434,6 +435,7 @@ input[type=checkbox].inline-block { #settings .settings-section .new-bot-form .control-label, #settings .settings-section .new-alert-word-form .control-label, #filter-settings .new-filter-form .control-label, + #profile-field-settings .new-profile-field-form .control-label, #settings .settings-section .edit-bot-form-box .control-label { display: block; width: 120px; @@ -449,6 +451,7 @@ input[type=checkbox].inline-block { #settings .settings-section .new-bot-form .controls, #settings .settings-section .new-alert-word-form button, #filter-settings .new-filter-form .controls, + #profile-field-settings .new-profile-field-form .controls, #settings .settings-section .edit-bot-form-box .controls { margin: auto; text-align: center; @@ -586,6 +589,7 @@ input[type=checkbox].inline-block { color: hsl(0, 0%, 66%); } +.add-new-profile-field-box button, .add-new-filter-box button { margin-left: calc(10em - -20px) !important; } @@ -594,10 +598,12 @@ input[type=checkbox].inline-block { margin: 10px 0px; } +.admin_profile_fields_table, .admin_filters_table { margin-top: 20px; } +#admin-profile-field-name-status, #admin-filter-pattern-status, #admin-filter-format-status { margin: 20px 0 0 0; @@ -777,6 +783,7 @@ input[type=checkbox].inline-block { #get_api_key_box .control-label, .admin-emoji-form .control-label, .admin-filter-form .control-label, +.admin-profile-field-form .control-label, .edit_bot_form .control-label { width: 10em; text-align: right; diff --git a/static/templates/admin_profile_field_list.handlebars b/static/templates/admin_profile_field_list.handlebars new file mode 100644 index 0000000000..b7c24cd02c --- /dev/null +++ b/static/templates/admin_profile_field_list.handlebars @@ -0,0 +1,38 @@ +{{#with profile_field}} + + + {{name}} + + + {{type}} + + {{#if ../can_modify}} + + + + + {{/if}} + + + +

+
+ + +
+
+ + +
+
+ + +{{/with}} diff --git a/static/templates/admin_tab.handlebars b/static/templates/admin_tab.handlebars index 0d74f25869..6949179e55 100644 --- a/static/templates/admin_tab.handlebars +++ b/static/templates/admin_tab.handlebars @@ -28,3 +28,5 @@ {{ partial "invites-list-admin" }} {{ partial "user-groups-admin" }} + +{{ partial "profile-field-settings-admin" }} diff --git a/static/templates/settings/profile-field-settings-admin.handlebars b/static/templates/settings/profile-field-settings-admin.handlebars new file mode 100644 index 0000000000..e6c4c81279 --- /dev/null +++ b/static/templates/settings/profile-field-settings-admin.handlebars @@ -0,0 +1,39 @@ +
+
+
+ + + + + {{#if is_admin}} + + {{/if}} + +
{{t "Label" }}{{t "Type" }}{{t "Actions" }}
+
+ {{#if is_admin}} +
+
+
+
{{t "Add a new profile field" }}
+
+ + +
+
+
+ + +
+ +
+
+
+ {{/if}} +
diff --git a/templates/zerver/settings_overlay.html b/templates/zerver/settings_overlay.html index b2758b3d94..2b3f0100c5 100644 --- a/templates/zerver/settings_overlay.html +++ b/templates/zerver/settings_overlay.html @@ -96,6 +96,12 @@
{{ _('Filter settings') }}
+ {% if development_environment %} +
  • + +
    {{ _('Custom profile fields') }}
    +
  • + {% endif %} {% if is_admin %}
  • diff --git a/zproject/settings.py b/zproject/settings.py index 5613e75c0e..edbf171a65 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -1136,6 +1136,7 @@ JS_SPECS = { 'js/settings_filters.js', 'js/settings_invites.js', 'js/settings_user_groups.js', + 'js/settings_profile_fields.js', 'js/settings.js', 'js/admin_sections.js', 'js/admin.js',