settings: Add support for adding/removing custom profile fields.

Now that we have support for displaying custom profile fields, this
adds administrator-level support for creating them.

Tweaked by tabbott to fix a few small bugs and clean up the commit message.

Fixes #1760.
This commit is contained in:
Umair Khan 2017-12-14 09:51:45 +05:00 committed by Tim Abbott
parent 7885dd4408
commit c14cefc24c
15 changed files with 386 additions and 0 deletions

View File

@ -61,6 +61,7 @@
"settings_filters": false, "settings_filters": false,
"settings_invites": false, "settings_invites": false,
"settings_user_groups": false, "settings_user_groups": false,
"settings_profile_fields": false,
"settings": false, "settings": false,
"resize": false, "resize": false,
"loading": false, "loading": false,

View File

@ -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 // Test custom realm filters
casper.then(function () { casper.then(function () {
casper.click("li[data-section='filter-settings']"); casper.click("li[data-section='filter-settings']");

View File

@ -476,6 +476,14 @@ var event_fixtures = {
type: 'delete_message', type: 'delete_message',
message_id: 1337, 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) { 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) { with_overrides(function (override) {
// default_streams // default_streams
var event = event_fixtures.default_streams; var event = event_fixtures.default_streams;

View File

@ -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'); 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 += '<tbody id="admin_profile_fields_table">';
html += render('admin_profile_field_list', args);
html += '</tbody>';
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 += '<tbody id="admin_profile_fields_table">';
html += render('admin_profile_field_list', args);
html += '</tbody>';
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() { (function admin_filter_list() {
// When the logged in user is admin // When the logged in user is admin

View File

@ -20,12 +20,15 @@ exports.show_or_hide_menu_item = function () {
.find("input:not(.search), button, select").attr("disabled", true); .find("input:not(.search), button, select").attr("disabled", true);
$(".organization-box [data-name='filter-settings']") $(".organization-box [data-name='filter-settings']")
.find("input, button, select").attr("disabled", true); .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"); $(".control-label-disabled").css("color", "#333333");
} }
}; };
function _setup_page() { function _setup_page() {
var options = { var options = {
custom_profile_field_types: page_params.custom_profile_field_types,
realm_name: page_params.realm_name, realm_name: page_params.realm_name,
realm_description: page_params.realm_description, realm_description: page_params.realm_description,
realm_restricted_to_domain: page_params.realm_restricted_to_domain, realm_restricted_to_domain: page_params.realm_restricted_to_domain,

View File

@ -35,6 +35,9 @@ exports.load_admin_section = function (name) {
case 'user-groups-admin': case 'user-groups-admin':
section = 'user-groups'; section = 'user-groups';
break; break;
case 'profile-field-settings':
section = 'profile-fields';
break;
default: default:
blueslip.error('Unknown admin id ' + name); blueslip.error('Unknown admin id ' + name);
return; return;
@ -68,6 +71,9 @@ exports.load_admin_section = function (name) {
case 'user-groups': case 'user-groups':
settings_user_groups.set_up(); settings_user_groups.set_up();
break; break;
case 'profile-fields':
settings_profile_fields.set_up();
break;
default: default:
blueslip.error('programming error for section ' + section); blueslip.error('programming error for section ' + section);
return; return;
@ -85,6 +91,7 @@ exports.reset_sections = function () {
settings_filters.reset(); settings_filters.reset();
settings_invites.reset(); settings_invites.reset();
settings_user_groups.reset(); settings_user_groups.reset();
settings_profile_fields.reset();
}; };
return exports; return exports;

View File

@ -160,6 +160,12 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
settings_filters.populate_filters(page_params.realm_filters); settings_filters.populate_filters(page_params.realm_filters);
break; 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': case 'realm_domains':
var i; var i;
if (event.op === 'add') { if (event.op === 'add') {

View File

@ -108,6 +108,7 @@ function _setup_page() {
"filter-settings": i18n.t("Filter settings"), "filter-settings": i18n.t("Filter settings"),
"invites-list-admin": i18n.t("Invitations"), "invites-list-admin": i18n.t("Invitations"),
"user-groups-admin": i18n.t("User groups"), "user-groups-admin": i18n.t("User groups"),
"profile-field-settings": i18n.t("Profile field settings"),
}; };
} }

View File

@ -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(
$("<p>").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;
}

View File

@ -423,6 +423,7 @@ input[type=checkbox].inline-block {
#settings .settings-section .new-bot-form, #settings .settings-section .new-bot-form,
#settings .settings-section .new-alert-word-form, #settings .settings-section .new-alert-word-form,
#filter-settings .new-filter-form, #filter-settings .new-filter-form,
#profile-field-settings .new-profile-field-form,
#settings .settings-section .notification-settings-form, #settings .settings-section .notification-settings-form,
#settings .settings-section .display-settings-form, #settings .settings-section .display-settings-form,
#settings .settings-section .edit-bot-form-box { #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-bot-form .control-label,
#settings .settings-section .new-alert-word-form .control-label, #settings .settings-section .new-alert-word-form .control-label,
#filter-settings .new-filter-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 { #settings .settings-section .edit-bot-form-box .control-label {
display: block; display: block;
width: 120px; width: 120px;
@ -449,6 +451,7 @@ input[type=checkbox].inline-block {
#settings .settings-section .new-bot-form .controls, #settings .settings-section .new-bot-form .controls,
#settings .settings-section .new-alert-word-form button, #settings .settings-section .new-alert-word-form button,
#filter-settings .new-filter-form .controls, #filter-settings .new-filter-form .controls,
#profile-field-settings .new-profile-field-form .controls,
#settings .settings-section .edit-bot-form-box .controls { #settings .settings-section .edit-bot-form-box .controls {
margin: auto; margin: auto;
text-align: center; text-align: center;
@ -586,6 +589,7 @@ input[type=checkbox].inline-block {
color: hsl(0, 0%, 66%); color: hsl(0, 0%, 66%);
} }
.add-new-profile-field-box button,
.add-new-filter-box button { .add-new-filter-box button {
margin-left: calc(10em - -20px) !important; margin-left: calc(10em - -20px) !important;
} }
@ -594,10 +598,12 @@ input[type=checkbox].inline-block {
margin: 10px 0px; margin: 10px 0px;
} }
.admin_profile_fields_table,
.admin_filters_table { .admin_filters_table {
margin-top: 20px; margin-top: 20px;
} }
#admin-profile-field-name-status,
#admin-filter-pattern-status, #admin-filter-pattern-status,
#admin-filter-format-status { #admin-filter-format-status {
margin: 20px 0 0 0; margin: 20px 0 0 0;
@ -777,6 +783,7 @@ input[type=checkbox].inline-block {
#get_api_key_box .control-label, #get_api_key_box .control-label,
.admin-emoji-form .control-label, .admin-emoji-form .control-label,
.admin-filter-form .control-label, .admin-filter-form .control-label,
.admin-profile-field-form .control-label,
.edit_bot_form .control-label { .edit_bot_form .control-label {
width: 10em; width: 10em;
text-align: right; text-align: right;

View File

@ -0,0 +1,38 @@
{{#with profile_field}}
<tr class="profile-field-row" data-profile-field-id="{{id}}">
<td>
<span class="profile_field_name">{{name}}</span>
</td>
<td>
<span class="profile_field_type">{{type}}</span>
</td>
{{#if ../can_modify}}
<td>
<button class="button rounded small delete btn-danger" title="{{t 'Delete' }}" data-profile-field-id="{{id}}">
<i class="icon-vector-trash"></i>
</button>
<button class="button rounded small btn-warning open-edit-form" title="{{t 'Edit' }}" data-profile-field-id="{{id}}">
<i class="icon-vector-pencil"></i>
</button>
</td>
{{/if}}
</tr>
<tr class="profile-field-form display-none" data-profile-field-id="{{id}}">
<td colspan="3">
<form class="form-horizontal name-setting">
<div class="input-group name_change_container">
<label for="name">{{t "Name" }}</label>
<input type="text" name="name" value="{{ name }}" />
</div>
<div class="input-group">
<button type="button" class="button rounded sea-green submit">
{{t 'Save changes' }}
</button>
<button type="button" class="button rounded btn-danger reset">
{{t 'Cancel' }}
</button>
</div>
</form>
</td>
</tr>
{{/with}}

View File

@ -28,3 +28,5 @@
{{ partial "invites-list-admin" }} {{ partial "invites-list-admin" }}
{{ partial "user-groups-admin" }} {{ partial "user-groups-admin" }}
{{ partial "profile-field-settings-admin" }}

View File

@ -0,0 +1,39 @@
<div id="profile-field-settings" class="settings-section" data-name="profile-field-settings">
<div class="admin-table-wrapper">
<div class="alert" id="admin-profile-field-status"></div>
<table class="table table-condensed table-striped admin_profile_fields_table">
<tbody id="admin_profile_fields_table">
<th>{{t "Label" }}</th>
<th>{{t "Type" }}</th>
{{#if is_admin}}
<th class="actions">{{t "Actions" }}</th>
{{/if}}
</tbody>
</table>
</div>
{{#if is_admin}}
<form class="form-horizontal admin-profile-field-form">
<div class="add-new-profile-field-box grey-box">
<div class="new-profile-field-form wrapper">
<div class="settings-section-title new-profile-field-section-title">{{t "Add a new profile field" }}</div>
<div class="control-group">
<label for="profile_field_name" class="control-label">{{t "Label" }}</label>
<input type="text" id="profile_field_name" name="name" />
<div class="alert" id="admin-profile-field-name-status"></div>
</div>
<div class="control-group">
<label for="profile_field_type" class="control-label">{{t "Type" }}</label>
<select id="profile_field_type" name="field_type">
{{#each custom_profile_field_types}}
<option value='{{this.[0]}}'>{{this.[1]}}</option>
{{/each}}
</select>
</div>
<button type="submit" class="button rounded sea-green">
{{t 'Add profile field' }}
</button>
</div>
</div>
</form>
{{/if}}
</div>

View File

@ -96,6 +96,12 @@
<i class="icon icon-vector-font"></i> <i class="icon icon-vector-font"></i>
<div class="text">{{ _('Filter settings') }}</div> <div class="text">{{ _('Filter settings') }}</div>
</li> </li>
{% if development_environment %}
<li class="admin" tabindex="0" data-section="profile-field-settings">
<i class="icon icon-vector-user"></i>
<div class="text">{{ _('Custom profile fields') }}</div>
</li>
{% endif %}
{% if is_admin %} {% if is_admin %}
<li class="admin" tabindex="0" data-section="invites-list-admin"> <li class="admin" tabindex="0" data-section="invites-list-admin">
<i class="icon icon-vector-user"></i> <i class="icon icon-vector-user"></i>

View File

@ -1136,6 +1136,7 @@ JS_SPECS = {
'js/settings_filters.js', 'js/settings_filters.js',
'js/settings_invites.js', 'js/settings_invites.js',
'js/settings_user_groups.js', 'js/settings_user_groups.js',
'js/settings_profile_fields.js',
'js/settings.js', 'js/settings.js',
'js/admin_sections.js', 'js/admin_sections.js',
'js/admin.js', 'js/admin.js',