FEATURE: Add page for all group membership requests. (#6909)

FEATURE: Add page for all group membership requests. (#6909)

diff --git a/app/assets/javascripts/discourse/controllers/group-requests.js.es6 b/app/assets/javascripts/discourse/controllers/group-requests.js.es6
new file mode 100644
index 0000000..ed07261
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/group-requests.js.es6
@@ -0,0 +1,122 @@
+import { ajax } from "discourse/lib/ajax";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import Group from "discourse/models/group";
+import {
+  default as computed,
+  observes
+} from "ember-addons/ember-computed-decorators";
+import debounce from "discourse/lib/debounce";
+
+export default Ember.Controller.extend({
+  queryParams: ["order", "desc", "filter"],
+  order: "",
+  desc: null,
+  loading: false,
+  limit: null,
+  offset: null,
+  filter: null,
+  filterInput: null,
+  application: Ember.inject.controller(),
+
+  @observes("filterInput")
+  _setFilter: debounce(function() {
+    this.set("filter", this.get("filterInput"));
+  }, 500),
+
+  @observes("order", "desc", "filter")
+  refreshRequesters(force) {
+    if (this.get("loading") || !this.get("model")) {
+      return;
+    }
+
+    if (
+      !force &&
+      this.get("count") &&
+      this.get("model.requesters.length") >= this.get("count")
+    ) {
+      this.set("application.showFooter", true);
+      return;
+    }
+
+    this.set("loading", true);
+    this.set("application.showFooter", false);
+
+    Group.loadMembers(
+      this.get("model.name"),
+      force ? 0 : this.get("model.requesters.length"),
+      this.get("limit"),
+      {
+        order: this.get("order"),
+        desc: this.get("desc"),
+        filter: this.get("filter"),
+        requesters: true
+      }
+    ).then(result => {
+      const requesters = (!force && this.get("model.requesters")) || [];
+      requesters.addObjects(result.members.map(m => Discourse.User.create(m)));
+      this.set("model.requesters", requesters);
+
+      this.setProperties({
+        loading: false,
+        count: result.meta.total,
+        limit: result.meta.limit,
+        offset: Math.min(
+          result.meta.offset + result.meta.limit,
+          result.meta.total
+        )
+      });
+    });
+  },
+
+  @computed("model.requesters")
+  hasRequesters(requesters) {
+    return requesters && requesters.length > 0;
+  },
+
+  @computed
+  filterPlaceholder() {
+    if (this.currentUser && this.currentUser.admin) {
+      return "groups.members.filter_placeholder_admin";
+    } else {
+      return "groups.members.filter_placeholder";
+    }
+  },
+
+  handleRequest(data) {
+    ajax(`/groups/${this.get("model.id")}/handle_membership_request.json`, {
+      data,
+      type: "PUT"
+    }).catch(popupAjaxError);
+  },
+
+  actions: {
+    loadMore() {
+      this.refreshRequesters();
+    },
+
+    acceptRequest(user) {
+      this.handleRequest({ user_id: user.get("id"), accept: true });
+      user.setProperties({
+        request_accepted: true,
+        request_denied: false
+      });
+    },
+
+    undoAcceptRequest(user) {
+      ajax("/groups/" + this.get("model.id") + "/members.json", {
+        type: "DELETE",
+        data: { user_id: user.get("id") }
+      }).then(() => {
+        user.set("request_undone", true);
+      });
+    },
+
+    denyRequest(user) {
+      this.handleRequest({ user_id: user.get("id") });
+      user.setProperties({
+        request_accepted: false,
+        request_denied: true
+      });
+    }
+  }
+});
diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6
index dcb0952..81302d7 100644
--- a/app/assets/javascripts/discourse/controllers/group.js.es6
+++ b/app/assets/javascripts/discourse/controllers/group.js.es6
@@ -15,8 +15,13 @@ export default Ember.Controller.extend({
   showing: "members",
   destroying: null,
 
-  @computed("showMessages", "model.user_count", "canManageGroup")
-  tabs(showMessages, userCount, canManageGroup) {
+  @computed(
+    "showMessages",
+    "model.user_count",
+    "canManageGroup",
+    "model.allow_membership_requests"
+  )
+  tabs(showMessages, userCount, canManageGroup, allowMembershipRequests) {
     const membersTab = Tab.create({
       name: "members",
       route: "group.index",
@@ -28,6 +33,16 @@ export default Ember.Controller.extend({
 
     const defaultTabs = [membersTab, Tab.create({ name: "activity" })];
 
+    if (canManageGroup && allowMembershipRequests) {
+      defaultTabs.push(
+        Tab.create({
+          name: "requests",
+          i18nKey: "requests.title",
+          icon: "user-plus"
+        })
+      );
+    }
+
     if (showMessages) {
       defaultTabs.push(
         Tab.create({
diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
index b9fc992..edf1ac6 100644
--- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6
+++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
@@ -66,6 +66,7 @@ export default function() {
 
   this.route("group", { path: "/g/:name", resetNamespace: true }, function() {
     this.route("members");
+    this.route("requests");
 
     this.route("activity", function() {
       this.route("posts");
diff --git a/app/assets/javascripts/discourse/routes/group-requests.js.es6 b/app/assets/javascripts/discourse/routes/group-requests.js.es6
new file mode 100644
index 0000000..c469c98
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/group-requests.js.es6
@@ -0,0 +1,21 @@
+export default Discourse.Route.extend({
+  titleToken() {
+    return I18n.t("groups.requests.title");
+  },
+
+  model(params) {
+    this._params = params;
+    return this.modelFor("group");
+  },
+
+  setupController(controller, model) {
+    this.controllerFor("group").set("showing", "requests");
+
+    controller.setProperties({
+      model,
+      filterInput: this._params.filter
+    });
+
+    controller.refreshRequesters(true);
+  }
+});
diff --git a/app/assets/javascripts/discourse/templates/group-requests.hbs b/app/assets/javascripts/discourse/templates/group-requests.hbs
new file mode 100644
index 0000000..d04bd11
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/group-requests.hbs
@@ -0,0 +1,51 @@
+<div class="group-members-actions">
+    {{text-field value=filterInput
+        placeholderKey=filterPlaceholder
+        class="group-username-filter no-blur"}}
+</div>
+
+{{#if hasRequesters}}
+  {{#load-more selector=".group-members tr" action=(action "loadMore")}}
+    <table class='group-members'>
+      <thead>
+        {{group-index-toggle order=order desc=desc field='username_lower' i18nKey='username'}}
+        {{group-index-toggle order=order desc=desc field='requested_at' i18nKey='groups.member_requested'}}
+        <th>{{i18n "groups.requests.reason"}}</th>
+        <th></th>
+        <th></th>
+      </thead>
+
+      <tbody>
+        {{#each model.requesters as |m|}}
+          <tr>
+            <td class='avatar'>
+              {{user-info user=m skipName=skipName}}
+            </td>
+            <td>
+              <span class="text">{{bound-date m.requested_at}}</span>
+            </td>
+            <td>{{m.reason}}</td>
+            <td>
+              {{#if m.request_undone}}
+                {{i18n "groups.requests.undone"}}
+              {{else if m.request_accepted}}
+                {{i18n "groups.requests.accepted"}}
+                {{d-button action=(action "undoAcceptRequest") actionParam=m label="undo"}}
+              {{else if m.request_denied}}
+                {{i18n "groups.requests.denied"}}
+              {{else}}
+                {{d-button action=(action "acceptRequest") actionParam=m label="groups.requests.accept" class="btn-primary"}}
+                {{d-button action=(action "denyRequest") actionParam=m label="groups.requests.deny" class="btn-danger"}}
+              {{/if}}
+            </td>
+            <td></td>
+          </tr>
+        {{/each}}
+      </tbody>
+    </table>
+  {{/load-more}}
+

[... diff too long, it was truncated ...]

GitHub sha: a9798f0c