UX: Split up group form into smaller sections.

This commit is contained in:
Guo Xiang Tan 2018-04-06 11:36:57 +08:00
parent ba905b24ca
commit 4319273cf5
44 changed files with 760 additions and 583 deletions

View File

@ -1,119 +0,0 @@
import { default as computed, observes } 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.Component.extend({
disableSave: null,
nameInput: '',
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 }
],
trustLevelOptions: [
{ name: I18n.t("groups.trust_levels.none"), value: 0 },
{ name: 1, value: 1 }, { name: 2, value: 2 }, { name: 3, value: 3 }, { name: 4, value: 4 }
],
init() {
this._super();
this.set('nameInput', this.get('model.name'));
},
@computed('model.visibility_level', 'model.public_admission')
disableMembershipRequestSetting(visibility_level, publicAdmission) {
visibility_level = parseInt(visibility_level);
return (visibility_level !== 0) || publicAdmission;
},
@computed('model.visibility_level', 'model.allow_membership_requests')
disablePublicSetting(visibility_level, allowMembershipRequests) {
visibility_level = parseInt(visibility_level);
return (visibility_level !== 0) || allowMembershipRequests;
},
@computed('basicNameValidation', 'uniqueNameValidation')
nameValidation(basicNameValidation, uniqueNameValidation) {
return uniqueNameValidation ? uniqueNameValidation : basicNameValidation;
},
@observes("nameInput")
_validateName() {
name = this.get('nameInput');
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() {
name = this.get('nameInput');
if (Ember.isEmpty(name)) return;
User.checkUsername(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);
this.set('model.name', this.get('nameInput'));
} 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;
this.set('basicNameValidation', InputValidation.create(options));
},
actions: {
save() {
this.sendAction("save");
},
}
});

View File

@ -0,0 +1,23 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
saving: null,
@computed('saving')
savingText(saving) {
if (saving !== undefined) {
return saving ? I18n.t('saving') : I18n.t('saved');
}
},
actions: {
save() {
this.set('saving', true);
return this.get('model').save()
.catch(popupAjaxError)
.finally(() => this.set('saving', false));
}
},
});

View File

@ -0,0 +1,23 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
visibilityLevelOptions: [
{ name: I18n.t("admin.groups.manage.interaction.visibility_levels.public"), value: 0 },
{ name: I18n.t("admin.groups.manage.interaction.visibility_levels.members"), value: 1 },
{ name: I18n.t("admin.groups.manage.interaction.visibility_levels.staff"), value: 2 },
{ name: I18n.t("admin.groups.manage.interaction.visibility_levels.owners"), value: 3 }
],
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 }
],
@computed('siteSettings.email_in', 'model.automatic', 'currentUser.admin')
showEmailSettings(emailIn, automatic, isAdmin) {
return emailIn && isAdmin && !automatic;
}
});

View File

@ -0,0 +1,20 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
trustLevelOptions: [
{ name: I18n.t("admin.groups.manage.membership.trust_levels_none"), value: 0 },
{ name: 1, value: 1 }, { name: 2, value: 2 }, { name: 3, value: 3 }, { name: 4, value: 4 }
],
@computed('model.visibility_level', 'model.public_admission')
disableMembershipRequestSetting(visibility_level, publicAdmission) {
visibility_level = parseInt(visibility_level);
return (visibility_level !== 0) || publicAdmission;
},
@computed('model.visibility_level', 'model.allow_membership_requests')
disablePublicSetting(visibility_level, allowMembershipRequests) {
visibility_level = parseInt(visibility_level);
return (visibility_level !== 0) || allowMembershipRequests;
}
});

View File

@ -0,0 +1,84 @@
import { default as computed, observes } 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.Component.extend({
saving: null,
nameInput: null,
didInsertElement() {
this._super();
const name = this.get('model.name');
if (name) this.set("nameInput", name);
},
@computed('basicNameValidation', 'uniqueNameValidation')
nameValidation(basicNameValidation, uniqueNameValidation) {
return uniqueNameValidation ? uniqueNameValidation : basicNameValidation;
},
@observes("nameInput")
_validateName() {
name = this.get('nameInput');
if (name === this.get('model.name')) return;
if (name === undefined) {
return this._failedInputValidation();
};
if (name === "") {
this.set('uniqueNameValidation', null);
return this._failedInputValidation(I18n.t('admin.groups.new.name.blank'));
}
if (name.length < this.siteSettings.min_username_length) {
return this._failedInputValidation(I18n.t('admin.groups.new.name.too_short'));
}
if (name.length > this.siteSettings.max_username_length) {
return this._failedInputValidation(I18n.t('admin.groups.new.name.too_long'));
}
this.checkGroupName();
return this._failedInputValidation(I18n.t('admin.groups.new.name.checking'));
},
checkGroupName: debounce(function() {
name = this.get('nameInput');
if (Ember.isEmpty(name)) return;
User.checkUsername(name).then(response => {
const validationName = 'uniqueNameValidation';
if (response.available) {
this.set(validationName, InputValidation.create({
ok: true,
reason: I18n.t('admin.groups.new.name.available')
}));
this.set('saving', false);
this.set('model.name', this.get('nameInput'));
} else {
let reason;
if (response.errors) {
reason = response.errors.join(' ');
} else {
reason = I18n.t('admin.groups.new.name.not_available');
}
this.set(validationName, this._failedInputValidation(reason));
}
});
}, 500),
_failedInputValidation(reason) {
this.set('saving', true);
const options = { failed: true };
if (reason) options.reason = reason;
this.set('basicNameValidation', InputValidation.create(options));
},
});

View File

@ -1,7 +1,8 @@
import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import { extractError } from 'discourse/lib/ajax-error';
import ModalFunctionality from 'discourse/mixins/modal-functionality';
export default Ember.Controller.extend({
export default Ember.Controller.extend(ModalFunctionality, {
loading: false,
setAsOwner: false,
@ -31,10 +32,13 @@ export default Ember.Controller.extend({
this.get('model.name'),
{ queryParams: { filter: usernames } }
);
model.set("usernames", null);
this.send('closeModal');
})
.catch(popupAjaxError)
.catch(error => {
this.flash(extractError(error), 'error');
})
.finally(() => this.set('loading', false));
},
},

View File

@ -1,23 +1,3 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
@computed('saving')
savingText(saving) {
if (saving !== undefined) {
return saving ? I18n.t('saving') : I18n.t('saved');
}
},
actions: {
save() {
this.set('saving', true);
return this.get('model').save()
.catch(popupAjaxError)
.finally(() => {
this.set('saving', false);
});
}
}
saving: null,
});

View File

@ -6,13 +6,17 @@ export default Ember.Controller.extend({
@computed("model.automatic")
tabs(automatic) {
const defaultTabs = [
{ route: 'group.manage.profile', title: 'groups.manage.profile.title' },
{ route: 'group.manage.interaction', title: 'groups.manage.interaction.title' },
{ route: 'group.manage.logs', title: 'groups.manage.logs.title' },
];
if (!automatic) {
defaultTabs.splice(0, 0,
{ route: 'group.manage.profile', title: 'groups.manage.profile.title' }
);
defaultTabs.splice(1, 0,
{ route: 'group.manage.members', title: 'groups.manage.members.title' }
{ route: 'group.manage.membership', title: 'groups.manage.membership.title' }
);
}

View File

@ -64,6 +64,8 @@ export default function() {
this.route('manage', function() {
this.route('profile');
this.route('membership');
this.route('interaction');
this.route('members');
this.route('logs');
});

View File

@ -1,3 +1,5 @@
import showModal from 'discourse/lib/show-modal';
export default Discourse.Route.extend({
titleToken() {
return I18n.t('groups.members.title');
@ -20,6 +22,10 @@ export default Discourse.Route.extend({
},
actions: {
showAddMembersModal() {
showModal('group-add-members', { model: this.modelFor('group') });
},
didTransition() {
this.controllerFor("group-index").set("filterInput", this._params.filter);
return true;

View File

@ -1,9 +0,0 @@
export default Discourse.Route.extend({
titleToken() {
return I18n.t('groups.manage.members.title');
},
model() {
return this.modelFor('group');
},
});

View File

@ -19,5 +19,5 @@ export default Discourse.Route.extend({
setupController(controller, model) {
this.controllerFor('group-manage').setProperties({ model });
this.controllerFor("group").set("showing", 'manage');
}
},
});

View File

@ -2,7 +2,7 @@ import Group from 'discourse/models/group';
export default Discourse.Route.extend({
titleToken() {
return I18n.t('groups.new.title');
return I18n.t('admin.groups.new.title');
},
model() {

View File

@ -1,36 +1,35 @@
<div class="group-flair-left">
<div>
<label for="flair_url">{{i18n 'groups.flair_url'}}</label>
{{text-field name="flair_url"
value=model.flair_url
placeholderKey="groups.flair_url_placeholder"}}
</div>
<div class='control-group'>
<label class="control-label" for="flair_url">{{i18n 'groups.flair_url'}}</label>
<div>
<label for="flair_bg_color">{{i18n 'groups.flair_bg_color'}}</label>
{{text-field name="flair_bg_color"
class="group-flair-bg-color"
value=model.flair_bg_color
placeholderKey="groups.flair_bg_color_placeholder"}}
</div>
{{#if flairPreviewIcon}}
<div>
<label for="flair_color">{{i18n 'groups.flair_color'}}</label>
{{text-field name="flair_color"
class="group-flair-color"
value=model.flair_color
placeholderKey="groups.flair_color_placeholder"}}
</div>
{{/if}}
<div>
<strong>{{i18n 'groups.flair_note'}}</strong>
</div>
{{text-field name="flair_url"
class="input-xxlarge"
value=model.flair_url
placeholderKey="groups.flair_url_placeholder"}}
</div>
<div class="group-flair-right">
<label>{{flairPreviewLabel}}</label>
<div class='control-group'>
<label class="control-label" for="flair_bg_color">{{i18n 'groups.flair_bg_color'}}</label>
{{text-field name="flair_bg_color"
class="group-flair-bg-color input-xxlarge"
value=model.flair_bg_color
placeholderKey="groups.flair_bg_color_placeholder"}}
</div>
{{#if flairPreviewIcon}}
<div class='control-group'>
<label class="control-label" for="flair_color">{{i18n 'groups.flair_color'}}</label>
{{text-field name="flair_color"
class="group-flair-color input-xxlarge"
value=model.flair_color
placeholderKey="groups.flair_color_placeholder"}}
</div>
{{/if}}
<div class='control-group'>
<label class='control-label'>{{flairPreviewLabel}}</label>
<div class="avatar-flair-preview">
<div class="avatar-wrapper">
<img width="45" height="45" src="{{demoAvatarUrl}}" class="avatar actor">

View File

@ -1,226 +0,0 @@
<form class="groups-form form-horizontal">
{{#if model.automatic}}
<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 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>
{{else}}
{{#if this.currentUser.admin}}
<div class="control-group">
<label for="name">{{i18n 'groups.name'}}</label>
{{text-field name="name"
class="input-xxlarge group-form-name"
value=nameInput
placeholderKey="groups.name_placeholder"}}
{{input-tip validation=nameValidation}}
</div>
{{/if}}
<div class="control-group">
<label for='full_name'>{{i18n 'groups.manage.full_name'}}</label>
{{text-field name='full_name'
class="input-xxlarge group-form-full-name"
value=model.full_name}}
</div>
{{#if this.currentUser.admin}}
<div class="control-group">
<label for="title">
{{i18n 'admin.groups.default_title'}}
</label>
{{input value=model.title name="title" class="input-xxlarge"}}
</div>
{{/if}}
<div class="control-group">
<label for="bio">{{i18n 'groups.bio'}}</label>
{{d-editor value=model.bio_raw class="group-form-bio"}}
</div>
{{#if manageMembership}}
<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>
{{/if}}
<div class="control-group">
{{#if currentUser.admin}}
<label>
{{input type="checkbox" checked=model.primary_group}}
{{i18n 'admin.groups.primary_group'}}
</label>
{{/if}}
<label>
{{input type="checkbox"
class="group-form-public-admission"
checked=model.public_admission
disabled=disablePublicSetting}}
{{i18n 'groups.public_admission'}}
</label>
<label>
{{input type='checkbox'
class='group-form-public-exit'
checked=model.public_exit}}
{{i18n 'groups.public_exit'}}
</label>
<label>
{{input type="checkbox"
class="group-form-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"
class='group-form-membership-request-template'
value=model.membership_request_template}}
</div>
{{/if}}
</div>
{{#if currentUser.admin}}
<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}}
{{/if}}
<div class="control-group">
{{group-flair-inputs model=model}}
</div>
{{plugin-outlet name="group-edit" args=(hash group=model)}}
{{/if}}
<div class='control-group buttons'>
{{d-button action="save"
disabled=disableSave
class='btn btn-primary group-form-save'
label=saveLabel}}
{{yield}}
</div>
</form>

View File

@ -0,0 +1,6 @@
<div class='control-group buttons'>
{{d-button action="save"
disabled=saving
class='btn btn-primary group-form-save'
label="save"}}
</div>

View File

@ -0,0 +1,58 @@
{{#if currentUser.admin}}
<div class="control-group">
<label class="control-label">{{i18n 'admin.groups.manage.interaction.visibility'}}</label>
<label for="visiblity">{{i18n 'admin.groups.manage.interaction.visibility_levels.title'}}</label>
{{combo-box name="alias"
valueAttribute="value"
value=model.visibility_level
content=visibilityLevelOptions
castInteger=true
class="groups-form-visibility-level"}}
</div>
{{/if}}
<div class="control-group">
<label class="control-label">{{i18n 'groups.manage.interaction.posting'}}</label>
<label for="alias">{{i18n 'groups.alias_levels.mentionable'}}</label>
{{combo-box name="alias"
valueAttribute="value"
value=model.mentionable_level
content=aliasLevelOptions
class="groups-form-mentionable-level"}}
</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
class="groups-form-messageable-level"}}
</div>
{{#if showEmailSettings}}
<div class="control-group">
<label class="control-label">{{i18n 'admin.groups.manage.interaction.email'}}</label>
<label for="incoming_email">{{i18n 'admin.groups.manage.interaction.incoming_email'}}</label>
{{text-field name="incoming_email"
class="input-xxlarge groups-form-incoming-email"
value=model.incoming_email
placeholderKey="admin.groups.manage.interaction.incoming_email_placeholder"}}
{{plugin-outlet name="group-email-in" args=(hash model=model)}}
</div>
{{/if}}
<label class="control-label">{{i18n 'groups.manage.interaction.notification'}}</label>
<div class="control-group">
<label>{{i18n 'groups.notification_level'}}</label>
{{notifications-button i18nPrefix='groups.notifications'
value=model.default_notification_level
class="groups-form-default-notification-level"}}
</div>

View File

@ -0,0 +1,80 @@
{{#if currentUser.admin}}
<div class="control-group">
<label class="control-label">{{i18n "admin.groups.manage.membership.automatic"}}</label>
<label for="automatic_membership">
{{i18n 'admin.groups.manage.membership.automatic_membership_email_domains'}}
</label>
{{list-setting name="automatic_membership" settingValue=model.emailDomains}}
<label>
{{input type="checkbox"
checked=model.automatic_membership_retroactive
class="groups-form-automatic-membership-retroactive"}}
{{i18n 'admin.groups.manage.membership.automatic_membership_retroactive'}}
</label>
<label>
{{input type="checkbox"
checked=model.primary_group
class="groups-form-primary-group"}}
{{i18n 'admin.groups.manage.membership.primary_group'}}
</label>
</div>
<div class="control-group">
<label class="control-label">{{i18n "admin.groups.manage.membership.trust_level"}}</label>
<label for="grant_trust_level">{{i18n 'admin.groups.manage.membership.trust_levels_title'}}</label>
{{combo-box name="grant_trust_level"
valueAttribute="value"
value=model.grant_trust_level
content=trustLevelOptions
class="groups-form-grant-trust-level"}}
</div>
{{/if}}
<div class="control-group">
<label class="control-label">{{i18n "groups.manage.membership.access"}}</label>
<label>
{{input type="checkbox"
class="group-form-public-admission"
checked=model.public_admission
disabled=disablePublicSetting}}
{{i18n 'groups.public_admission'}}
</label>
<label>
{{input type='checkbox'
class='group-form-public-exit'
checked=model.public_exit}}
{{i18n 'groups.public_exit'}}
</label>
<label>
{{input type="checkbox"
class="group-form-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"
class='group-form-membership-request-template input-xxlarge'
value=model.membership_request_template}}
</div>
{{/if}}
</div>

View File

@ -0,0 +1,41 @@
{{#if this.currentUser.admin}}
<div class="control-group">
<label class="control-label" for="name">{{i18n 'groups.name'}}</label>
{{text-field name="name"
class="input-xxlarge group-form-name"
value=nameInput
placeholderKey="admin.groups.name_placeholder"}}
{{input-tip validation=nameValidation}}
</div>
{{/if}}
<div class="control-group">
<label class="control-label" for='full_name'>{{i18n 'groups.manage.full_name'}}</label>
{{text-field name='full_name'
class="input-xxlarge group-form-full-name"
value=model.full_name}}
</div>
{{#if this.currentUser.admin}}
<div class="control-group">
<label class="control-label" for="title">
{{i18n 'admin.groups.default_title'}}
</label>
{{input value=model.title name="title" class="input-xxlarge"}}
</div>
{{/if}}
<div class="control-group">
<label class="control-label" for="bio">{{i18n 'groups.bio'}}</label>
{{d-editor value=model.bio_raw class="group-form-bio input-xxlarge"}}
</div>
{{yield}}
<div class="control-group">
{{group-flair-inputs model=model}}
</div>

View File

@ -3,10 +3,10 @@
class="group-username-filter no-blur"}}
{{#if canManageGroup}}
{{#link-to "group.manage.members" model.name class="btn btn-primary pull-right"}}
{{d-icon "plus"}}
{{i18n "groups.manage.add_members"}}
{{/link-to}}
{{d-button action='showAddMembersModal'
icon="plus"
label="groups.add_members.title"
class="btn btn-primary pull-right"}}
{{/if}}
{{#if hasMembers}}

View File

@ -0,0 +1,4 @@
<form class="groups-form form-vertical">
{{groups-form-interaction-fields model=model}}
{{group-manage-save-button model=model}}
</form>

View File

@ -1,25 +0,0 @@
<form class="form-horizontal group-manage-members">
<div class="control-group">
<label>{{i18n "groups.manage.members.usernames"}}</label>
{{user-selector
usernames=model.usernames
placeholderKey="groups.selector_placeholder"
id="group-manage-members-user-selector"}}
</div>
{{#if this.currentUser.admin}}
<div class="control-group group-manage-members-make-owner">
<label>
{{input type="checkbox" class="inline" checked=setAsOwner}}
{{i18n "groups.manage.members.as_owner"}}
</label>
</div>
{{/if}}
{{d-button action="addMembers"
class="add btn-primary"
icon="plus"
disabled=disableAddButton
label="groups.add"}}
</form>

View File

@ -0,0 +1,4 @@
<form class="groups-form form-vertical">
{{groups-form-membership-fields model=model}}
{{group-manage-save-button model=model}}
</form>

View File

@ -1,5 +1,4 @@
{{group-form model=model
saveLabel="save"
save="save"
disableSave=saving
manageMembership=false}}
<form class="groups-form form-vertical">
{{groups-form-profile-fields model=model saving=saving}}
{{group-manage-save-button model=model saving=saving}}
</form>

View File

@ -4,7 +4,7 @@
{{d-button action="new"
class="groups-header-new pull-right"
icon="plus"
label="groups.new.title"}}
label="admin.groups.new.title"}}
{{/if}}
<div class="groups-header-filters">

View File

@ -1,13 +1,41 @@
{{#d-section pageClass="groups-new"}}
<h1>{{i18n "groups.new.title"}}</h1>
<h1>{{i18n "admin.groups.new.title"}}</h1>
{{#group-form model=model
saveLabel="groups.new.create"
save="save"
disableSave=saving}}
<hr/>
<form class="groups-form form-vertical">
{{#groups-form-profile-fields model=model saving=saving}}
<div class="control-group">
<label class="control-label" for="owner-selector">{{i18n 'admin.groups.add_owners'}}</label>
{{user-selector usernames=model.ownerUsernames
class="input-xxlarge"
placeholderKey="groups.selector_placeholder"
id="owner-selector"}}
</div>
<div class="control-group">
<label class="control-label" for="member-selector">{{i18n 'groups.members.title'}}</label>
{{user-selector usernames=model.usernames
class="input-xxlarge"
placeholderKey="groups.selector_placeholder"
id="member-selector"}}
</div>
{{/groups-form-profile-fields}}
{{groups-form-membership-fields model=model}}
{{groups-form-interaction-fields model=model}}
<div class='control-group buttons'>
{{d-button action="save"
disabled=saving
class='btn btn-primary group-form-save'
label="admin.groups.new.create"}}
{{#link-to "groups"}}
{{i18n 'cancel'}}
{{/link-to}}
{{/group-form}}
</div>
</form>
{{/d-section}}

View File

@ -2,6 +2,13 @@
placeholderKey=filterPlaceholder
class="group-username-filter no-blur"}}
{{#if canManageGroup}}
{{d-button action='showAddMembersModal'
icon="plus"
label="groups.add_members.title"
class="group-add-members-btn btn btn-primary"}}
{{/if}}
{{#if hasMembers}}
{{#load-more selector=".group-members .user-info" action="loadMore"}}
<div class="group-members">

View File

@ -0,0 +1,32 @@
{{#d-modal-body title="groups.add_members.title"}}
<form class="form-vertical group-add-members">
<div class="control-group">
<label class="control-label">
{{i18n "groups.add_members.usernames"}}
</label>
{{user-selector
class="input-xxlarge"
usernames=model.usernames
placeholderKey="groups.selector_placeholder"
id="group-add-members-user-selector"}}
</div>
{{#if currentUser.admin}}
<div class="control-group group-add-members-make-owner">
<label>
{{input type="checkbox" class="inline" checked=setAsOwner}}
{{i18n "admin.groups.add_members.as_owner"}}
</label>
</div>
{{/if}}
</form>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action="addMembers"
class="add btn-primary"
icon="plus"
disabled=disableAddButton
label="groups.add"}}
</div>

View File

@ -3,12 +3,6 @@
padding: 20px;
margin-bottom: 15px;
position: relative;
.group-details-button {
position: absolute;
top: 20px;
right: 20px;
}
}
.group-outlet {
@ -189,23 +183,9 @@ table.group-members {
color: $primary;
}
.form-horizontal {
.form-vertical {
.group-flair-inputs {
display: inline-block;
input[type="text"] {
width: 80% !important;
margin-bottom: 10px;
}
.group-flair-left {
float: left;
}
.group-flair-right {
float: left;
margin-left: 30px;
}
}
.avatar-flair-preview {
@ -222,8 +202,8 @@ table.group-members {
margin-right: 20px;
}
.group-manage-members {
.group-manage-members-make-owner {
.group-add-members {
.group-add-members-make-owner {
label {
display: inline;
vertical-align: middle;

View File

@ -4,6 +4,6 @@
}
}
.group-manage-membership-request-template {
.group-add-membership-request-template {
width: 98%;
}

View File

@ -52,7 +52,7 @@
background-color: $secondary;
position: relative;
border: 1px solid $primary-medium;
textarea {
background: transparent;
}
@ -73,7 +73,7 @@
min-height: 30px;
padding-left: 3px;
border-bottom: 1px solid $primary-low;
button {
background-color: transparent;
color: $primary-high;
@ -108,13 +108,13 @@
.d-editor-input,
.d-editor-preview {
box-sizing: border-box;
flex: 1 1 100%;
width: 100%;
flex: 1 1 100%;
width: 100%;
margin: 0;
min-height: auto;
word-wrap: break-word;
-webkit-appearance: none;
border-radius: 0;
border-radius: 0;
&:focus {
box-shadow: none;
border: 0;
@ -127,7 +127,7 @@
padding: 10px;
height: 100%;
overflow-x: hidden;
resize: none;
resize: none;
}
.d-editor-preview {
@ -155,6 +155,7 @@
}
.user-preferences .bio-composer,
.group-form-bio,
.edit-category-tab-topic-template {
textarea {
width: 100%;
@ -179,7 +180,8 @@
}
}
.user-preferences .bio-composer {
.user-preferences .bio-composer,
.group-form-bio {
padding: 10px;
border: 1px solid $primary-low;
}

View File

@ -66,3 +66,9 @@
}
}
}
.group-details-button {
position: absolute;
top: 20px;
right: 20px;
}

View File

@ -19,7 +19,7 @@
}
.group-nav.mobile-nav {
margin-bottom: 15px;
margin-bottom: 10px;
float: left;
}
@ -31,7 +31,7 @@
&.mobile-nav {
position: absolute;
right: 0;
top: -57px;
top: -51px;
}
}
@ -82,6 +82,10 @@ table.group-manage-logs {
}
.group-username-filter {
top: -57px;
top: -52px;
height: 27px;
}
.group-add-members-btn {
margin-bottom: 10px;
}

View File

@ -403,17 +403,10 @@ 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"
add_members:
title: "Add Members"
description: "Manage the membership of this group"
usernames: "Usernames"
manage:
title: 'Manage'
name: 'Name'
@ -422,11 +415,13 @@ en:
delete_member_confirm: "Remove '%{username}' from the '%{group}' group?"
profile:
title: Profile
members:
title: "Members"
description: "Manage the membership of this group"
usernames: "Usernames"
as_owner: "Set user(s) as owner(s) of this group"
interaction:
title: Interaction
posting: Posting
notification: Notification
membership:
title: Membership
access: Access
logs:
title: "Logs"
when: "When"
@ -437,7 +432,6 @@ en:
details: "Details"
from: "From"
to: "To"
name_placeholder: "Group name, no spaces, same as username rule"
public_admission: "Allow users to join the group freely (Requires publicly visible group)"
public_exit: "Allow users to leave the group freely"
empty:
@ -505,12 +499,6 @@ en:
mentions: "Mentions"
messages: "Messages"
notification_level: "Default notification level for group messages"
visibility_levels:
title: "Who can see this group?"
public: "Everyone"
members: "Group owners, members and admins"
staff: "Group owners and staff"
owners: "Group owners and admins"
alias_levels:
mentionable: "Who can @mention this group?"
messageable: "Who can message this group?"
@ -519,9 +507,6 @@ en:
mods_and_admins: "Only moderators and Admins"
members_mods_and_admins: "Only group members, moderators and admins"
everyone: "Everyone"
trust_levels:
title: "Trust level automatically granted to members when they're added:"
none: "None"
notifications:
watching:
title: "Watching"
@ -546,7 +531,6 @@ en:
flair_color_placeholder: "(Optional) Hex color value"
flair_preview_icon: "Preview Icon"
flair_preview_image: "Preview Image"
flair_note: "Note: Flair will only show for a user's primary group."
user_action_groups:
"1": "Likes Given"
@ -2848,12 +2832,44 @@ en:
notify_moderators: "custom"
groups:
new:
title: "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"
add_members:
as_owner: "Set user(s) as owner(s) of this group"
manage:
interaction:
email: Email
incoming_email: "Custom incoming email address"
incoming_email_placeholder: "enter email address"
visibility: Visibility
visibility_levels:
title: "Who can see this group?"
public: "Everyone"
members: "Group owners, members and admins"
staff: "Group owners and staff"
owners: "Group owners and admins"
membership:
automatic: Automatic
trust_level: Trust Level
trust_levels_title: "Trust level automatically granted to members when they're added:"
trust_levels_none: "None"
automatic_membership_email_domains: "Users who register with an email domain that exactly matches one in this list will be automatically added to this group:"
automatic_membership_retroactive: "Apply the same email domain rule to add existing registered users"
primary_group: "Automatically set as primary group"
name_placeholder: "Group name, no spaces, same as username rule"
primary: "Primary Group"
no_primary: "(no primary group)"
title: "Groups"
edit: "Edit Groups"
refresh: "Refresh"
new: "New"
about: "Edit your group membership and names here"
group_members: "Group members"
delete: "Delete"
@ -2861,7 +2877,6 @@ en:
delete_failed: "Unable to delete group. If this is an automatic group, it cannot be destroyed."
delete_owner_confirm: "Remove owner privilege for '%{username}'?"
add: "Add"
add_members: "Add members"
custom: "Custom"
bulk_complete: "The users have been added to the group."
bulk_complete_users_not_added: "These users were not added (make sure they have an account):"
@ -2869,14 +2884,9 @@ en:
bulk_paste: "Paste a list of usernames or emails, one per line:"
bulk_select: "(select a group)"
automatic: "Automatic"
automatic_membership_email_domains: "Users who register with an email domain that exactly matches one in this list will be automatically added to this group:"
automatic_membership_retroactive: "Apply the same email domain rule to add existing registered users"
default_title: "Default title for all users in this group"
primary_group: "Automatically set as primary group"
group_owners: Owners
add_owners: Add owners
incoming_email: "Custom incoming email address"
incoming_email_placeholder: "enter email address"
none_selected: "Select a group to get started"
no_custom_groups: "Create a new custom group"

View File

@ -479,6 +479,8 @@ Discourse::Application.routes.draw do
manage
manage/profile
manage/members
manage/membership
manage/interaction
manage/logs
}.each do |path|
get path => 'groups#show'

View File

@ -1,6 +1,5 @@
import { acceptance } from "helpers/qunit-helpers";
import { acceptance, replaceCurrentUser } from "helpers/qunit-helpers";
import { displayPollBuilderButton } from "discourse/plugins/poll/helpers/display-poll-builder-button";
import { replaceCurrentUser } from "discourse/plugins/poll/helpers/replace-current-user";
import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer";
acceptance("Poll Builder - polls are disabled", {

View File

@ -1,6 +1,5 @@
import { acceptance } from "helpers/qunit-helpers";
import { acceptance, replaceCurrentUser } from "helpers/qunit-helpers";
import { displayPollBuilderButton } from "discourse/plugins/poll/helpers/display-poll-builder-button";
import { replaceCurrentUser } from "discourse/plugins/poll/helpers/replace-current-user";
import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer";
acceptance("Poll Builder - polls are enabled", {

View File

@ -1,5 +0,0 @@
export function replaceCurrentUser(properties) {
const currentUser = Discourse.User.current();
currentUser.setProperties(properties);
Discourse.User.resetCurrent(currentUser);
}

View File

@ -0,0 +1,71 @@
import { acceptance, replaceCurrentUser } from "helpers/qunit-helpers";
acceptance("Managing Group Interaction Settings", {
loggedIn: true,
settings: {
email_in: true
}
});
QUnit.test("As an admin", assert => {
visit("/groups/discourse/manage/interaction");
andThen(() => {
assert.equal(
find('.groups-form-visibility-level').length, 1,
'it should display visibility level selector'
);
assert.equal(
find('.groups-form-mentionable-level').length, 1,
'it should display mentionable level selector'
);
assert.equal(
find('.groups-form-messageable-level').length, 1,
'it should display messageable level selector'
);
assert.equal(
find('.groups-form-incoming-email').length, 1,
'it should display incoming email input'
);
assert.equal(
find('.groups-form-default-notification-level').length, 1,
'it should display default notification level input'
);
});
});
QUnit.test("As a group owner", assert => {
replaceCurrentUser({ admin: false, staff: false });
visit("/groups/discourse/manage/interaction");
andThen(() => {
assert.equal(
find('.groups-form-visibility-level').length, 0,
'it should display visibility level selector'
);
assert.equal(
find('.groups-form-mentionable-level').length, 1,
'it should display mentionable level selector'
);
assert.equal(
find('.groups-form-messageable-level').length, 1,
'it should display messageable level selector'
);
assert.equal(
find('.groups-form-incoming-email').length, 0,
'it should not display incoming email input'
);
assert.equal(
find('.groups-form-default-notification-level').length, 1,
'it should display default notification level input'
);
});
});

View File

@ -0,0 +1,119 @@
import { acceptance, replaceCurrentUser } from "helpers/qunit-helpers";
acceptance("Managing Group Membership", {
loggedIn: true
});
QUnit.test("As an admin", assert => {
visit("/groups/discourse/manage/membership");
andThen(() => {
assert.ok(
find('label[for="automatic_membership"]').length === 1,
'it should display automatic membership label'
);
assert.ok(
find('.groups-form-automatic-membership-retroactive').length === 1,
'it should display automatic membership retroactive checkbox'
);
assert.ok(
find('.groups-form-primary-group').length === 1,
'it should display set as primary group checkbox'
);
assert.ok(
find('.groups-form-grant-trust-level').length === 1,
'it should display grant trust level selector'
);
assert.ok(
find('.group-form-public-admission').length === 1,
'it should display group public admission input'
);
assert.ok(
find('.group-form-public-exit').length === 1,
'it should display group public exit input'
);
assert.ok(
find('.group-form-allow-membership-requests').length === 1,
'it should display group allow_membership_request input'
);
assert.ok(
find('.group-form-allow-membership-requests[disabled]').length === 1,
'it should disable group allow_membership_request input'
);
});
click('.group-form-public-admission');
click('.group-form-allow-membership-requests');
andThen(() => {
assert.ok(
find('.group-form-public-admission[disabled]').length === 1,
'it should disable group public admission input'
);
assert.ok(
find('.group-form-public-exit[disabled]').length === 0,
'it should not disable group public exit input'
);
assert.equal(
find('.group-form-membership-request-template').length, 1,
'it should display the membership request template field'
);
});
});
QUnit.test("As a group owner", assert => {
replaceCurrentUser({ staff: false, admin: false });
visit("/groups/discourse/manage/membership");
andThen(() => {
assert.ok(
find('label[for="automatic_membership"]').length === 0,
'it should not display automatic membership label'
);
assert.ok(
find('.groups-form-automatic-membership-retroactive').length === 0,
'it should not display automatic membership retroactive checkbox'
);
assert.ok(
find('.groups-form-primary-group').length === 0,
'it should not display set as primary group checkbox'
);
assert.ok(
find('.groups-form-grant-trust-level').length === 0,
'it should not display grant trust level selector'
);
assert.ok(
find('.group-form-public-admission').length === 1,
'it should display group public admission input'
);
assert.ok(
find('.group-form-public-exit').length === 1,
'it should display group public exit input'
);
assert.ok(
find('.group-form-allow-membership-requests').length === 1,
'it should display group allow_membership_request input'
);
assert.ok(
find('.group-form-allow-membership-requests[disabled]').length === 1,
'it should disable group allow_membership_request input'
);
});
});

View File

@ -1,8 +1,8 @@
import { acceptance, logIn } from "helpers/qunit-helpers";
import { acceptance, logIn, replaceCurrentUser } from "helpers/qunit-helpers";
acceptance("Managing Group Profile");
QUnit.test("Editing group", assert => {
QUnit.test("As an admin", assert => {
logIn();
Discourse.reset();
@ -13,50 +13,25 @@ QUnit.test("Editing group", assert => {
assert.ok(find('.group-form-bio').length === 1, 'it should display group bio input');
assert.ok(find('.group-form-name').length === 1, 'it should display group name input');
assert.ok(find('.group-form-full-name').length === 1, 'it should display group full name input');
assert.ok(
find('.group-form-public-admission').length === 1,
'it should display group public admission input'
);
assert.ok(
find('.group-form-public-exit').length === 1,
'it should display group public exit input'
);
assert.ok(
find('.group-form-allow-membership-requests').length === 1,
'it should display group allow_membership_request input'
);
assert.ok(
find('.group-form-allow-membership-requests[disabled]').length === 1,
'it should disable group allow_membership_request input'
);
});
});
click('.group-form-public-admission');
click('.group-form-allow-membership-requests');
QUnit.test("As a group owner", assert => {
logIn();
Discourse.reset();
replaceCurrentUser({ staff: false, admin: false });
visit("/groups/discourse/manage/profile");
andThen(() => {
assert.ok(
find('.group-form-public-admission[disabled]').length === 1,
'it should disable group public admission input'
);
assert.ok(
find('.group-form-public-exit[disabled]').length === 0,
'it should not disable group public exit input'
);
assert.equal(
find('.group-form-membership-request-template').length, 1,
'it should display the membership request template field'
find('.group-form-name').length, 0,
'it should not display group name input'
);
});
});
QUnit.test("Editing group as an anonymous user", assert => {
QUnit.test("As an anonymous user", assert => {
visit("/groups/discourse/manage/profile");
andThen(() => {

View File

@ -167,18 +167,3 @@ QUnit.test("Admin Viewing Group", assert => {
assert.equal(find('.group-info-name').text(), 'Awesome Team', 'it should display the group name');
});
});
QUnit.test("Admin Viewing Automatic Group", assert => {
logIn();
Discourse.reset();
visit("/groups/moderators");
click(".nav-pills li a[title='Manage']");
andThen(() => {
assert.equal(
count('.groups-form .control-group'), 5,
'it should display the right fields'
);
});
});

View File

@ -1,4 +1,3 @@
export default {
"/session/current.json": {"current_user":{"id":19,"username":"eviltrout","uploaded_avatar_id":5275,"avatar_template":"/user_avatar/localhost/eviltrout/{size}/5275.png","name":"Robin Ward","total_unread_notifications":205,"unread_notifications":0,"unread_private_messages":0,"admin":true,"notification_channel_position":null,"site_flagged_posts_count":1,"moderator":true,"staff":true,"title":"co-founder","reply_count":859,"topic_count":36,"enable_quoting":true,"external_links_in_new_tab":false,"dynamic_favicon":true,"trust_level":4,"can_edit":true,"can_invite_to_forum":true,"should_be_redirected_to_top":false,"disable_jump_reply":false,"custom_fields":{},"muted_category_ids":[],"dismissed_banner_key":null,"akismet_review_count":0}}
};

View File

@ -15,6 +15,12 @@ export function currentUser() {
return Discourse.User.create(sessionFixtures['/session/current.json'].current_user);
}
export function replaceCurrentUser(properties) {
const user = Discourse.User.current();
user.setProperties(properties);
Discourse.User.resetCurrent(user);
}
export function logIn() {
Discourse.User.resetCurrent(currentUser());
}