Index: /trunk/src/bp-core/bp-core-rest-api.php =================================================================== --- /trunk/src/bp-core/bp-core-rest-api.php (revision 12405) +++ /trunk/src/bp-core/bp-core-rest-api.php (revision 12405) @@ -0,0 +1,77 @@ +plugin_url, bp_core_get_minified_asset_suffix() ), + $dependencies, + bp_get_version(), + true + ); + wp_localize_script( + 'bp-api-request', + 'bpApiSettings', + array( + 'root' => esc_url_raw( get_rest_url() ), + 'nonce' => wp_create_nonce( 'wp_rest' ), + ) + ); +} +add_action( 'bp_init', 'bp_rest_api_register_request_script' ); Index: /trunk/src/bp-core/js/bp-api-request.js =================================================================== --- /trunk/src/bp-core/js/bp-api-request.js (revision 12405) +++ /trunk/src/bp-core/js/bp-api-request.js (revision 12405) @@ -0,0 +1,34 @@ +/** + * jQuery.ajax wrapper for BP REST API requests. + * + * @since 5.0.0 + * @output bp-core/js/bp-api-request.js + */ +/* global bpApiSettings */ +window.bp = window.bp || {}; + +( function( wp, bp, $ ) { + // Bail if not set + if ( typeof bpApiSettings === 'undefined' ) { + return; + } + + bp.isRestEnabled = true; + + // Polyfill wp.apiRequest if WordPress < 4.9 + bp.apiRequest = wp.apiRequest || function( options ) { + var url = bpApiSettings.root; + + if ( options.path ) { + url = url + options.path.replace( /^\//, '' ); + } + + options.url = url; + options.beforeSend = function( xhr ) { + xhr.setRequestHeader( 'X-WP-Nonce', bpApiSettings.nonce ); + }; + + return $.ajax( options ); + }; + +} )( window.wp || {}, window.bp, jQuery ); Index: /trunk/src/bp-groups/admin/css/admin-rtl.css =================================================================== --- /trunk/src/bp-groups/admin/css/admin-rtl.css (revision 12404) +++ /trunk/src/bp-groups/admin/css/admin-rtl.css (revision 12405) @@ -141,4 +141,37 @@ } +#group-manage-members-ui .subnav-filters .filter.last { + float: left; +} + +#group-manage-members-ui .subnav-filters .left-menu { + float: right; +} + +#group-manage-members-ui .subnav-filters .group-members-paginate-button:last-child { + margin-left: 2em; +} + +#group-manage-members-ui .subnav-filters .dashicons { + padding-top: 2px; +} + +#group-manage-members-ui .uname-column .profile-photo { + margin-left: 1em; +} + +#bp-no-group-members .bp-feedback.info { + background: #fff; + border-right: 4px solid #00a0d2; + box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1); + margin: 5px 5px 2px; + padding: 1px 12px; +} + +#bp-no-group-members .bp-feedback.info p { + margin: 0.5em 0; + padding: 2px; +} + @media screen and (max-width: 782px) { Index: /trunk/src/bp-groups/admin/css/admin.css =================================================================== --- /trunk/src/bp-groups/admin/css/admin.css (revision 12404) +++ /trunk/src/bp-groups/admin/css/admin.css (revision 12405) @@ -141,4 +141,37 @@ } +#group-manage-members-ui .subnav-filters .filter.last { + float: right; +} + +#group-manage-members-ui .subnav-filters .left-menu { + float: left; +} + +#group-manage-members-ui .subnav-filters .group-members-paginate-button:last-child { + margin-right: 2em; +} + +#group-manage-members-ui .subnav-filters .dashicons { + padding-top: 2px; +} + +#group-manage-members-ui .uname-column .profile-photo { + margin-right: 1em; +} + +#bp-no-group-members .bp-feedback.info { + background: #fff; + border-left: 4px solid #00a0d2; + box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1); + margin: 5px 5px 2px; + padding: 1px 12px; +} + +#bp-no-group-members .bp-feedback.info p { + margin: 0.5em 0; + padding: 2px; +} + @media screen and (max-width: 782px) { Index: /trunk/src/bp-groups/bp-groups-admin.php =================================================================== --- /trunk/src/bp-groups/bp-groups-admin.php (revision 12404) +++ /trunk/src/bp-groups/bp-groups-admin.php (revision 12405) @@ -892,4 +892,22 @@ */ function bp_groups_admin_edit_metabox_members( $item ) { + // Use the BP REST API if it supported. + if ( bp_rest_api_is_available() && bp_groups_has_manage_group_members_templates() ) { + wp_enqueue_script( 'bp-group-manage-members' ); + wp_localize_script( + 'bp-group-manage-members', + 'bpGroupManageMembersSettings', + bp_groups_get_group_manage_members_script_data( $item->id ) + ); + + bp_get_template_part( 'common/js-templates/group-members/index' ); + + /** + * Echo out the JavaScript variable. + * This seems to be required by the autocompleter, leaving this here for now... + */ + echo ''; + return; + } // Pull up a list of group members, so we can separate out the types Index: /trunk/src/bp-groups/bp-groups-cssjs.php =================================================================== --- /trunk/src/bp-groups/bp-groups-cssjs.php (revision 12405) +++ /trunk/src/bp-groups/bp-groups-cssjs.php (revision 12405) @@ -0,0 +1,62 @@ +plugin_url, bp_core_get_minified_asset_suffix() ), + array( 'json2', 'wp-backbone', 'bp-api-request' ), + bp_get_version(), + true + ); +} +add_action( 'bp_enqueue_scripts', 'bp_groups_register_scripts', 1 ); +add_action( 'bp_admin_enqueue_scripts', 'bp_groups_register_scripts', 1 ); + +/** + * Get JavaScript data for the Manage Group Members UI. + * + * @since 5.0.0 + * + * @param integer $group_id Required. The Group ID whose members has to be managed. + * @return array The JavaScript data. + */ +function bp_groups_get_group_manage_members_script_data( $group_id = 0 ) { + if ( ! $group_id ) { + return array(); + } else { + $group_id = (int) $group_id; + } + + $path = sprintf( '/%1$s/%2$s/%3$s/%4$s/members?exclude_admins=false', + bp_rest_namespace(), + bp_rest_version(), + buddypress()->groups->id, + $group_id + ); + + $preloaded_members = array(); + if ( function_exists( 'rest_preload_api_request' ) ) { + $preloaded_members = rest_preload_api_request( '', $path ); + } + + return array( + 'path' => remove_query_arg( 'exclude_admins', $path ), + 'preloaded' => reset( $preloaded_members ), + 'roles' => bp_groups_get_group_roles(), + ); +} Index: /trunk/src/bp-groups/bp-groups-functions.php =================================================================== --- /trunk/src/bp-groups/bp-groups-functions.php (revision 12404) +++ /trunk/src/bp-groups/bp-groups-functions.php (revision 12405) @@ -1113,4 +1113,48 @@ /** + * Get the Group roles. + * + * @since 5.0.0 + * + * @return array The list of Group role objects. + */ +function bp_groups_get_group_roles() { + return array( + 'admin' => (object) array( + 'id' => 'admin', + 'name' => __( 'Administrator', 'buddypress' ), + 'is_admin' => true, + 'is_banned' => false, + 'is_confirmed' => true, + 'is_mod' => false, + ), + 'mod' => (object) array( + 'id' => 'mod', + 'name' => __( 'Moderator', 'buddypress' ), + 'is_admin' => false, + 'is_banned' => false, + 'is_confirmed' => true, + 'is_mod' => true, + ), + 'member' => (object) array( + 'id' => 'member', + 'name' => __( 'Member', 'buddypress' ), + 'is_admin' => false, + 'is_banned' => false, + 'is_confirmed' => true, + 'is_mod' => false, + ), + 'banned' => (object) array( + 'id' => 'banned', + 'name' => __( 'Banned', 'buddypress' ), + 'is_admin' => false, + 'is_banned' => true, + 'is_confirmed' => true, + 'is_mod' => false, + ), + ); +} + +/** * Check whether a user is an admin of a given group. * Index: /trunk/src/bp-groups/bp-groups-template.php =================================================================== --- /trunk/src/bp-groups/bp-groups-template.php (revision 12404) +++ /trunk/src/bp-groups/bp-groups-template.php (revision 12405) @@ -6218,2 +6218,14 @@ return apply_filters( 'bp_groups_get_profile_stats', $r['output'], $r ); } + +/** + * Check if the active template pack includes the Group Membership management UI templates. + * + * @since 5.0.0 + * + * @return boolean True if the active template pack includes the Group Membership management UI templates. + * False otherwise. + */ +function bp_groups_has_manage_group_members_templates() { + return file_exists( bp_locate_template( 'common/js-templates/group-members/index.php' ) ); +} Index: /trunk/src/bp-groups/classes/class-bp-groups-component.php =================================================================== --- /trunk/src/bp-groups/classes/class-bp-groups-component.php (revision 12404) +++ /trunk/src/bp-groups/classes/class-bp-groups-component.php (revision 12405) @@ -129,5 +129,6 @@ 'adminbar', 'functions', - 'notifications' + 'notifications', + 'cssjs', ); Index: /trunk/src/bp-groups/js/manage-members.js =================================================================== --- /trunk/src/bp-groups/js/manage-members.js (revision 12405) +++ /trunk/src/bp-groups/js/manage-members.js (revision 12405) @@ -0,0 +1,549 @@ +/* global bpGroupManageMembersSettings, _, Backbone */ +/* @version 5.0.0 */ + +( function( wp, bp, $ ) { + + // Bail if not set + if ( typeof bpGroupManageMembersSettings === 'undefined' || ! bp.isRestEnabled ) { + return; + } + + // Copy useful WP Objects into BP. + _.extend( bp, _.pick( wp, 'Backbone', 'template' ) ); + + bp.Models = bp.Models || {}; + bp.Collections = bp.Collections || {}; + bp.Views = bp.Views || {}; + + /** + * Model for the Member of the displayed group. + */ + bp.Models.groupMember = Backbone.Model.extend( { + defaults: { + id: 0, + name: '', + avatar_urls : {}, + is_admin: false, + is_banned: false, + is_confirmed: false, + is_mod: false, + link: '' + }, + options : { + path: bpGroupManageMembersSettings.path, + type: 'POST', + data: {}, + dataType: 'json' + }, + + initialize: function() { + // Make sure to reset data & path on model's sync. + this.on( 'sync', this.resetRequestOptions, this ); + }, + + resetRequestOptions: function() { + this.options.data = {}; + this.options.path = bpGroupManageMembersSettings.path; + }, + + sync: function( method, model, options ) { + options = options || {}; + options.context = this; + var data = options.data || {}; + this.options.path = this.options.path.concat( '/' + model.get( 'id' ) ); + + _.extend( options, this.options ); + _.extend( options.data, data ); + + if ( 'delete' === method || 'update' === method ) { + if ( 'delete' === method ) { + options.headers = { 'X-HTTP-Method-Override': 'DELETE' }; + } else { + options.headers = { 'X-HTTP-Method-Override': 'PUT' }; + } + + return bp.apiRequest( options ); + } + }, + + parse: function( response ) { + if ( _.isArray( response ) ) { + response = _.first( response ); + } + + return response; + } + } ); + + /** + * Collection for the Members of the displayed group. + */ + bp.Collections.groupMembers = Backbone.Collection.extend( { + model: bp.Models.groupMember, + options : { + path: bpGroupManageMembersSettings.path, + type: 'GET', + data: {}, + dataType: 'json' + }, + + initialize: function() { + // Make sure to reset data on collection's reset. + this.on( 'reset', function() { + this.options.data = {}; + }, this ); + }, + + sync: function( method, collection, options ) { + options = options || {}; + options.context = this; + var data = options.data || {}; + + _.extend( options, this.options ); + _.extend( options.data, data ); + + if ( 'read' === method ) { + var self = this, success = options.success; + options.success = function( data, textStatus, request ) { + if ( ! _.isUndefined( request ) ) { + self.totalPages = parseInt( request.getResponseHeader( 'X-WP-TotalPages' ), 10 ); + self.totalGroupMembers = parseInt( request.getResponseHeader( 'X-WP-Total' ), 10 ); + } + + self.currentPage = options.data.page || 1; + + if ( success ) { + return success.apply( this, arguments ); + } + }; + + return bp.apiRequest( options ); + } + } + } ); + + // Extend wp.Backbone.View with .prepare(). + bp.View = bp.View || bp.Backbone.View.extend( { + prepare: function() { + if ( ! _.isUndefined( this.model ) && _.isFunction( this.model.toJSON ) ) { + return this.model.toJSON(); + } else { + return {}; + } + } + } ); + + bp.Views.GroupMemberUpdatingInfo = bp.View.extend( { + tagName: 'p', + template : bp.template( 'bp-manage-members-updating' ), + + initialize: function() { + this.model = new Backbone.Model( { + type: this.options.value + } ); + } + } ); + + bp.Views.GroupMemberErrorInfo = bp.View.extend( { + tagName: 'p', + template : bp.template( 'bp-manage-members-error' ), + + initialize: function() { + this.model = new Backbone.Model( { + message: this.options.value + } ); + } + } ); + + bp.Views.GroupsMembersLabel = bp.Views.GroupMemberUpdatingInfo.extend( { + tagName: 'label', + template: bp.template( 'bp-manage-members-label' ) + } ); + + bp.Views.GroupRolesDropDown = bp.View.extend( { + tagName: 'select', + filters: _.extend( { all: { name: 'All members' } }, bpGroupManageMembersSettings.roles ), + + events: { + change: 'change' + }, + + initialize: function() { + if ( this.options.omits ) { + this.filters = _.omit( this.filters, this.options.omits ); + } + + // Build `' ).val( value ).html( filter.name )[0]; + + if ( this.options.currentRole && value === this.options.currentRole ) { + return { + el: $( optionOutput ).prop( 'selected', true ) + }; + } else { + return { + el: optionOutput + }; + } + }, this ).pluck( 'el' ).value() ); + }, + + change: function( event ) { + var role = $( event.target ).val(), queryArgs = { roles: [ role ] }; + + if ( ! this.collection ) { + return; + } + + if ( 'all' === role ) { + // Unset the current role. + this.collection.currentRole = ''; + + queryArgs = { 'exclude_admins': false }; + } else { + // Set the current role. + this.collection.currentRole = role; + } + + // Reset the current page. + this.collection.currentPage = 1; + + queryArgs.page = 1; + $( '#manage-members-search' ).val( '' ); + + this.collection.fetch( { + data: queryArgs, + reset: true + } ); + } + } ); + + bp.Views.GroupMembersSearch = bp.View.extend( { + className: 'bp-dir-search-form', + tagName: 'form', + template: bp.template( 'bp-manage-members-search' ), + + events: { + 'click #manage-members-search-submit' : 'searchMember' + }, + + searchMember: function( event ) { + event.preventDefault(); + + var searchTerms = $( '#manage-members-search' ).val(), + queryArgs = _.extend( this.collection.options.data, { search: searchTerms, page: 1 } ); + + // Reset the current page. + this.collection.currentPage = 1; + + if ( ! this.collection.currentRole ) { + queryArgs.exclude_admins = false; + } else { + queryArgs.roles = [ this.collection.currentRole ]; + } + + this.collection.fetch( { + data: queryArgs, + reset: true + } ); + } + } ); + + bp.Views.GroupsMembersPagination = bp.View.extend( { + className: 'bp-pagination', + template: bp.template( 'bp-manage-members-paginate' ), + + events: { + 'click .group-members-paginate-button' : 'queryPage' + }, + + initialize: function() { + this.collection.on( 'reset', this.setPagination, this ); + }, + + setPagination: function( collection ) { + var attributes = _.pick( collection, [ 'currentPage', 'totalGroupMembers', 'totalPages' ] ); + + if ( attributes.totalPages > 1 ) { + attributes.nextPage = attributes.currentPage + 1; + attributes.prevPage = attributes.currentPage - 1; + } + + this.model = new Backbone.Model( attributes ); + this.render(); + }, + + queryPage: function( event ) { + event.preventDefault(); + + var page = $( event.currentTarget ).data( 'page' ), + searchTerms = $( '#manage-members-search' ).val(), + queryArgs = _.extend( this.collection.options.data, { search: searchTerms, page: page } ); + + if ( ! this.collection.currentRole ) { + queryArgs.exclude_admins = false; + } else { + queryArgs.roles = [ this.collection.currentRole ]; + } + + this.collection.fetch( { + data: queryArgs, + reset: true + } ); + } + } ); + + bp.Views.GroupMembersNoMatches = bp.View.extend( { + tagName: 'tr', + template : bp.template( 'bp-manage-members-empty-row' ) + } ); + + bp.Views.GroupMembersListRow = bp.View.extend( { + tagName: 'tr', + template : bp.template( 'bp-manage-members-row' ), + + events: { + 'click .group-member-actions a' : 'doMemberAction', + 'change .group-member-edit select' : 'editMemberRole' + }, + + initialize: function() { + var roleProps = [ 'is_admin', 'is_banned', 'is_confirmed', 'is_mod' ], + self = this; + + _.each( bpGroupManageMembersSettings.roles, function( props ) { + if ( _.isMatch( self.model.attributes, _.pick( props, roleProps ) ) ) { + self.model.set( 'role', _.pick( props, ['id', 'name'] ), { silent: true } ); + } + } ); + + this.model.collection.on( 'reset', this.clearRow, this ); + }, + + clearRow: function() { + this.views.view.remove(); + }, + + renderEditForm: function() { + var userId = this.model.get( 'id' ); + + this.render(); + + this.views.set( '#edit-group-member-' + userId, [ + new bp.Views.GroupsMembersLabel( { value: userId, attributes: { 'for': 'group-member' + userId + '-role' } } ), + new bp.Views.GroupRolesDropDown( { id: 'group-member' + userId + '-role', omits: [ 'all', 'banned' ], currentRole: this.model.get( 'role' ).id } ).render() + ] ); + }, + + resetRow: function() { + this.model.set( 'editing', false ); + + return this.render(); + }, + + getRoleObject: function( roleId ) { + var roles = bpGroupManageMembersSettings.roles; + + if ( _.isUndefined( roles[ roleId ] ) ) { + return {}; + } + + return _.extend( + { role: _.pick( roles[ roleId ], ['id', 'name'] ) }, + _.pick( roles[ roleId ], [ 'is_admin', 'is_banned', 'is_confirmed', 'is_mod' ] ) + ); + }, + + doMemberAction: function( event ) { + event.preventDefault(); + + var action = $( event.target ).data( 'action' ), self = this; + + if ( 'edit' === action ) { + this.model.set( 'editing', true ); + return this.renderEditForm(); + + } else if ( 'abort' === action ) { + return this.resetRow(); + + } else if ( 'ban' === action || 'unban' === action ) { + var newRole = ( 'ban' === action ) ? 'banned' : 'member', roleObject = this.getRoleObject( newRole ); + + if ( ! roleObject ) { + return this.resetRow(); + } else { + this.model.set( 'managingBan', true ); + this.render(); + } + + // Display user feedback. + this.views.set( '#edit-group-member-' + this.model.get( 'id' ), new bp.Views.GroupMemberUpdatingInfo( { value: action } ).render() ); + + // Update Group member's role. + this.model.save( roleObject, { + wait: true, + data: { action: action }, + success: function( model) { + self.model.collection.remove( model ); + return self.clearRow(); + }, + error: function( model, response ) { + self.views.set( '#edit-group-member-' + model.get( 'id' ), new bp.Views.GroupMemberErrorInfo( { value: response.responseJSON.message } ).render() ); + + // Make sure to reset request options. + model.resetRequestOptions(); + model.set( 'managingBan', false ); + } + } ); + } else if ( 'remove' === action ) { + this.model.set( 'removing', true ); + this.render(); + + // Display user feedback. + this.views.set( '#edit-group-member-' + this.model.get( 'id' ), new bp.Views.GroupMemberUpdatingInfo( { value: action } ).render() ); + + // Destroy the membership model. + this.model.destroy( { + wait: true, + data: {}, + success: function() { + return self.clearRow(); + }, + error: function( model, response ) { + self.views.set( '#edit-group-member-' + model.get( 'id' ), new bp.Views.GroupMemberErrorInfo( { value: response.responseJSON.message } ).render() ); + + // Make sure to reset request options. + model.resetRequestOptions(); + model.set( 'removing', false ); + } + } ); + } + }, + + editMemberRole: function( event ) { + var newRole = $( event.target ).val(), roleObject = this.getRoleObject( newRole ), + currentRole = this.model.get( 'role').id, roleAction = 'promote', self = this; + + if ( newRole === this.model.get( 'role' ).id || ! roleObject ) { + return this.resetRow(); + } + + this.views.set( '#edit-group-member-' + this.model.get( 'id' ), new bp.Views.GroupMemberUpdatingInfo().render() ); + + if ( 'admin' === currentRole || ( 'mod' === currentRole && 'member' === newRole ) ) { + roleAction = 'demote'; + } + + // Update Group member's role + this.model.save( roleObject, { + wait: true, + data: { + action: roleAction, + role: newRole + }, + success: function( model ) { + if ( self.model.collection.currentRole && newRole !== self.model.collection.currentRole ) { + self.model.collection.remove( model ); + return self.clearRow(); + } else { + return self.resetRow(); + } + }, + error: function( model, response ) { + self.views.set( '#edit-group-member-' + model.get( 'id' ), new bp.Views.GroupMemberErrorInfo( { value: response.responseJSON.message } ).render() ); + + // Make sure to reset request options. + model.resetRequestOptions(); + model.set( 'editing', false ); + } + } ); + } + } ); + + bp.Views.GroupMembersListHeader = bp.View.extend( { + tagName: 'thead', + template : bp.template( 'bp-manage-members-header' ) + } ); + + bp.Views.GroupMembersListTable = bp.View.extend( { + tagName: 'tbody', + + initialize: function() { + var preloaded = bpGroupManageMembersSettings.preloaded || {}, + models = []; + + this.collection.on( 'reset', this.addListTableRows, this ); + + if ( preloaded.body && preloaded.body.length > 0 ) { + _.each( preloaded.body, function( member ) { + models.push( new bp.Models.groupMember( member ) ); + } ); + + this.collection.currentPage = 1; + if ( preloaded.headers && preloaded.headers[ 'X-WP-TotalPages' ] ) { + this.collection.totalPages = parseInt( preloaded.headers[ 'X-WP-TotalPages' ], 10 ); + } + + if ( preloaded.headers && preloaded.headers[ 'X-WP-Total' ] ) { + this.collection.totalGroupMembers = parseInt( preloaded.headers[ 'X-WP-Total' ], 10 ); + } + + this.collection.reset( models ); + } else { + this.collection.fetch( { + data: { 'exclude_admins': false }, + reset: true + } ); + } + }, + + addListTableRows: function( collection ) { + if ( this.views._views ) { + var noMembersRow = _.findWhere( this.views._views[''] , { id: 'bp-no-group-members' } ); + + if ( noMembersRow ) { + noMembersRow.remove(); + } + } + + if ( ! collection.length ) { + this.views.add( new bp.Views.GroupMembersNoMatches( { id: 'bp-no-group-members' } ) ); + } else { + _.each( collection.models, function( member ) { + this.views.add( new bp.Views.GroupMembersListRow( { model: member } ) ); + }, this ); + } + } + } ); + + bp.Views.GroupMembersUI = bp.View.extend( { + className: 'group-members', + + initialize: function() { + var groupMembers = new bp.Collections.groupMembers(); + + // Set filters. + this.views.set( '#group-roles-filter', [ + new bp.Views.GroupsMembersLabel( { attributes: { 'for': 'group-members-role-filter' } } ), + new bp.Views.GroupRolesDropDown( { id: 'group-members-role-filter', collection: groupMembers } ) + ] ); + + // Set the search form. + this.views.set( '#group-members-search-form', new bp.Views.GroupMembersSearch( { id: 'group-members-search', collection: groupMembers } ) ); + + // Set Paginate links. + this.views.set( '#group-members-pagination', new bp.Views.GroupsMembersPagination( { collection: groupMembers } ) ); + + // Set Group members list header and body. + this.views.set( '#group-members-list-table', [ + new bp.Views.GroupMembersListHeader(), + new bp.Views.GroupMembersListTable( { collection: groupMembers } ) + ] ); + } + } ); + + // Inject the UI to manage Group Members into the DOM. + bp.manageGroupMembersUI = new bp.Views.GroupMembersUI( { el:'#group-manage-members-ui' } ).render(); + +} )( window.wp || {}, window.bp || {}, jQuery ); Index: /trunk/src/bp-templates/bp-nouveau/buddypress/common/js-templates/group-members/index.php =================================================================== --- /trunk/src/bp-templates/bp-nouveau/buddypress/common/js-templates/group-members/index.php (revision 12405) +++ /trunk/src/bp-templates/bp-nouveau/buddypress/common/js-templates/group-members/index.php (revision 12405) @@ -0,0 +1,147 @@ + + + +