FEATURE: invite page tabs

This commit is contained in:
Arpit Jalan 2015-07-11 17:39:12 +05:30
parent 84549929ba
commit e0c9054748
27 changed files with 170 additions and 93 deletions

View File

@ -4,26 +4,26 @@
<div class="full-width">
<ul class="nav nav-pills">
{{admin-nav-item route='admin.dashboard' label='admin.dashboard.title'}}
{{nav-item route='admin.dashboard' label='admin.dashboard.title'}}
{{#if currentUser.admin}}
{{admin-nav-item route='adminSiteSettings' label='admin.site_settings.title'}}
{{nav-item route='adminSiteSettings' label='admin.site_settings.title'}}
{{/if}}
{{admin-nav-item route='adminUsersList.show' routeParam='active' label='admin.users.title'}}
{{nav-item route='adminUsersList.show' routeParam='active' label='admin.users.title'}}
{{#if showBadges}}
{{admin-nav-item route='adminBadges.index' label='admin.badges.title'}}
{{nav-item route='adminBadges.index' label='admin.badges.title'}}
{{/if}}
{{#if currentUser.admin}}
{{admin-nav-item route='adminGroups' label='admin.groups.title'}}
{{nav-item route='adminGroups' label='admin.groups.title'}}
{{/if}}
{{admin-nav-item route='adminEmail' label='admin.email.title'}}
{{admin-nav-item route='adminFlags' label='admin.flags.title'}}
{{admin-nav-item route='adminLogs' label='admin.logs.title'}}
{{nav-item route='adminEmail' label='admin.email.title'}}
{{nav-item route='adminFlags' label='admin.flags.title'}}
{{nav-item route='adminLogs' label='admin.logs.title'}}
{{#if currentUser.admin}}
{{admin-nav-item route='adminCustomize.colors' label='admin.customize.title'}}
{{admin-nav-item route='admin.api' label='admin.api.title'}}
{{admin-nav-item route='admin.backups' label='admin.backups.title'}}
{{nav-item route='adminCustomize.colors' label='admin.customize.title'}}
{{nav-item route='admin.api' label='admin.api.title'}}
{{nav-item route='admin.backups' label='admin.backups.title'}}
{{/if}}
{{admin-nav-item route='adminPlugins' label='admin.plugins.title'}}
{{nav-item route='adminPlugins' label='admin.plugins.title'}}
{{plugin-outlet "admin-menu" tagName="li"}}
</ul>

View File

@ -1,8 +1,8 @@
<div class="admin-controls">
<div class="span15">
<ul class="nav nav-pills">
{{admin-nav-item route='admin.backups.index' label='admin.backups.menu.backups'}}
{{admin-nav-item route='admin.backups.logs' label='admin.backups.menu.logs'}}
{{nav-item route='admin.backups.index' label='admin.backups.menu.backups'}}
{{nav-item route='admin.backups.logs' label='admin.backups.menu.logs'}}
</ul>
</div>
<div class="pull-right">

View File

@ -1,9 +1,9 @@
{{#admin-nav}}
{{admin-nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
{{admin-nav-item route='adminCustomize.css_html' label='admin.customize.css_html.title'}}
{{admin-nav-item route='adminSiteText' label='admin.site_text.title'}}
{{admin-nav-item route='adminUserFields' label='admin.user_fields.title'}}
{{admin-nav-item route='adminEmojis' label='admin.emoji.title'}}
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
{{nav-item route='adminCustomize.css_html' label='admin.customize.css_html.title'}}
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -1,9 +1,9 @@
{{#admin-nav}}
{{admin-nav-item route='adminEmail.index' label='admin.email.settings'}}
{{admin-nav-item route='adminEmail.all' label='admin.email.all'}}
{{admin-nav-item route='adminEmail.sent' label='admin.email.sent'}}
{{admin-nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
{{admin-nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
{{nav-item route='adminEmail.index' label='admin.email.settings'}}
{{nav-item route='adminEmail.all' label='admin.email.all'}}
{{nav-item route='adminEmail.sent' label='admin.email.sent'}}
{{nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -1,6 +1,6 @@
{{#admin-nav}}
{{admin-nav-item route='adminFlags.list' routeParam='active' label='admin.flags.active'}}
{{admin-nav-item route='adminFlags.list' routeParam='old' label='admin.flags.old'}}
{{nav-item route='adminFlags.list' routeParam='active' label='admin.flags.active'}}
{{nav-item route='adminFlags.list' routeParam='old' label='admin.flags.old'}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -1,6 +1,6 @@
{{#admin-nav}}
{{admin-nav-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom'}}
{{admin-nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic'}}
{{nav-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom'}}
{{nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic'}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -1,10 +1,10 @@
{{#admin-nav}}
{{admin-nav-item route='adminLogs.staffActionLogs' label='admin.logs.staff_actions.title'}}
{{admin-nav-item route='adminLogs.screenedEmails' label='admin.logs.screened_emails.title'}}
{{admin-nav-item route='adminLogs.screenedIpAddresses' label='admin.logs.screened_ips.title'}}
{{admin-nav-item route='adminLogs.screenedUrls' label='admin.logs.screened_urls.title'}}
{{nav-item route='adminLogs.staffActionLogs' label='admin.logs.staff_actions.title'}}
{{nav-item route='adminLogs.screenedEmails' label='admin.logs.screened_emails.title'}}
{{nav-item route='adminLogs.screenedIpAddresses' label='admin.logs.screened_ips.title'}}
{{nav-item route='adminLogs.screenedUrls' label='admin.logs.screened_urls.title'}}
{{#if currentUser.admin}}
{{admin-nav-item path='/logs' label='admin.logs.logster.title'}}
{{nav-item path='/logs' label='admin.logs.logster.title'}}
{{/if}}
{{/admin-nav}}

View File

@ -1,9 +1,9 @@
<div class="admin-nav pull-left">
<ul class="nav nav-stacked">
{{admin-nav-item route='adminPlugins.index' label="admin.plugins.title"}}
{{nav-item route='adminPlugins.index' label="admin.plugins.title"}}
{{#each route in adminRoutes}}
{{admin-nav-item route=route.full_location label=route.label}}
{{nav-item route=route.full_location label=route.label}}
{{/each}}
</ul>
</div>

View File

@ -1,15 +1,15 @@
<div class='admin-controls'>
<div class='span15'>
<ul class="nav nav-pills">
{{admin-nav-item route='adminUsersList.show' routeParam='active' label='admin.users.nav.active'}}
{{admin-nav-item route='adminUsersList.show' routeParam='new' label='admin.users.nav.new'}}
{{nav-item route='adminUsersList.show' routeParam='active' label='admin.users.nav.active'}}
{{nav-item route='adminUsersList.show' routeParam='new' label='admin.users.nav.new'}}
{{#if siteSettings.must_approve_users}}
{{admin-nav-item route='adminUsersList.show' routeParam='pending' label='admin.users.nav.pending'}}
{{nav-item route='adminUsersList.show' routeParam='pending' label='admin.users.nav.pending'}}
{{/if}}
{{admin-nav-item route='adminUsersList.show' routeParam='staff' label='admin.users.nav.staff'}}
{{admin-nav-item route='adminUsersList.show' routeParam='suspended' label='admin.users.nav.suspended'}}
{{admin-nav-item route='adminUsersList.show' routeParam='blocked' label='admin.users.nav.blocked'}}
{{admin-nav-item route='adminUsersList.show' routeParam='suspect' label='admin.users.nav.suspect'}}
{{nav-item route='adminUsersList.show' routeParam='staff' label='admin.users.nav.staff'}}
{{nav-item route='adminUsersList.show' routeParam='suspended' label='admin.users.nav.suspended'}}
{{nav-item route='adminUsersList.show' routeParam='blocked' label='admin.users.nav.blocked'}}
{{nav-item route='adminUsersList.show' routeParam='suspect' label='admin.users.nav.suspect'}}
</ul>
</div>
<div class="pull-right">

View File

@ -3,7 +3,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
export default ObjectController.extend(Presence, ModalFunctionality, {
needs: ['user-invited'],
needs: ['user-invited-show'],
// If this isn't defined, it will proxy to the user model on the preferences
// page which is wrong.
@ -132,14 +132,14 @@ export default ObjectController.extend(Presence, ModalFunctionality, {
if (this.get('disabled')) { return; }
const groupNames = this.get('groupNames'),
userInvitedController = this.get('controllers.user-invited');
userInvitedController = this.get('controllers.user-invited-show');
this.setProperties({ saving: true, error: false });
return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames).then(result => {
this.setProperties({ saving: false, finished: true });
if (!this.get('invitingToTopic')) {
Discourse.Invite.findInvitedBy(Discourse.User.current()).then(invite_model => {
Discourse.Invite.findInvitedBy(Discourse.User.current(), userInvitedController.get('filter')).then(invite_model => {
userInvitedController.set('model', invite_model);
userInvitedController.set('totalInvites', invite_model.invites.length);
});

View File

@ -2,6 +2,7 @@
export default Ember.ObjectController.extend({
user: null,
model: null,
filter: null,
totalInvites: null,
canLoadMore: true,
invitesLoading: false,
@ -20,11 +21,13 @@ export default Ember.ObjectController.extend({
**/
_searchTermChanged: Discourse.debounce(function() {
var self = this;
Discourse.Invite.findInvitedBy(self.get('user'), this.get('searchTerm')).then(function (invites) {
Discourse.Invite.findInvitedBy(self.get('user'), this.get('filter'), this.get('searchTerm')).then(function (invites) {
self.set('model', invites);
});
}, 250).observes('searchTerm'),
inviteRedeemed: Em.computed.equal('filter', 'redeemed'),
/**
Can the currently logged in user invite users to the site
@ -82,7 +85,7 @@ export default Ember.ObjectController.extend({
if (self.get('canLoadMore') && !self.get('invitesLoading')) {
self.set('invitesLoading', true);
Discourse.Invite.findInvitedBy(self.get('user'), self.get('searchTerm'), model.invites.length).then(function(invite_model) {
Discourse.Invite.findInvitedBy(self.get('user'), self.get('filter'), self.get('searchTerm'), model.invites.length).then(function(invite_model) {
self.set('invitesLoading', false);
model.invites.pushObjects(invite_model.invites);
if(invite_model.invites.length === 0 || invite_model.invites.length < Discourse.SiteSettings.invites_per_page) {

View File

@ -37,11 +37,12 @@ Discourse.Invite.reopenClass({
return result;
},
findInvitedBy: function(user, filter, offset) {
findInvitedBy: function(user, filter, search, offset) {
if (!user) { return Em.RSVP.resolve(); }
var data = {};
if (!Em.isNone(filter)) { data.filter = filter; }
if (!Em.isNone(search)) { data.search = search; }
data.offset = offset || 0;
return Discourse.ajax("/users/" + user.get('username_lower') + "/invited.json", {data: data}).then(function (result) {

View File

@ -79,7 +79,9 @@ export default function() {
this.route('card-badge', { path: '/card-badge' });
});
this.route('invited');
this.resource('userInvited', { path: '/invited' }, function() {
this.route('show', { path: '/:filter' });
});
});
this.route('signup', {path: '/signup'});

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
beforeModel: function() {
this.replaceWith('userInvited.show', 'redeemed');
}
});

View File

@ -2,18 +2,17 @@ import ShowFooter from 'discourse/mixins/show-footer';
import showModal from 'discourse/lib/show-modal';
export default Discourse.Route.extend(ShowFooter, {
renderTemplate() {
this.render({ into: 'user' });
},
model() {
return Discourse.Invite.findInvitedBy(this.modelFor('user'));
model: function(params) {
this.inviteFilter = params.filter;
return Discourse.Invite.findInvitedBy(this.modelFor('user'), params.filter);
},
setupController(controller, model) {
controller.setProperties({
model: model,
user: this.controllerFor('user').get('model'),
filter: this.inviteFilter,
searchTerm: '',
totalInvites: model.invites.length
});

View File

@ -1,32 +1,46 @@
{{#if canInviteToForum}}
<section class='user-content'>
<h2>{{i18n 'user.invited.title'}}</h2>
<div class="pull-right">
<button {{action "showInvite"}} class='btn'>{{i18n 'user.invited.create'}}</button>
{{#if canBulkInvite}}
{{resumable-upload target="/invites/upload" success="uploadSuccess" error="uploadError" uploadText=uploadText}}
{{/if}}
</div>
{{#if can_see_invite_details}}
<div class='user-invite-controls'>
<div class='span15'>
<ul class="nav nav-pills">
{{nav-item route='userInvited.show' routeParam='redeemed' label='user.invited.redeemed_tab'}}
{{nav-item route='userInvited.show' routeParam='pending' label='user.invited.pending_tab'}}
</ul>
</div>
<div class="pull-right">
{{d-button action="showInvite" label='user.invited.create' class='btn'}}
{{#if canBulkInvite}}
{{resumable-upload target="/invites/upload" success="uploadSuccess" error="uploadError" uploadText=uploadText}}
{{/if}}
</div>
</div>
{{/if}}
{{#if showSearch}}
<form>
{{text-field value=searchTerm placeholderKey="user.invited.search"}}
</form>
<div class="user-invite-search">
<form>{{text-field value=searchTerm placeholderKey="user.invited.search"}}</form>
</div>
{{/if}}
{{#if model.invites}}
<table class='table invite-list'>
<table class='table user-invite-list'>
<tr>
<th>{{i18n 'user.invited.user'}}</th>
<th>{{i18n 'user.invited.redeemed_at'}}</th>
{{#if can_see_invite_details}}
<th>{{i18n 'user.last_seen'}}</th>
<th>{{i18n 'user.invited.topics_entered'}}</th>
<th>{{i18n 'user.invited.posts_read_count'}}</th>
<th>{{i18n 'user.invited.time_read'}}</th>
<th>{{i18n 'user.invited.days_visited'}}</th>
{{#if inviteRedeemed}}
<th>{{i18n 'user.invited.user'}}</th>
<th>{{i18n 'user.invited.redeemed_at'}}</th>
{{#if can_see_invite_details}}
<th>{{i18n 'user.last_seen'}}</th>
<th>{{i18n 'user.invited.topics_entered'}}</th>
<th>{{i18n 'user.invited.posts_read_count'}}</th>
<th>{{i18n 'user.invited.time_read'}}</th>
<th>{{i18n 'user.invited.days_visited'}}</th>
{{/if}}
{{else}}
<th colspan="7">{{i18n 'user.invited.user'}}</th>
{{/if}}
</tr>
{{#each invite in model.invites}}
@ -56,13 +70,13 @@
{{#if invite.rescinded}}
{{i18n 'user.invited.rescinded'}}
{{else}}
<button class='btn' {{action "rescind" invite}}><i class="fa fa-times"></i>{{i18n 'user.invited.rescind'}}</button>
{{d-button icon="times" action="rescind" actionParam=invite class="btn" label="user.invited.rescind"}}
{{/if}}
&nbsp;&nbsp;&nbsp;&nbsp;
{{#if invite.reinvited}}
{{i18n 'user.invited.reinvited'}}
{{else}}
<button class='btn' {{action "reinvite" invite}}><i class="fa fa-user-plus"></i>{{i18n 'user.invited.reinvite'}}</button>
{{d-button icon="user-plus" action="reinvite" actionParam=invite class="btn" label="user.invited.reinvite"}}
{{/if}}
</td>
{{/if}}
@ -72,12 +86,13 @@
{{conditional-loading-spinner condition=invitesLoading}}
{{else}}
{{#if canBulkInvite}}
{{{i18n 'user.invited.bulk_invite.none'}}}
{{else}}
{{i18n 'user.invited.none'}}
{{/if}}
<div class="user-invite-none">
{{#if canBulkInvite}}
{{{i18n 'user.invited.bulk_invite.none'}}}
{{else}}
{{i18n 'user.invited.none'}}
{{/if}}
</div>
{{/if}}
</section>
{{/if}}

View File

@ -55,7 +55,7 @@
<li>{{#link-to 'preferences' class="btn right"}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}</li>
{{/if}}
{{#if canInviteToForum}}
<li>{{#link-to 'user.invited' class="btn right"}}{{fa-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}</li>
<li>{{#link-to 'userInvited' class="btn right"}}{{fa-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}</li>
{{/if}}
</ul>
</section>

View File

@ -2,6 +2,6 @@ import LoadMore from "discourse/mixins/load-more";
export default Ember.View.extend(LoadMore, {
classNames: ['paginated-topics-list'],
eyelineSelector: '.paginated-topics-list .invite-list tr',
templateName: 'user/invited'
eyelineSelector: '.paginated-topics-list .user-invite-list tr',
templateName: 'user-invited-show'
});

View File

@ -184,6 +184,26 @@
max-height: 45px;
}
}
.user-invite-list {
margin-top: 15px;
}
.user-invite-controls {
background-color: dark-light-diff($primary, $secondary, 90%, -75%);
padding: 5px 10px 0px 0;
height: 35px;
}
.user-invite-search {
clear: both;
margin: 15px 0px -15px 0px;
}
.user-invite-none {
clear: both;
padding: 15px;
}
}
.about {

View File

@ -168,14 +168,15 @@ class UsersController < ApplicationController
def invited
inviter = fetch_user_from_params
offset = params[:offset].to_i || 0
filter_by = params[:filter]
invites = if guardian.can_see_invite_details?(inviter)
Invite.find_all_invites_from(inviter, offset)
invites = if guardian.can_see_invite_details?(inviter) && filter_by == "pending"
Invite.find_pending_invites_from(inviter, offset)
else
Invite.find_redeemed_invites_from(inviter, offset)
end
invites = invites.filter_by(params[:filter])
invites = invites.filter_by(params[:search])
render_json_dump invites: serialize_data(invites.to_a, InviteSerializer),
can_see_invite_details: guardian.can_see_invite_details?(inviter)
end

View File

@ -162,8 +162,12 @@ class Invite < ActiveRecord::Base
.references('user_stats')
end
def self.find_pending_invites_from(inviter, offset=0)
find_all_invites_from(inviter, offset).where('invites.user_id IS NULL').order('invites.created_at DESC')
end
def self.find_redeemed_invites_from(inviter, offset=0)
find_all_invites_from(inviter, offset).where('invites.user_id IS NOT NULL')
find_all_invites_from(inviter, offset).where('invites.user_id IS NOT NULL').order('invites.redeemed_at DESC')
end
def self.filter_by(email_or_username)

View File

@ -572,8 +572,10 @@ en:
none: "You haven't invited anyone here yet."
truncated: "Showing the first {{count}} invites."
redeemed: "Redeemed Invites"
redeemed_tab: "Redeemed"
redeemed_at: "Redeemed"
pending: "Pending Invites"
pending_tab: "Pending"
topics_entered: "Topics Viewed"
posts_read_count: "Posts Read"
expired: "This invite has expired."

View File

@ -268,6 +268,7 @@ Discourse::Application.routes.draw do
get "users/:username/staff-info" => "users#staff_info", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/invited" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/invited/:filter" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT}
post "users/action/send_activation_email" => "users#send_activation_email"
get "users/:username/activity" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/activity/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}

View File

@ -911,7 +911,7 @@ describe UsersController do
user: invitee
)
xhr :get, :invited, username: inviter.username, filter: 'billybob'
xhr :get, :invited, username: inviter.username, search: 'billybob'
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(1)
@ -933,7 +933,7 @@ describe UsersController do
user: Fabricate(:user, username: 'jimtom')
)
xhr :get, :invited, username: inviter.username, filter: 'billybob'
xhr :get, :invited, username: inviter.username, search: 'billybob'
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(1)
@ -946,7 +946,7 @@ describe UsersController do
inviter = Fabricate(:user)
Fabricate(:invite, invited_by: inviter)
xhr :get, :invited, username: inviter.username
xhr :get, :invited, username: inviter.username, filter: 'pending'
invites = JSON.parse(response.body)['invites']
expect(invites).to be_empty
@ -980,7 +980,7 @@ describe UsersController do
with(inviter).returns(true)
end
xhr :get, :invited, username: inviter.username
xhr :get, :invited, username: inviter.username, filter: 'pending'
invites = JSON.parse(response.body)['invites']
expect(invites.size).to eq(1)
@ -999,7 +999,7 @@ describe UsersController do
with(inviter).returns(false)
end
xhr :get, :invited, username: inviter.username
xhr :get, :invited, username: inviter.username, filter: 'pending'
json = JSON.parse(response.body)['invites']
expect(json).to be_empty

View File

@ -387,6 +387,30 @@ describe Invite do
end
end
describe '.find_pending_invites_from' do
it 'returns pending invites only' do
inviter = Fabricate(:user)
Fabricate(
:invite,
invited_by: inviter,
user_id: 123,
email: 'redeemed@example.com'
)
pending_invite = Fabricate(
:invite,
invited_by: inviter,
user_id: nil,
email: 'pending@example.com'
)
invites = Invite.find_pending_invites_from(inviter)
expect(invites.size).to eq(1)
expect(invites.first).to eq pending_invite
end
end
describe '.find_redeemed_invites_from' do
it 'returns redeemed invites only' do
inviter = Fabricate(:user)