UX: Add `groups/custom/new` route for admins to create a new group.

This commit is contained in:
Guo Xiang Tan 2018-03-27 16:45:21 +08:00
parent 558914b986
commit 7edab1c0b9
18 changed files with 535 additions and 61 deletions

View File

@ -35,6 +35,10 @@ export default Ember.Controller.extend({
actions: {
loadMore() {
this.get('model').loadMore();
},
new() {
this.transitionToRoute("groups.new");
}
}
});

View File

@ -0,0 +1,103 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
import User from "discourse/models/user";
import InputValidation from 'discourse/models/input-validation';
import debounce from 'discourse/lib/debounce';
export default Ember.Controller.extend({
disableSave: null,
aliasLevelOptions: [
{ name: I18n.t("groups.alias_levels.nobody"), value: 0 },
{ name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 },
{ name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 },
{ name: I18n.t("groups.alias_levels.everyone"), value: 99 }
],
visibilityLevelOptions: [
{ name: I18n.t("groups.visibility_levels.public"), value: 0 },
{ name: I18n.t("groups.visibility_levels.members"), value: 1 },
{ name: I18n.t("groups.visibility_levels.staff"), value: 2 },
{ name: I18n.t("groups.visibility_levels.owners"), value: 3 }
],
@computed('model.visibility_level', 'model.public_admission')
disableMembershipRequestSetting(visibility_level, publicAdmission) {
visibility_level = parseInt(visibility_level);
return (visibility_level !== 0) || publicAdmission;
},
@computed('basicNameValidation', 'uniqueNameValidation')
nameValidation(basicNameValidation, uniqueNameValidation) {
return uniqueNameValidation ? uniqueNameValidation : basicNameValidation;
},
@computed('model.name')
basicNameValidation(name) {
if (name === undefined) {
return this._failedInputValidation();
};
if (name === "") {
this.set('uniqueNameValidation', null);
return this._failedInputValidation(I18n.t('groups.new.name.blank'));
}
if (name.length < this.siteSettings.min_username_length) {
return this._failedInputValidation(I18n.t('groups.new.name.too_short'));
}
if (name.length > this.siteSettings.max_username_length) {
return this._failedInputValidation(I18n.t('groups.new.name.too_long'));
}
this.checkGroupName();
return this._failedInputValidation(I18n.t('groups.new.name.checking'));
},
checkGroupName: debounce(function() {
User.checkUsername(this.get('model.name')).then(response => {
const validationName = 'uniqueNameValidation';
if (response.available) {
this.set(validationName, InputValidation.create({
ok: true,
reason: I18n.t('groups.new.name.available')
}));
this.set('disableSave', false);
} else {
let reason;
if (response.errors) {
reason = response.errors.join(' ');
} else {
reason = I18n.t('groups.new.name.not_available');
}
this.set(validationName, this._failedInputValidation(reason));
}
});
}, 500),
_failedInputValidation(reason) {
this.set('disableSave', true);
const options = { failed: true };
if (reason) options.reason = reason;
return InputValidation.create(options);
},
actions: {
save() {
this.set('disableSave', true);
const group = this.get('model');
group.create().then(() => {
this.transitionToRoute("group.members", group.name);
}).catch(popupAjaxError)
.finally(() => this.set('disableSave', false));
},
}
});

View File

@ -49,7 +49,9 @@ export default function() {
this.route('categoryWithID', { path: '/c/:parentSlug/:slug/:id' });
});
this.route('groups', { resetNamespace: true });
this.route('groups', { resetNamespace: true }, function() {
this.route("new", { path: "custom/new" });
});
this.route('group', { path: '/groups/:name', resetNamespace: true }, function() {
this.route('members');

View File

@ -0,0 +1,21 @@
import Group from 'discourse/models/group';
export default Discourse.Route.extend({
titleToken() {
return I18n.t('groups.new.title');
},
model() {
return Group.create({ automatic: false, visibility_level: 0 });
},
setupController(controller, model) {
controller.set("model", model);
},
afterModel() {
if (!(this.currentUser && this.currentUser.admin)) {
this.transitionTo("groups");
}
},
});

View File

@ -1,6 +1,12 @@
{{#d-section pageClass="groups"}}
<h1>{{i18n "groups.index.title"}}</h1>
{{#if currentUser.admin}}
<div class="list-controls">
{{group-admin-dropdown new="new"}}
</div>
{{/if}}
<div class="groups-filter">
{{combo-box value=type
content=types

View File

@ -0,0 +1,183 @@
{{#d-section pageClass="groups-new"}}
<h1>{{i18n "groups.new.title"}}</h1>
<form class="groups-new-form form-horizontal">
<div class="control-group">
<label for="name">{{i18n 'groups.name'}}</label>
{{text-field name="name"
class="input-xxlarge"
value=model.name
placeholderKey="groups.name_placeholder"}}
{{input-tip validation=nameValidation}}
</div>
<div class="control-group">
<label for='full_name'>{{i18n 'groups.manage.full_name'}}</label>
{{text-field name='full_name'
class="input-xxlarge group-manage-full-name"
value=model.full_name}}
</div>
<div class="control-group">
<label for="title">
{{i18n 'admin.groups.default_title'}}
</label>
{{input value=model.title name="title" class="input-xxlarge"}}
</div>
<div class="control-group">
<label for="bio">{{i18n 'groups.bio'}}</label>
{{d-editor value=model.bio_raw}}
</div>
<div class="control-group">
<label for="owner-selector">{{i18n 'admin.groups.add_owners'}}</label>
{{user-selector usernames=model.ownerUsernames
placeholderKey="groups.selector_placeholder"
id="owner-selector"}}
</div>
<div class="control-group">
<label for="member-selector">{{i18n 'groups.members.title'}}</label>
{{user-selector usernames=model.usernames
placeholderKey="groups.selector_placeholder"
id="member-selector"}}
</div>
<div class="control-group">
<label for="visiblity">{{i18n 'groups.visibility_levels.title'}}</label>
{{combo-box name="alias"
valueAttribute="value"
value=model.visibility_level
content=visibilityLevelOptions
castInteger=true}}
</div>
<div class="control-group">
<label>
{{input type="checkbox" checked=model.primary_group}}
{{i18n 'admin.groups.primary_group'}}
</label>
<label>
{{input type="checkbox"
class="groups-new-public-admission"
checked=model.public_admission
disabled=disablePublicSetting}}
{{i18n 'groups.public_admission'}}
</label>
<label>
{{input type='checkbox'
checked=model.public_exit}}
{{i18n 'groups.public_exit'}}
</label>
<label>
{{input type="checkbox"
class="groups-new-allow-membership-requests"
checked=model.allow_membership_requests
disabled=disableMembershipRequestSetting}}
{{i18n 'groups.allow_membership_requests'}}
</label>
{{#if model.allow_membership_requests}}
<div>
<label for="membership-request-template">
{{i18n 'groups.membership_request_template'}}
</label>
{{expanding-text-area name="membership-request-template"
value=model.membership_request_template}}
</div>
{{/if}}
</div>
<div class="control-group">
<label for="alias">{{i18n 'groups.alias_levels.mentionable'}}</label>
{{combo-box name="alias"
valueAttribute="value"
value=model.mentionable_level
content=aliasLevelOptions}}
</div>
<div class="control-group">
<label for="alias">{{i18n 'groups.alias_levels.messageable'}}</label>
{{combo-box name="alias"
valueAttribute="value"
value=model.messageable_level
content=aliasLevelOptions}}
</div>
<div class="control-group">
<label>{{i18n 'groups.notification_level'}}</label>
{{notifications-button i18nPrefix='groups.notifications'
value=model.default_notification_level}}
</div>
<div class="control-group">
<label for="automatic_membership">
{{i18n 'admin.groups.automatic_membership_email_domains'}}
</label>
{{list-setting name="automatic_membership" settingValue=model.emailDomains}}
<label>
{{input type="checkbox" checked=model.automatic_membership_retroactive}}
{{i18n 'admin.groups.automatic_membership_retroactive'}}
</label>
</div>
<div class="control-group">
<label for="grant_trust_level">{{i18n 'groups.trust_levels.title'}}</label>
{{combo-box name="grant_trust_level"
valueAttribute="value"
value=model.grant_trust_level
content=trustLevelOptions}}
</div>
{{#if siteSettings.email_in}}
<div class="control-group">
<label for="incoming_email">{{i18n 'admin.groups.incoming_email'}}</label>
{{text-field name="incoming_email"
class="input-xxlarge"
value=model.incoming_email
placeholderKey="admin.groups.incoming_email_placeholder"}}
{{plugin-outlet name="group-email-in" args=(hash model=model)}}
</div>
{{/if}}
<div class="control-group">
{{group-flair-inputs model=model}}
</div>
{{plugin-outlet name="group-edit" args=(hash group=model)}}
<div class='control-group buttons'>
{{d-button action="save"
disabled=disableSave
class='btn btn-primary'
label='groups.new.create'}}
{{#link-to "groups"}}
{{i18n 'cancel'}}
{{/link-to}}
</div>
</form>
{{/d-section}}

View File

@ -0,0 +1,29 @@
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
export default DropdownSelectBoxComponent.extend({
classNames: "groups-admin-dropdown pull-right",
headerIcon: ["bars", "caret-down"],
showFullTitle: false,
computeContent() {
const items = [
{
id: "new",
name: I18n.t("groups.new.title"),
description: I18n.t("groups.new.description"),
icon: "plus"
}
];
return items;
},
mutateValue(value) {
switch (value) {
case 'new': {
this.sendAction("new");
break;
}
}
},
});

View File

@ -201,7 +201,8 @@ table.group-members {
}
}
.group-manage {
.group-manage,
.groups-new-page {
.form-horizontal {
label {
font-weight: bold;

View File

@ -20,6 +20,7 @@
@import "desktop/history";
@import "desktop/queued-posts";
@import "desktop/group";
@import "desktop/groups";
// Import all component-specific files
@import "desktop/components/*";

View File

@ -0,0 +1,19 @@
.groups-page {
.list-controls {
float: right;
}
}
$filter-line-height: 1.5;
.groups-filter {
.groups-type-filter {
.select-kit-header {
line-height: $filter-line-height;
}
}
input {
line-height: $filter-line-height;
}
}

View File

@ -7,7 +7,8 @@ class GroupsController < ApplicationController
:update,
:histories,
:request_membership,
:search
:search,
:new
]
skip_before_action :preload_json, :check_xhr, only: [:posts_feed, :mentions_feed]
@ -113,6 +114,9 @@ class GroupsController < ApplicationController
end
end
def new
end
def edit
end

View File

@ -403,6 +403,17 @@ en:
remove_user_as_group_owner: "Revoke owner"
groups:
new:
title: "New Group"
description: "Create a new group"
create: "Create"
name:
too_short: "Group name is too short"
too_long: "Group name is too long"
checking: "Checking group name availability..."
available: "Group name is available"
not_available: "Group name is not available"
blank: "Group name cannot be blank"
manage:
title: 'Manage'
name: 'Name'
@ -452,7 +463,6 @@ en:
submit: "Submit Request"
title: "Request to join @%{group_name}"
reason: "Let the group owners know why you belong in this group"
membership: "Membership"
name: "Name"
user_count: "Members Count"

View File

@ -462,6 +462,7 @@ Discourse::Application.routes.draw do
get 'logs' => 'groups#histories'
collection do
get 'custom/new' => 'groups#new', constraints: AdminConstraint.new
get "search" => "groups#search"
end

View File

@ -984,4 +984,34 @@ describe GroupsController do
end
end
end
describe '#new' do
describe 'for an anon user' do
it 'should return 404' do
get '/groups/custom/new'
expect(response.status).to eq(404)
end
end
describe 'for a normal user' do
before { sign_in(user) }
it 'should return 404' do
get '/groups/custom/new'
expect(response.status).to eq(404)
end
end
describe 'for an admin user' do
before { sign_in(Fabricate(:admin)) }
it 'should return 404' do
get '/groups/custom/new'
expect(response.status).to eq(200)
end
end
end
end

View File

@ -1,5 +1,7 @@
import { acceptance, logIn } from "helpers/qunit-helpers";
acceptance("Group");
const response = object => {
return [
200,
@ -8,63 +10,6 @@ const response = object => {
];
};
acceptance("Groups", {
beforeEach() {
server.get('/groups/snorlax.json', () => { // eslint-disable-line no-undef
return response({"basic_group":{"id":41,"automatic":false,"name":"snorlax","user_count":1,"alias_level":0,"visible":true,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":true,"title":"Team Snorlax","grant_trust_level":null,"incoming_email":null,"has_messages":false,"flair_url":"","flair_bg_color":"","flair_color":"","bio_raw":"","bio_cooked":null,"public":true,"is_group_user":true,"is_group_owner":true}});
});
// Workaround while awaiting https://github.com/tildeio/route-recognizer/issues/53
server.get('/groups/snorlax/logs.json', request => { // eslint-disable-line no-undef
if (request.queryParams["filters[action]"]) {
return response({"logs":[{"action":"change_group_setting","subject":"title","prev_value":null,"new_value":"Team Snorlax","created_at":"2016-12-12T08:27:46.408Z","acting_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"target_user":null}],"all_loaded":true});
} else {
return response({"logs":[{"action":"change_group_setting","subject":"title","prev_value":null,"new_value":"Team Snorlax","created_at":"2016-12-12T08:27:46.408Z","acting_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"target_user":null},{"action":"add_user_to_group","subject":null,"prev_value":null,"new_value":null,"created_at":"2016-12-12T08:27:27.725Z","acting_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"target_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"}}],"all_loaded":true});
}
});
}
});
QUnit.test("Browsing Groups", assert => {
visit("/groups");
andThen(() => {
assert.equal(count('.groups-table-row'), 2, 'it displays visible groups');
assert.equal(find('.group-index-join').length, 1, 'it shows button to join group');
assert.equal(find('.group-index-request').length, 1, 'it shows button to request for group membership');
});
click('.group-index-join');
andThen(() => {
assert.ok(exists('.modal.login-modal'), 'it shows the login modal');
});
click('.login-modal .close');
andThen(() => {
assert.ok(invisible('.modal.login-modal'), 'it closes the login modal');
});
click('.group-index-request');
andThen(() => {
assert.ok(exists('.modal.login-modal'), 'it shows the login modal');
});
click("a[href='/groups/discourse/members']");
andThen(() => {
assert.equal(find('.group-info-name').text().trim(), 'Awesome Team', "it displays the group page");
});
click('.group-index-join');
andThen(() => {
assert.ok(exists('.modal.login-modal'), 'it shows the login modal');
});
});
QUnit.test("Anonymous Viewing Group", assert => {
visit("/groups/discourse");

View File

@ -0,0 +1,43 @@
import { acceptance, logIn } from "helpers/qunit-helpers";
acceptance("Groups");
QUnit.test("Browsing Groups", assert => {
visit("/groups");
andThen(() => {
assert.equal(count('.groups-table-row'), 2, 'it displays visible groups');
assert.equal(find('.group-index-join').length, 1, 'it shows button to join group');
assert.equal(find('.group-index-request').length, 1, 'it shows button to request for group membership');
});
click('.group-index-join');
andThen(() => {
assert.ok(exists('.modal.login-modal'), 'it shows the login modal');
});
click('.login-modal .close');
andThen(() => {
assert.ok(invisible('.modal.login-modal'), 'it closes the login modal');
});
click('.group-index-request');
andThen(() => {
assert.ok(exists('.modal.login-modal'), 'it shows the login modal');
});
click("a[href='/groups/discourse/members']");
andThen(() => {
assert.equal(find('.group-info-name').text().trim(), 'Awesome Team', "it displays the group page");
});
click('.group-index-join');
andThen(() => {
assert.ok(exists('.modal.login-modal'), 'it shows the login modal');
});
});

View File

@ -0,0 +1,72 @@
import { acceptance, logIn } from "helpers/qunit-helpers";
acceptance("New Group");
QUnit.test("As an anon user", assert => {
visit("/groups");
andThen(() => {
assert.equal(
find('.groups-admin-dropdown').length, 0,
'it should not display the admin dropdown'
);
});
});
QUnit.test("Creating a new group", assert => {
logIn();
Discourse.reset();
visit("/groups");
selectKit('.groups-admin-dropdown').expand().selectRowByValue("new");
fillIn("input[name='name']", '1');
andThen(() => {
assert.equal(
find('.tip.bad').text().trim(), I18n.t("groups.new.name.too_short"),
'it should show the right validation tooltip'
);
assert.ok(
find("button[title='Create']:disabled").length === 1,
'it should disable the save button'
);
});
fillIn("input[name='name']", 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
andThen(() => {
assert.equal(
find('.tip.bad').text().trim(), I18n.t("groups.new.name.too_long"),
'it should show the right validation tooltip'
);
});
fillIn("input[name='name']", '');
andThen(() => {
assert.equal(
find('.tip.bad').text().trim(), I18n.t("groups.new.name.blank"),
'it should show the right validation tooltip'
);
});
fillIn("input[name='name']", 'goodusername');
andThen(() => {
assert.equal(
find('.tip.good').text().trim(), I18n.t("groups.new.name.available"),
'it should show the right validation tooltip'
);
});
click(".groups-new-public-admission");
andThen(() => {
assert.equal(
find('groups-new-allow-membership-requests').length, 0,
'it should disable the membership requests checkbox'
);
});
});