diff --git a/modules/grequest/config/install/field.storage.group_content.grequest_status.yml b/modules/grequest/config/install/field.storage.group_content.grequest_status.yml
new file mode 100644
index 0000000..496fd0d
--- /dev/null
+++ b/modules/grequest/config/install/field.storage.group_content.grequest_status.yml
@@ -0,0 +1,21 @@
+langcode: en
+status: true
+dependencies:
+ enforced:
+ module:
+ - group
+ - grequest
+id: group_content.grequest_status
+field_name: grequest_status
+entity_type: group_content
+type: integer
+settings:
+ unsigned: false
+ size: normal
+module: core
+locked: true
+cardinality: 1
+translatable: false
+indexes: { }
+persist_with_no_fields: true
+custom_storage: false
diff --git a/modules/grequest/config/install/field.storage.group_content.grequest_updated_by.yml b/modules/grequest/config/install/field.storage.group_content.grequest_updated_by.yml
new file mode 100644
index 0000000..23222eb
--- /dev/null
+++ b/modules/grequest/config/install/field.storage.group_content.grequest_updated_by.yml
@@ -0,0 +1,20 @@
+langcode: en
+status: TRUE
+dependencies:
+ enforced:
+ module:
+ - group
+ - grequest
+id: group_content.grequest_updated_by
+field_name: grequest_updated_by
+entity_type: group_content
+type: entity_reference
+settings:
+ target_type: user
+module: core
+locked: true
+cardinality: 1
+translatable: false
+indexes: { }
+persist_with_no_fields: true
+custom_storage: false
diff --git a/modules/grequest/config/optional/views.view.group_pending_members.yml b/modules/grequest/config/optional/views.view.group_pending_members.yml
new file mode 100644
index 0000000..f0d615a
--- /dev/null
+++ b/modules/grequest/config/optional/views.view.group_pending_members.yml
@@ -0,0 +1,660 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - user
+ enforced:
+ module:
+ - group
+ - grequest
+id: group_pending_members
+label: 'Group pending members'
+module: views
+description: ''
+tag: ''
+base_table: group_content_field_data
+base_field: id
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: 0
+ display_options:
+ access:
+ type: group_permission
+ options:
+ group_permission: 'administer members'
+ cache:
+ type: tag
+ options: { }
+ query:
+ type: views_query
+ options:
+ disable_sql_rewrite: false
+ distinct: false
+ replica: false
+ query_comment: ''
+ query_tags: { }
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Apply
+ reset_button: false
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: true
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: mini
+ options:
+ items_per_page: 10
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 25, 50'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ tags:
+ previous: ‹‹
+ next: ››
+ style:
+ type: table
+ options:
+ grouping: { }
+ row_class: ''
+ default_row_class: true
+ override: true
+ sticky: false
+ caption: ''
+ summary: ''
+ description: ''
+ columns:
+ label: label
+ info:
+ label:
+ sortable: false
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ default: '-1'
+ empty_table: false
+ row:
+ type: fields
+ fields:
+ name:
+ id: name
+ table: users_field_data
+ field: name
+ relationship: gc__user
+ group_type: group
+ admin_label: ''
+ label: Name
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: user_name
+ settings:
+ link_to_entity: true
+ group_column: value
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ entity_type: user
+ entity_field: name
+ plugin_id: field
+ created:
+ id: created
+ table: group_content_field_data
+ field: created
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Requested on'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: timestamp
+ settings:
+ date_format: short
+ custom_date_format: ''
+ timezone: ''
+ group_column: value
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ entity_type: group_content
+ entity_field: created
+ plugin_id: field
+ gid:
+ id: gid
+ table: group_content_field_data
+ field: gid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Parent group'
+ exclude: true
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: target_id
+ type: entity_reference_entity_id
+ settings: { }
+ group_column: target_id
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ entity_type: group_content
+ entity_field: gid
+ plugin_id: field
+ id:
+ id: id
+ table: group_content_field_data
+ field: id
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: ID
+ exclude: true
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: number_integer
+ settings:
+ thousand_separator: ''
+ prefix_suffix: false
+ group_column: value
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ entity_type: group_content
+ entity_field: id
+ plugin_id: field
+ nothing:
+ id: nothing
+ table: views
+ field: nothing
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Approve membership'
+ exclude: true
+ alter:
+ alter_text: true
+ text: 'Approve membership'
+ make_link: false
+ path: '/group/{{ raw_arguments.gid }}/content/{{ id }}/approve-membership'
+ absolute: true
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: 'Approve membership'
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: false
+ plugin_id: custom
+ nothing_1:
+ id: nothing_1
+ table: views
+ field: nothing
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Reject Membership'
+ exclude: true
+ alter:
+ alter_text: true
+ text: 'Reject Membership'
+ make_link: false
+ path: '/group/{{ raw_arguments.gid }}/content/{{ id }}/reject-membership'
+ absolute: true
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: 'Reject Membership'
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: false
+ plugin_id: custom
+ dropbutton:
+ id: dropbutton
+ table: views
+ field: dropbutton
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: Operations
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ fields:
+ nothing: nothing
+ nothing_1: nothing_1
+ name: '0'
+ created: '0'
+ gid: '0'
+ id: '0'
+ destination: true
+ plugin_id: dropbutton
+ filters:
+ grequest_status_value:
+ id: grequest_status_value
+ table: group_content__grequest_status
+ field: grequest_status_value
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: '='
+ value:
+ min: ''
+ max: ''
+ value: '0'
+ group: 1
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ placeholder: ''
+ min_placeholder: ''
+ max_placeholder: ''
+ operator_limit_selection: false
+ operator_list: { }
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ plugin_id: numeric
+ sorts: { }
+ title: 'Group pending members'
+ header: { }
+ footer: { }
+ empty:
+ area_text_custom:
+ id: area_text_custom
+ table: views
+ field: area_text_custom
+ relationship: none
+ group_type: group
+ admin_label: ''
+ empty: true
+ tokenize: false
+ content: 'No membership requests available.'
+ plugin_id: text_custom
+ relationships:
+ gc__user:
+ id: gc__user
+ table: group_content_field_data
+ field: gc__user
+ relationship: none
+ group_type: group
+ admin_label: 'Group content User'
+ required: true
+ group_content_plugins:
+ group_membership_request: group_membership_request
+ group_membership: '0'
+ entity_type: group_content
+ plugin_id: group_content_to_entity
+ arguments:
+ gid:
+ id: gid
+ table: group_content_field_data
+ field: gid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ default_action: 'access denied'
+ exception:
+ value: all
+ title_enable: false
+ title: All
+ title_enable: true
+ title: '{{ arguments.gid|placeholder }} pending members'
+ default_argument_type: fixed
+ default_argument_options:
+ argument: '3'
+ default_argument_skip_url: false
+ summary_options:
+ base_path: ''
+ count: true
+ items_per_page: 25
+ override: false
+ summary:
+ sort_order: asc
+ number_of_records: 0
+ format: default_summary
+ specify_validation: false
+ validate:
+ type: none
+ fail: 'not found'
+ validate_options: { }
+ break_phrase: false
+ not: false
+ entity_type: group_content
+ entity_field: gid
+ plugin_id: group_id
+ display_extenders: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - route.group
+ - url
+ - url.query_args
+ - user.group_permissions
+ tags: { }
+ page_1:
+ display_plugin: page
+ id: page_1
+ display_title: Page
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: group/%group/members-pending
+ menu:
+ type: tab
+ title: 'Membership requests'
+ description: ''
+ expanded: false
+ parent: ''
+ weight: 21
+ context: '0'
+ menu_name: main
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - route.group
+ - url
+ - url.query_args
+ - user.group_permissions
+ tags: { }
diff --git a/modules/grequest/grequest.info.yml b/modules/grequest/grequest.info.yml
new file mode 100644
index 0000000..0c8f7ad
--- /dev/null
+++ b/modules/grequest/grequest.info.yml
@@ -0,0 +1,9 @@
+name: 'Group Membership Request'
+type: module
+description: 'Allows users to request access to Groups'
+package: Group
+core: 8.x
+core_version_requirement: ^8 || ^9
+php: 7.1
+dependencies:
+ - group:group
diff --git a/modules/grequest/grequest.module b/modules/grequest/grequest.module
new file mode 100644
index 0000000..f3c544b
--- /dev/null
+++ b/modules/grequest/grequest.module
@@ -0,0 +1,34 @@
+setFormClass('group-approve-membership', 'Drupal\grequest\Entity\Form\GroupMembershipApproveForm')
+ ->setLinkTemplate('group-approve-membership', '/group/{group}/content/{group_content}/approve-membership')
+ ->setFormClass('group-reject-membership', 'Drupal\grequest\Entity\Form\GroupMembershipRejectForm')
+ ->setLinkTemplate('group-reject-membership', '/group/{group}/content/{group_content}/reject-membership')
+ ->setFormClass('group-request-membership', 'Drupal\grequest\Entity\Form\GroupMembershipRequestForm');
+
+ $entity_types['group']->setLinkTemplate('group-request-membership', '/group/{group}/request-membership');
+}
+
+/**
+ * Implements hook_menu_local_actions_alter().
+ */
+function grequest_menu_local_tasks_alter(&$data, $route_name) {
+ $route_matcher = \Drupal::service('current_route_match');
+ $group = $route_matcher->getParameter('group');
+
+ if ($group instanceof GroupInterface && !$group->getGroupType()->hasContentPlugin('group_membership_request')) {
+ unset($data['tabs'][0]['views_view:view.group_pending_members.page_1']);
+ }
+}
diff --git a/modules/grequest/grequest.routing.yml b/modules/grequest/grequest.routing.yml
new file mode 100644
index 0000000..1ed0662
--- /dev/null
+++ b/modules/grequest/grequest.routing.yml
@@ -0,0 +1,40 @@
+entity.group.group_request_membership:
+ path: '/group/{group}/request-membership'
+ defaults:
+ _controller: '\Drupal\grequest\Controller\GroupMembershipRequestController::requestMembership'
+ _title_callback: '\Drupal\grequest\Controller\GroupMembershipRequestController::requestMembershipTitle'
+ requirements:
+ _group_permission: 'request group membership'
+ _group_member: 'FALSE'
+ options:
+ parameters:
+ group:
+ type: 'entity:group'
+
+entity.group_content.group_approve_membership:
+ path: '/group/{group}/content/{group_content}/approve-membership'
+ defaults:
+ _controller: '\Drupal\grequest\Controller\GroupMembershipRequestController::approveMembership'
+ _title_callback: '\Drupal\grequest\Controller\GroupMembershipRequestController::approveMembershipTitle'
+ requirements:
+ _group_permission: 'administer members'
+ options:
+ parameters:
+ group:
+ type: 'entity:group'
+ group_content:
+ type: 'entity:group_content'
+
+entity.group_content.group_reject_membership:
+ path: '/group/{group}/content/{group_content}/reject-membership'
+ defaults:
+ _controller: '\Drupal\grequest\Controller\GroupMembershipRequestController::rejectMembership'
+ _title_callback: '\Drupal\grequest\Controller\GroupMembershipRequestController::rejectMembershipTitle'
+ requirements:
+ _group_permission: 'administer members'
+ options:
+ parameters:
+ group:
+ type: 'entity:group'
+ group_content:
+ type: 'entity:group_content'
diff --git a/modules/grequest/grequest.views.inc b/modules/grequest/grequest.views.inc
new file mode 100644
index 0000000..d0c0a09
--- /dev/null
+++ b/modules/grequest/grequest.views.inc
@@ -0,0 +1,19 @@
+ t('Request Membership'),
+ 'help' => t('Provides an Request Membership link to the Group.'),
+ 'field' => [
+ 'id' => 'group_request_membership',
+ ],
+ ];
+}
diff --git a/modules/grequest/src/Controller/GroupMembershipRequestController.php b/modules/grequest/src/Controller/GroupMembershipRequestController.php
new file mode 100644
index 0000000..dece110
--- /dev/null
+++ b/modules/grequest/src/Controller/GroupMembershipRequestController.php
@@ -0,0 +1,110 @@
+ $group
+ ->getGroupType()
+ ->getContentPlugin('group_membership_request')
+ ->getContentTypeConfigId(),
+ 'gid' => $group->id(),
+ 'entity_id' => $this->currentUser()->id(),
+ 'grequest_status' => GroupMembershipRequest::REQUEST_PENDING,
+ ]);
+
+ return $this->entityFormBuilder()->getForm($group_content, 'group-request-membership');
+ }
+
+ /**
+ * The _title_callback for the request membership form route.
+ *
+ * @param \Drupal\group\Entity\GroupInterface $group
+ * The group to request membership of.
+ *
+ * @return string
+ * The page title.
+ */
+ public function requestMembershipTitle(GroupInterface $group) {
+ return $this->t('Request membership group %label', ['%label' => $group->label()]);
+ }
+
+ /**
+ * Provides the form for approval a group membership.
+ *
+ * @param \Drupal\group\Entity\GroupInterface $group
+ * The group where we approve membership.
+ *
+ * @return array
+ * A group approval membership form.
+ */
+ public function approveMembership(GroupInterface $group, GroupContentInterface $group_content) {
+ return $this->entityFormBuilder()->getForm($group_content, 'group-approve-membership');
+ }
+
+ /**
+ * The _title_callback for the approval requested membership form route.
+ *
+ * @param \Drupal\group\Entity\GroupInterface $group
+ * The group containing the requested membership.
+ *
+ * @return string
+ * The page title.
+ */
+ public function approveMembershipTitle(GroupInterface $group) {
+ return $this->t('Approve membership request for group %label', ['%label' => $group->label()]);
+ }
+
+ /**
+ * Provides the form for rejection a group membership.
+ *
+ * @param \Drupal\group\Entity\GroupInterface $group
+ * The group where we reject membership.
+ *
+ * @return array
+ * A group rejection membership form.
+ */
+ public function rejectMembership(GroupInterface $group, GroupContentInterface $group_content) {
+ return $this->entityFormBuilder()->getForm($group_content, 'group-reject-membership');
+ }
+
+ /**
+ * The _title_callback for the rejection requested membership form route.
+ *
+ * @param \Drupal\group\Entity\GroupInterface $group
+ * The group containing the requested membership.
+ *
+ * @return string
+ * The page title.
+ */
+ public function rejectMembershipTitle(GroupInterface $group) {
+ return $this->t('Reject membership request for group %label', ['%label' => $group->label()]);
+ }
+
+}
diff --git a/modules/grequest/src/Entity/Form/GroupMembershipApproveForm.php b/modules/grequest/src/Entity/Form/GroupMembershipApproveForm.php
new file mode 100644
index 0000000..66161ae
--- /dev/null
+++ b/modules/grequest/src/Entity/Form/GroupMembershipApproveForm.php
@@ -0,0 +1,83 @@
+getEntity()->getContentPlugin();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ return $this->t('Are you sure you want to Approve this request?');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return Url::fromUserInput(\Drupal::destination()->get());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Approve');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $group_content = $this->getEntity();
+
+ $group_content
+ ->set('grequest_status', GroupMembershipRequest::REQUEST_APPROVED)
+ // Who created request will become an 'approver' for Membership request.
+ ->set('grequest_updated_by', $this->currentUser()->id());
+ $result = $group_content->save();
+
+ if ($result) {
+ $this->messenger()->addStatus($this->t('Membership Request approved'));
+
+ // Adding user to a group.
+ $group_content->getGroup()->addMember($group_content->getEntity());
+ }
+ else {
+ $this->messenger()->addError($this->t('Error updating Request'));
+ }
+
+ \Drupal::logger('group_content')->notice('@type: approved %title.', [
+ '@type' => $group_content->bundle(),
+ '%title' => $group_content->label(),
+ ]);
+
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ }
+
+}
diff --git a/modules/grequest/src/Entity/Form/GroupMembershipRejectForm.php b/modules/grequest/src/Entity/Form/GroupMembershipRejectForm.php
new file mode 100644
index 0000000..60eb1b1
--- /dev/null
+++ b/modules/grequest/src/Entity/Form/GroupMembershipRejectForm.php
@@ -0,0 +1,82 @@
+getEntity()->getContentPlugin();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ return $this->t('Are you sure you want to Reject this request?');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return Url::fromUserInput(\Drupal::destination()->get());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Reject');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $group_content = $this->getEntity();
+
+ $group_content
+ ->set('grequest_status', GroupMembershipRequest::REQUEST_REJECTED)
+ ->set('grequest_updated_by', $this->currentUser()->id());
+ $result = $group_content->save();
+
+ if ($result) {
+ $this->messenger()->addStatus($this->t('Membership Request rejected'));
+
+ // Adding user to a group.
+ $group_content->getGroup()->addMember($group_content->getEntity());
+ }
+ else {
+ $this->messenger()->addError($this->t('Error updating Request'));
+ }
+
+ \Drupal::logger('group_content')->notice('@type: rejected %title.', [
+ '@type' => $group_content->bundle(),
+ '%title' => $group_content->label(),
+ ]);
+
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ }
+
+}
diff --git a/modules/grequest/src/Entity/Form/GroupMembershipRequestForm.php b/modules/grequest/src/Entity/Form/GroupMembershipRequestForm.php
new file mode 100644
index 0000000..66193dd
--- /dev/null
+++ b/modules/grequest/src/Entity/Form/GroupMembershipRequestForm.php
@@ -0,0 +1,107 @@
+get());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions = parent::actions($form, $form_state);
+ $actions['submit']['#value'] = $this->t('Request group membership');
+ $actions['cancel'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Cancel'),
+ '#submit' => ['::cancelSubmit'],
+ ];
+
+ return $actions;
+ }
+
+ /**
+ * Form cancel handler.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function cancelSubmit(array &$form, FormStateInterface $form_state) {
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $form = parent::buildForm($form, $form_state);
+ // Hide entity_id field, it will be prefilled.
+ $form['entity_id']['#access'] = FALSE;
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\group\Entity\GroupContentInterface $group_content */
+ $group_content = parent::validateForm($form, $form_state);
+ $group = $group_content->getGroup();
+ $plugin = $group_content->getContentPlugin();
+ $entity_cardinality = $plugin->getEntityCardinality();
+
+ // Get the current instances of this content entity in the group.
+ $entity_instances = $group->getContentByEntityId($plugin->getPluginId(), $group_content->getEntity()->id());
+ $entity_count = count($entity_instances);
+
+ // If the current group content entity has an ID, exclude that one.
+ if ($group_content_id = $group_content->id()) {
+ foreach ($entity_instances as $instance) {
+ if ($instance->id() == $group_content_id) {
+ $entity_count--;
+ break;
+ }
+ }
+ }
+
+ // Raise a violation if the content has reached the cardinality limit.
+ if ($entity_count >= $entity_cardinality) {
+ $form_state->setErrorByName('', $this->t("You have already submitted a request. It is waiting for Group Administrator's approval"));
+ }
+
+ return $group_content;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ $return = parent::save($form, $form_state);
+
+ $this->messenger()->addMessage($this->t("Your request is waiting for Group Administrator's approval"));
+
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ return $return;
+ }
+
+}
diff --git a/modules/grequest/src/Plugin/GroupContentEnabler/GroupMembershipRequest.php b/modules/grequest/src/Plugin/GroupContentEnabler/GroupMembershipRequest.php
new file mode 100644
index 0000000..0cf2421
--- /dev/null
+++ b/modules/grequest/src/Plugin/GroupContentEnabler/GroupMembershipRequest.php
@@ -0,0 +1,188 @@
+getMember($account) && $group->hasPermission('request group membership', $account)) {
+ $operations['group-request-membership'] = [
+ 'title' => $this->t('Request group membership'),
+ 'url' => $group->toUrl('group-request-membership'),
+ 'weight' => 99,
+ ];
+ }
+
+ return $operations;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createAccess(GroupInterface $group, AccountInterface $account) {
+ return GroupAccessResult::allowedIfHasGroupPermission($group, $account, 'administer members');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function viewAccess(GroupContentInterface $group_content, AccountInterface $account) {
+ return GroupAccessResult::allowedIfHasGroupPermission($group_content->getGroup(), $account, 'administer members');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function updateAccess(GroupContentInterface $group_content, AccountInterface $account) {
+ return GroupAccessResult::allowedIfHasGroupPermission($group_content->getGroup(), $account, 'administer members');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function deleteAccess(GroupContentInterface $group_content, AccountInterface $account) {
+ return GroupAccessResult::allowedIfHasGroupPermission($group_content->getGroup(), $account, 'administer members');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntityReferenceSettings() {
+ $settings = parent::getEntityReferenceSettings();
+ $settings['handler_settings']['include_anonymous'] = FALSE;
+ return $settings;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postInstall() {
+ if (!\Drupal::isConfigSyncing()) {
+ $group_content_type_id = $this->getContentTypeConfigId();
+
+ // Add Status field.
+ FieldConfig::create([
+ 'field_storage' => FieldStorageConfig::loadByName('group_content', 'grequest_status'),
+ 'bundle' => $group_content_type_id,
+ 'label' => $this->t('Request status'),
+ 'required' => TRUE,
+ 'default_value' => self::REQUEST_PENDING,
+ ])->save();
+
+ // Add "Updated by" field, to save reference to
+ // user who approved/denied request.
+ FieldConfig::create([
+ 'field_storage' => FieldStorageConfig::loadByName('group_content', 'grequest_updated_by'),
+ 'bundle' => $group_content_type_id,
+ 'label' => $this->t('Approved/Rejected by'),
+ 'settings' => [
+ 'handler' => 'default',
+ 'target_bundles' => NULL,
+ ],
+ ])->save();
+
+ // Build the 'default' display ID for both the entity form and view mode.
+ $default_display_id = "group_content.$group_content_type_id.default";
+ // Build or retrieve the 'default' view mode.
+ if (!$view_display = EntityViewDisplay::load($default_display_id)) {
+ $view_display = EntityViewDisplay::create([
+ 'targetEntityType' => 'group_content',
+ 'bundle' => $group_content_type_id,
+ 'mode' => 'default',
+ 'status' => TRUE,
+ ]);
+ }
+
+ // Assign display settings for the 'default' view mode.
+ $view_display
+ ->setComponent('grequest_status', [
+ 'type' => 'number_integer',
+ ])
+ ->setComponent('grequest_updated_by', [
+ 'label' => 'above',
+ 'type' => 'entity_reference_label',
+ 'settings' => [
+ 'link' => 1,
+ ],
+ ])
+ ->save();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ $config = parent::defaultConfiguration();
+ $config['entity_cardinality'] = 1;
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+
+ // Disable the entity cardinality field as the functionality of this module
+ // relies on a cardinality of 1. We don't just hide it, though, to keep a UI
+ // that's consistent with other content enabler plugins.
+ $info = $this->t("This field has been disabled by the plugin to guarantee the functionality that's expected of it.");
+ $form['entity_cardinality']['#disabled'] = TRUE;
+ $form['entity_cardinality']['#description'] .= '
' . $info . '';
+
+ return $form;
+ }
+
+}
diff --git a/modules/grequest/src/Plugin/GroupMembershipRequestPermissionProvider.php b/modules/grequest/src/Plugin/GroupMembershipRequestPermissionProvider.php
new file mode 100644
index 0000000..ff10194
--- /dev/null
+++ b/modules/grequest/src/Plugin/GroupMembershipRequestPermissionProvider.php
@@ -0,0 +1,56 @@
+ 'Request group membership',
+ 'allowed for' => ['outsider'],
+ ];
+
+ return $permissions;
+ }
+
+}
diff --git a/modules/grequest/src/Plugin/views/field/RequestMembership.php b/modules/grequest/src/Plugin/views/field/RequestMembership.php
new file mode 100644
index 0000000..c259831
--- /dev/null
+++ b/modules/grequest/src/Plugin/views/field/RequestMembership.php
@@ -0,0 +1,70 @@
+currentDisplay = $view->current_display;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function usesGroupBy() {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query() {
+ // Do nothing -- to override the parent query.
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return \Drupal\Component\Render\MarkupInterface|\Drupal\Core\GeneratedUrl|\Drupal\views\Render\ViewsRenderPipelineMarkup|string
+ * @throws \Drupal\Core\Entity\EntityMalformedException
+ */
+ public function render(ResultRow $values) {
+ /** @var \Drupal\Core\Entity\EntityInterface $entity */
+ $group = $values->_entity;
+ $build = NULL;
+ if ($group->getGroupType()->hasContentPlugin('group_membership_request') && empty($group->getMember(\Drupal::currentUser()))) {
+ $build = $group->toLink($this->t('Request Membership'), 'group-request-membership')->toString();
+ }
+
+ return $build;
+ }
+
+}