diff --git a/Config/Schema/schema.xml b/Config/Schema/schema.xml index 506f630..9b65be3 100644 --- a/Config/Schema/schema.xml +++ b/Config/Schema/schema.xml @@ -51,4 +51,29 @@ + + + + + + + + REFERENCES cm_co_grouper_lite_widgets(id) + + + REFERENCES cm_co_people(id) + + + REFERENCES cm_co_people(id) + + + + + + co_grouper_lite_widget_id + + + co_person_id + +
\ No newline at end of file diff --git a/Controller/ActAsPeopleController.php b/Controller/ActAsPeopleController.php new file mode 100644 index 0000000..b4ddd18 --- /dev/null +++ b/Controller/ActAsPeopleController.php @@ -0,0 +1,292 @@ + [ + 'validatePost' => false, + 'csrfUseOnce' => false + ] + ]; + + + public $name = 'ActAsPeople'; + + /** + * Overrides parent beforeFilter to verify that Session contains the correct API settings. + * + * @return void + */ + public function beforeFilter() + { + parent::beforeFilter(); + + if(empty($this->request->params['named']['glid'])) { + throw new InvalidArgumentException(_txt('er.grouperlite.glid'), + HttpStatusCodesEnum::HTTP_BAD_REQUEST); + } + $this->response->disableCache(); + $this->RequestHandler->addInputType('json', ['json_decode', true]); + + $this->Security->unlockedActions = [ + 'delete', + 'upsert', + ]; + + // Get the config + $args = array(); + $args['conditions']['CoGrouperLiteWidget.id'] = $this->request->params['named']['glid']; + $args['contain'] = false; + $cfg = $this->CoGrouperLiteWidget->find('first', $args); + // Set the config so that everybody can access it + $this->CoGrouperLiteWidget->setConfig($cfg); + } + + /** + * Update or Insert an ActAs user + * + * @since COmanage Registry v4.4.0 + */ + + public function upsert(): void + { + $this->request->allowMethod('ajax'); + $this->layout = 'ajax'; + + if(!$this->request->is('restful') + && !$this->request->is('ajax')) { + throw new RuntimeException('HTTP Method Not Allowed', HttpStatusCodesEnum::HTTP_METHOD_NOT_ALLOWED); + } + + $data = $this->request->data; + + $fullName = $data['fullname']; + unset($data['fullname']); + + $args = []; + $args['conditions']['ActAsPerson.co_person_id'] = $data['co_person_id']; + if(!empty($this->request->named['actrecordId'])) { + $args['conditions']['ActAsPerson.id'] = $this->request->named['actrecordId']; + } + $args['conditions']['ActAsPerson.act_as_co_person_id'] = $data['act_as_co_person_id']; + $args['contain'] = false; + if(!empty($this->ActAsPerson->find('first', $args))) { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_CONFLICT, 'Already Exists'); + $error = ErrorsEnum::Conflict; + $this->set(compact('error')); + $this->set('_serialize', 'error'); + return; + } + + // We will update + if(!empty($this->request->named['actrecordId'])) { + $this->ActAsPerson->id = $this->request->named['actrecordId']; + } + + try { + $ret = $this->ActAsPerson->save($data); + } + catch(Exception $e) { + $err = filter_var($e->getMessage(),FILTER_SANITIZE_SPECIAL_CHARS); + $this->Flash->set($err, ['key' => 'error']); + } + + if($ret) { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_CREATED, 'Added'); + // I am registering a flash message. It will render after the frontend triggers a reload + $this->Flash->set("Act As {$fullName} Enabled" , ['key' => 'success']); + + $args = []; + $args['conditions']['ActAsPerson.id'] = $this->ActAsPerson->id; + $args['contain'] = false; + $data = $this->ActAsPerson->find('first', $args); + + $resp = [ + 'ActAsPerson' => $data['ActAsPerson'] + ]; + $this->set(compact('resp')); + $this->set('_serialize', 'resp'); + } else { + $fs = $this->ActAsPerson->invalidFields(); + + if(!empty($fs)) { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_BAD_REQUEST, 'Invalid Fields'); + $this->Flash->set('Invalid Fields', ['key' => 'error']); + $this->set(compact('fs')); + $this->set('_serialize', 'fs'); + } elseif ($e + && isset(_txt('en.http.status.codes')[$e->getCode()]) ) { + $this->Api->restResultHeader($e->getCode(), _txt('en.http.status.codes')[$e->getCode()]); + if(!empty($e->getMessage())) { + $vv_error = $e->getMessage(); + $this->set(compact('vv_error')); + $this->set('_serialize', 'vv_error'); + $this->Flash->set($e->getMessage(), ['key' => 'error']); + } + } else { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, 'Other Error'); + $error = ErrorsEnum::Error; + $this->set(compact('error')); + $this->set('_serialize', 'error'); + } + } + } + + /** + * Handle an Act As Person Delete request. + * + * @since COmanage Registry v4.4.0 + */ + + public function delete():void + { + $this->request->allowMethod('ajax'); + $this->layout = 'ajax'; + + if(!$this->request->is('delete') + && !$this->request->is('ajax') + ) { + throw new RuntimeException('HTTP Method Not Allowed', HttpStatusCodesEnum::HTTP_METHOD_NOT_ALLOWED); + } + + if(empty($this->request->named['copersonid']) + || empty($this->request->named['act_as_copersonid'])) { + $this->log(__METHOD__ . '::message: Named Parameter missing', LOG_ERROR); + throw new BadRequestException('Named Parameter missing'); + } + + try { + $conditions = [ + 'ActAsPerson.co_person_id' => $this->request->named['copersonid'], + 'ActAsPerson.act_as_co_person_id' => $this->request->named['act_as_copersonid'] + ]; + + $ret = $this->ActAsPerson->deleteAll($conditions, true, true); + } catch(Exception $e) { + $error = filter_var($e->getMessage(),FILTER_SANITIZE_SPECIAL_CHARS); + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, 'Other Error'); + $this->set(compact('error')); + $this->set('_serialize', 'error'); + return; + } + + // Set flash message + if(!$ret) { + $error = ErrorsEnum::NotDeleted; + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_BAD_REQUEST, 'Bad Request'); + $this->set(compact('error')); + $this->set('_serialize', 'error'); + return; + } + + // We will register a Flash message here. The frontend will reload and it will be rendered + $this->Flash->set('Act As Disabled', ['key' => 'success']); + + // We do not redirect but we pass data back to the frontend because we + // want to keep the view structure as simple as possible. If we redirect we will + // have to add more JSON view infrastructure. We will let the frontend + // handle the redirect + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_OK, 'Success'); + $success = ['status' => 'Success', 'message' => 'Succeeded']; + $this->set(compact('success')); + $this->set('_serialize', 'success'); + } + + + /** + * NOTE: All permissions will be done on the Grouper side. All Authenticated users will be able to + * use this plugin for self-admin of groups. + * + * Authorization for this Controller, called by Auth component + * - precondition: Session.Auth holds data used for authz decisions + * - postcondition: $permissions set with calculated permissions + * + * @return array|bool Permissions + * @since COmanage Registry v4.4.0 + */ + public function isAuthorized(): array|bool + { + $roles = $this->Role->calculateCMRoles(); + $this->set('roles', $roles); + + $pids = $this->parsePersonID($this->request->data); + + $cfg = $this->CoGrouperLiteWidget->getConfig(); + // Find the identifier + $args = array(); + $args['conditions']['Identifier.type'] = $cfg['CoGrouperLiteWidget']['identifier_type']; + $args['conditions']['Identifier.status'] = SuspendableStatusEnum::Active; + $args['conditions']['Identifier.co_person_id'] = !empty($roles['copersonid']) ? $roles['copersonid'] : $pids['copersonid']; + $args['contain'] = false; + + $identifiers = $this->Identifier->find('first', $args); + if(!empty($identifiers) + && is_array($identifiers) + && isset($identifiers['Identifier']['identifier']) + ) { + $this->setUserId($identifiers['Identifier']['identifier']); + } + + // Find if the user belongs to Group + $eligibleGroup = $cfg['CoGrouperLiteWidget']['act_as_grp_name']; + $isActAsEligibilityGroupmember = false; + + if(!empty($eligibleGroup)) { + $isActAsEligibilityGroupmember = $this->GrouperGroup + ->isGroupMember($this->getUserId(), $eligibleGroup, $cfg); + } + + // Determine what operations this user can perform + // Construct the permission set for this user, which will also be passed to the view. + $p = []; + + $p['delete'] = $isActAsEligibilityGroupmember; + $p['upsert'] = $isActAsEligibilityGroupmember; + + $this->set('permissions', $p); + + return ($p[$this->action]); + } + + /** + * @return null + */ + public function getUserId() + { + return $this->userId; + } + + + /** + * @param null $userId + */ + private function setUserId($userId): void + { + $this->userId = $userId; + } +} \ No newline at end of file diff --git a/Controller/CoGrouperLiteWidgetsController.php b/Controller/CoGrouperLiteWidgetsController.php index 1ea93f0..bd6e92c 100644 --- a/Controller/CoGrouperLiteWidgetsController.php +++ b/Controller/CoGrouperLiteWidgetsController.php @@ -45,7 +45,7 @@ public function display($id) { $cfg = $this->CoGrouperLiteWidget->getConfig(); $this->set('pl_grouperlite_index_url', Router::url([ - 'plugin' => "grouper_lite", + 'plugin' => 'grouper_lite', 'controller' => 'grouper_groups', 'action' => 'index', 'co' => $this->cur_co['Co']['id'], diff --git a/Controller/GrouperGroupsController.php b/Controller/GrouperGroupsController.php index b9749a0..4a7610b 100644 --- a/Controller/GrouperGroupsController.php +++ b/Controller/GrouperGroupsController.php @@ -28,6 +28,7 @@ App::uses('Validator', 'Vendor/cakephp/Validation'); App::uses('CoGrouperLite', 'GrouperLiteWidget.Model/'); App::uses('GrouperGroup', 'GrouperLiteWidget.Model/'); +App::uses('ActAsPerson', 'GrouperLiteWidget.Model/'); App::uses('Identifier', 'Model'); /** @@ -37,21 +38,29 @@ */ class GrouperGroupsController extends GrouperLiteWidgetAppController { - public $helpers = array('Html', 'Form', 'Flash'); + public $helpers = ['Html', 'Form', 'Flash']; // Dynamic properties are deprecated, so we will define the property here private $userId = null; + private $actAsIdentifier = null; - public $uses = array( + public $uses = [ 'GrouperLiteWidget.GrouperGroup', 'GrouperLiteWidget.CoGrouperLiteWidget', + 'GrouperLiteWidget.ActAsPerson', 'Identifier', - 'CoPerson'); - - public $components = array('Flash', 'Paginator', 'RequestHandler', 'Security' => array( - 'validatePost' => false, - 'csrfUseOnce' => false - )); + 'CoPerson' + ]; + + public $components = [ + 'Flash', + 'Paginator', + 'RequestHandler', + 'Security' => [ + 'validatePost' => false, + 'csrfUseOnce' => false + ] + ]; public $name = 'GrouperGroups'; @@ -75,7 +84,7 @@ public function addSubscriber(): void : $groupName; try { - if(!$this->GrouperGroup->addGroupMember($this->userId, + if(!$this->GrouperGroup->addGroupMember($this->getActAsIdentifier(), $groupNameFormatted, $addUserId, $this->CoGrouperLiteWidget->getConfig())) { @@ -94,8 +103,7 @@ public function addSubscriber(): void /** * Overrides parent beforeFilter to verify that Session contains the correct API settings. * - * @return CakeResponse|void|null - * + * @return void */ public function beforeFilter() { @@ -106,7 +114,7 @@ public function beforeFilter() HttpStatusCodesEnum::HTTP_BAD_REQUEST); } $this->response->disableCache(); - $this->RequestHandler->addInputType('json', array('json_decode', true)); + $this->RequestHandler->addInputType('json', ['json_decode', true]); $this->Security->unlockedActions = [ 'removeSubscriber', @@ -120,16 +128,61 @@ public function beforeFilter() // Get the config $args = array(); - $args['conditions']['CoGrouperLiteWidget.id'] = $this->request->params["named"]["glid"]; + $args['conditions']['CoGrouperLiteWidget.id'] = $this->request->params['named']['glid']; $args['contain'] = false; $cfg = $this->CoGrouperLiteWidget->find('first', $args); // Set the config so that everybody can access it $this->CoGrouperLiteWidget->setConfig($cfg); + + // XXX We will make the ActAs calculations here the beforeRender callback does not always fire + // This is happening because we have a mixed schema with API calls as well as MVC structure. + // Get the ActAs User Data + $roles = $this->Role->calculateCMRoles(); + $co_person_id = $roles['copersonid']; + // Get the act as data from the database + if(!empty($co_person_id)) { + $args = []; + $args['conditions']['ActAsPerson.co_person_id'] = $co_person_id; + $args['contain'] = false; + $act_as_record = $this->ActAsPerson->find('first', $args); + } + $this->set('vv_act_as_people', []); + + // Get ActAs configuration + if(!empty($act_as_record)) { + $act_as_person = $this->GrouperGroup->dataConstructForPicker($this->cur_co['Co']['id'], + PeoplePickerModeEnum::All, + [$act_as_record['ActAsPerson']['act_as_co_person_id']]); + $this->set('vv_act_as_people', $act_as_person); + $this->set('vv_act_as_record_id', $act_as_record['ActAsPerson']['id']); + $act_as_identifier = $this->GrouperGroup->getIdentifierFromPersonId($act_as_record['ActAsPerson']['act_as_co_person_id'], + $cfg['CoGrouperLiteWidget']['identifier_type']); + $this->setActAsIdentifier($act_as_identifier); + } + + // XXX Localizations + // Get all the localizations in an array + global $cm_texts; + global $cm_lang; + + $texts = []; + + foreach(array_keys($cm_texts[$cm_lang]) as $k) { + if(!is_array($cm_texts[$cm_lang][$k]) + // Also skip strings that can already be dynamically changed + && !preg_match('/^em\./', $k)) { + $texts[$k] = $cm_texts[$cm_lang][$k]; + } + } + + ksort($texts); + + $this->set('vv_cm_texts', Hash::expand($texts)); } /** * Callback after controller methods are invoked but before views are rendered. - * - precondition: Request Handler component has set $this->request + * - Precondition: A request Handler component has set $this->request * * @since COmanage Registry v4.4.0 */ @@ -139,19 +192,30 @@ public function beforeRender() { $cfg = $this->CoGrouperLiteWidget->getConfig(); $this->set('vv_config', $cfg); - $this->set('title', _txt('pl.grouperlite.title.groupmember')); - $this->set('vv_is_user_owner', $this->GrouperGroup->isUserGroupOwner($this->userId ?? '', $cfg) ); + $this->set('vv_title', _txt('pl.grouperlite.title.dashboard')); + $this->set('vv_coid', $this->cur_co['Co']['id']); + + $roles = $this->Role->calculateCMRoles(); + $this->set('vv_copersonid', $roles['copersonid'] ?? null); + $this->set('vv_picker_mode', PeoplePickerModeEnum::All); + + $this->set('vv_act_as_identifier', $this->getActAsIdentifier()); + $this->set('vv_is_user_owner', + $this->GrouperGroup->isUserGroupOwner($this->getUserId(), + $this->getActAsIdentifier(), + $cfg) + ); // $this->set('vv_is_template_user', $this->GrouperGroup->isTemplateUser($this->userId ?? '', $cfg) ); // $this->set('vv_is_grouper_visible', $this->GrouperGroup->isGrouperVisible($this->userId ?? '', $cfg)); - $this->set('vv_coid', $this->cur_co['Co']['id']); } /** - * @return null + * @return null|string */ - public function getUserId() + public function getActAsIdentifier(): ?string { - return $this->userId; + //If the actor Identifier is not set we will return the current user + return $this->actAsIdentifier ?? $this->getUserId(); } /** @@ -181,6 +245,14 @@ public function findSubscriber(): void $this->set('_serialize', 'matches'); } + /** + * @return null|string + */ + public function getUserId(): null|string + { + return $this->userId; + } + /** * GroupMember vue route for rendering groupmemberapi results * @@ -227,7 +299,7 @@ public function groupSubscribers(): void : $groupName; try { - $subscribers = $this->GrouperGroup->getGroupMembers($this->userId, + $subscribers = $this->GrouperGroup->getGroupMembers($this->getActAsIdentifier(), $groupNameFormatted, $this->CoGrouperLiteWidget->getConfig()); } catch (Exception $e) { @@ -246,6 +318,7 @@ public function groupOwnerApi(): void { //Set initial setting $arguments = [ 'userId' => $this->userId, + 'actorUserId' => $this->getActAsIdentifier(), 'cfg' => $this->CoGrouperLiteWidget->getConfig() ]; @@ -255,7 +328,7 @@ public function groupOwnerApi(): void { //Add settings for search Owned Groups $arguments['searchCriteria'] = $searchCriteria; - $arguments['searchPage'] = 'ownerGroups'; + $arguments['searchPage'] = 'getOwnedGroups'; $func = 'getSearchedGroups'; $errorHint = 'Search'; @@ -287,6 +360,7 @@ public function groupMemberApi(): void { //Set initial setting $arguments = [ 'userId' => $this->userId, + 'actorUserId' => $this->getActAsIdentifier(), 'cfg' => $this->CoGrouperLiteWidget->getConfig() ]; @@ -331,6 +405,7 @@ public function groupOptinApi() { //Set initial setting $arguments = [ 'userId' => $this->userId, + 'actorUserId' => $this->getActAsIdentifier(), 'cfg' => $this->CoGrouperLiteWidget->getConfig() ]; @@ -374,7 +449,7 @@ public function groupCreateTemplate() { if ($this->request->is('post')) { try { - $status = $this->GrouperGroup->createGroupWithTemplate($this->userId, + $status = $this->GrouperGroup->createGroupWithTemplate($this->getActAsIdentifier(), $this->request->data, $this->CoGrouperLiteWidget->getConfig()); @@ -415,20 +490,21 @@ public function index(): void public function isAuthorized(): array|bool { $roles = $this->Role->calculateCMRoles(); + $this->set('roles', $roles); + $pids = $this->parsePersonID($this->request->data); + $cfg = $this->CoGrouperLiteWidget->getConfig(); // Find the identifier - $args = array(); - $args['conditions']['Identifier.type'] = $cfg['CoGrouperLiteWidget']['identifier_type']; - $args['conditions']['Identifier.status'] = SuspendableStatusEnum::Active; - $args['conditions']['Identifier.co_person_id'] = $roles['copersonid']; - $args['contain'] = false; + $copersonid = !empty($roles['copersonid']) ? $roles['copersonid'] : $pids['copersonid']; + $this->setUserId($this->GrouperGroup->getIdentifierFromPersonId($copersonid, + $cfg['CoGrouperLiteWidget']['identifier_type'])); - $identifiers = $this->Identifier->find('first', $args); - if(!empty($identifiers) - && is_array($identifiers) - && isset($identifiers['Identifier']['identifier']) - ) { - $this->setUserId($identifiers['Identifier']['identifier']); + // Find if the user belongs to Group + $eligibleGroup = $cfg['CoGrouperLiteWidget']['act_as_grp_name']; + $isActAsEligibilityGroupmember = false; + + if(!empty($eligibleGroup)) { + $isActAsEligibilityGroupmember = $this->GrouperGroup->isGroupMember($this->getUserId(), $eligibleGroup, $cfg); } // Determine what operations this user can perform @@ -454,6 +530,7 @@ public function isAuthorized(): array|bool $p['joinGroup'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); $p['leaveGroup'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); $p['groupcreatetemplate'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['actAsAction'] = $isActAsEligibilityGroupmember; $this->set('permissions', $p); @@ -476,7 +553,7 @@ public function joinGroup(): void try { // Add myself - if(!$this->GrouperGroup->addGroupMember($this->userId, + if(!$this->GrouperGroup->addGroupMember($this->getActAsIdentifier(), $groupName, $this->userId, $this->CoGrouperLiteWidget->getConfig())) { @@ -503,7 +580,7 @@ public function leaveGroup(): void $groupName = urldecode($this->request->query['GroupName']); try { - if(!$this->GrouperGroup->removeGroupMember($this->userId, + if(!$this->GrouperGroup->removeGroupMember($this->getActAsIdentifier(), $groupName, $this->userId, $this->CoGrouperLiteWidget->getConfig())) { @@ -562,7 +639,7 @@ public function removeSubscriber(): void : $groupName; try { - if(!$this->GrouperGroup->removeGroupMember($this->userId, + if(!$this->GrouperGroup->removeGroupMember($this->getActAsIdentifier(), $groupNameFormatted, $remUserId, $this->CoGrouperLiteWidget->getConfig())) { @@ -606,9 +683,17 @@ public function restResponse(int $status, } /** - * @param null $userId + * @param null|string $actAsIdentifier + */ + public function setActAsIdentifier(?string $actAsIdentifier): void + { + $this->actAsIdentifier = $actAsIdentifier; + } + + /** + * @param null|string $userId */ - private function setUserId($userId): void + private function setUserId(?string $userId): void { $this->userId = $userId; } @@ -638,7 +723,7 @@ public function userManagerApi(): void $memberId = urldecode($this->request->query['memberid']); try { - $groupsimanage = $this->GrouperGroup->getManagedUsers($this->userId, + $groupsimanage = $this->GrouperGroup->getManagedUsers($this->getActAsIdentifier(), $memberId, $cfg); } catch (Exception $e) { @@ -655,10 +740,10 @@ public function userManagerApi(): void } /** - * Override the default sanity check performed in AppController + * Override the default check performed in AppController * * @since COmanage Registry v4.3.0 - * @return Boolean True if sanity check is successful + * @return Boolean True if check is successful */ public function verifyRequestedId(): bool diff --git a/Lib/GrouperApiAccess.php b/Lib/GrouperApiAccess.php index 6f77fa9..5bed1e0 100644 --- a/Lib/GrouperApiAccess.php +++ b/Lib/GrouperApiAccess.php @@ -472,11 +472,12 @@ public function getUserMemberships(string $userId, string $actAsUserId, string $ true); // Build request logic + // XXX This is not working very well? $data = [ 'WsRestGetMembershipsRequest' => [ 'fieldName' => $groupType, 'actAsSubjectLookup' => [ - 'subjectId' => true ? '': $actAsUserId + 'subjectId' => ($userId !== $actAsUserId) ? $actAsUserId : '' ], 'wsSubjectLookups' => [ ['subjectId' => $isOptinsOrOptouts ? GrouperConfigEnums::ALL : $userId] diff --git a/Lib/enum.php b/Lib/enum.php index c496330..76a5413 100644 --- a/Lib/enum.php +++ b/Lib/enum.php @@ -1,11 +1,12 @@ 'ERROR', 'message' => 'ERROR']; - const Exception = ['status' => 'ERROR', 'message' => 'EXCEPTION']; - const NoAccess = ['status' => 'ERROR', 'message' => 'NO ACCESS']; - const NotAdded = ['status' => 'ERROR', 'message' => 'NOT ADDED']; - const NotDeleted = ['status' => 'ERROR', 'message' => 'NOT DELETED']; + const Conflict = ['status' => 'ERROR', 'message' => 'Already Exists']; + const Error = ['status' => 'ERROR', 'message' => 'Error']; + const Exception = ['status' => 'ERROR', 'message' => 'Exception']; + const NoAccess = ['status' => 'ERROR', 'message' => 'No Access']; + const NotAdded = ['status' => 'ERROR', 'message' => 'Not Added']; + const NotDeleted = ['status' => 'ERROR', 'message' => 'Not Deleted']; } class GrouperSpecialGroups { diff --git a/Lib/lang.php b/Lib/lang.php index d5e490b..8152ae0 100644 --- a/Lib/lang.php +++ b/Lib/lang.php @@ -1,6 +1,6 @@ 'Grouper Configuration Settings', 'pl.grouperlite.config.edit.title' => 'Edit Grouper Configuration Settings', 'pl.grouperlite.config.grouper-url' => 'Grouper Site URL', @@ -37,7 +37,7 @@ 'pl.grouperlite.dashboard.heading.groups' => 'Groups', 'pl.grouperlite.dashboard.heading.email-lists' => 'Email lists', - 'pl.grouperlite.title.root' => 'Grouper Collaborations:', + 'pl.grouperlite.title.dashboard' => 'Grouper Lite Dashboard', 'pl.grouperlite.title.groupinfo' => 'Group configuration and attributes', 'pl.grouperlite.title.groupowner' => 'Groups I manage', 'pl.grouperlite.title.groupmember' => 'My Memberships', @@ -48,6 +48,7 @@ 'pl.grouperlite.title.emaillistsinfo' => 'Email list configuration and attributes', 'pl.grouperlite.title.groupcreate' => 'Create a group', 'pl.grouperlite.title.templatecreate' => 'Create a working group', + 'pl.grouperlite.title.root' => 'Grouper Collaborations:', 'pl.grouperlite.message.flash.join-group-success' => 'You have been added to the group:', 'pl.grouperlite.message.flash.join-group-failed' => 'You are unable to join the group:', @@ -125,7 +126,10 @@ 'pl.grouperlite.action.close' => 'Close', 'pl.grouperlite.action.clear' => 'Clear', 'pl.grouperlite.action.add-user' => 'Add', + 'pl.grouperlite.action.act-as' => 'Act As', 'pl.grouperlite.action.remove-user' => 'Remove', + 'pl.grouperlite.action.enable' => 'Enable', + 'pl.grouperlite.action.find' => 'Find', 'pl.grouperlite.message.user-not-found-error' => 'Error: User not found.', 'pl.grouperlite.message.user-not-added-error' => 'Error: Unable to add user.', 'pl.grouperlite.message.user-not-removed-error' => 'Error: Unable to remove user.', @@ -175,4 +179,4 @@ 'pl.grouperlite.members.empty' => 'This group has no member OR you are not authorized to see the members of this group.', 'er.grouperlite.glid' => 'Named parameter glid was not found', -); \ No newline at end of file +]; \ No newline at end of file diff --git a/Model/ActAsPerson.php b/Model/ActAsPerson.php new file mode 100644 index 0000000..15d6299 --- /dev/null +++ b/Model/ActAsPerson.php @@ -0,0 +1,77 @@ + ['ActAsPerson'], + 'CoPerson' => [ + 'ActAsPerson' => [ + 'className' => 'ActAsPerson', + 'foreignKey' => 'co_person_id' + ] + ], + 'CoPerson' => [ + 'ActAsPerson' => [ + 'className' => 'ActAsPerson', + 'foreignKey' => 'act_as_co_person_id' + ] + ] + ]; + + // Association rules from this model to other models + public $belongsTo = [ + 'ActAsCoPerson' => [ + 'className' => 'CoPerson', + 'foreignKey' => 'act_as_co_person_id' + ], + 'CoPerson', + 'CoGrouperLiteWidget' + ]; + + // Validation rules for table elements + public $validate = [ + 'co_grouper_lite_widget_id' => [ + 'rule' => 'numeric', + 'required' => true, + 'allowEmpty' => false + ], + 'act_as_co_person_id' => [ + 'rule' => 'numeric', + 'required' => true, + 'allowEmpty' => false + ], + 'co_person_id' => [ + 'rule' => 'numeric', + 'required' => true, + 'allowEmpty' => false + ], + ]; + +} \ No newline at end of file diff --git a/Model/GrouperGroup.php b/Model/GrouperGroup.php index 1468b07..fd4184e 100644 --- a/Model/GrouperGroup.php +++ b/Model/GrouperGroup.php @@ -61,26 +61,28 @@ class GrouperGroup extends GrouperLiteWidgetAppModel /** - * Verifies if user is an owner/admin of a group. + * Verifies if a user is an owner/admin of a group. * Session variable is reset on Group Creation and Group Deletion * - * @param string $userId Id of User - * @return String T or F - * @throws GrouperLiteWidgetException + * @param string $userId Id of User + * @param string $actorUserId + * @param array $cfg * + * @return bool T or F + * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ - public function isUserGroupOwner(string $userId, array $cfg): bool + public function isUserGroupOwner(string $userId, string $actorUserId, array $cfg): bool { $this->initApi($cfg); - if(empty($userId)) { + if(empty($userId) || empty($actorUserId)) { return false; } try { - $resultsAdmin = $this->grouperAPI->getUserMemberships($userId, $userId, GrouperGroupTypeEnum::ADMIN); - $resultsUpdate = $this->grouperAPI->getUserMemberships($userId, $userId, GrouperGroupTypeEnum::UPDATE); + $resultsAdmin = $this->grouperAPI->getUserMemberships($userId, $actorUserId, GrouperGroupTypeEnum::ADMIN); + $resultsUpdate = $this->grouperAPI->getUserMemberships($userId, $actorUserId, GrouperGroupTypeEnum::UPDATE); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; @@ -127,77 +129,108 @@ private function initApi(array $cfg) { } /** - * Return all Groups that a User belongs to in Grouper. - * Will also add OptOut Groups and flag them as joined so can display Optout option in UI. + * Add a member to a specific Grouper Group * - * @param string $userId + * @param string $actorUserId Id of User + * @param string $groupName + * @param string $addUserId * @param array $cfg * - * @return array Records of Groups from Grouper that the User belongs to - * @throws GrouperLiteWidgetException + * @return string success of Request + * @throws GrouperLiteWidgetException Captured in Controller + * @throws JsonException * @since COmanage Registry v4.4.0 */ - public function filteredMemberOfGroups(string $userId, array $cfg): array + public function addGroupMember(string $actorUserId, string $groupName, string $addUserId, array $cfg) { $this->initApi($cfg); - try { - $memberOfGroups = $this->memberOfGroups($userId, $userId, $cfg); - // Determine which groups can be left by user, if wanted. - $optOutGroups = $this->grouperAPI->getUserMemberships($userId, $userId, GrouperGroupTypeEnum::OPTOUTS); - $optOutGroupsNames = Hash::combine($optOutGroups, '{n}.name', '{n}.displayExtension'); + return $this->grouperAPI->addGroupMember($actorUserId, $groupName, $addUserId); + } - foreach ($memberOfGroups as &$memberOfGroup) { - $memberOfGroup['optOut'] = isset($optOutGroupsNames[$memberOfGroup['name']]); + /** + * Breakout Working Groups from AdHoc Groups. + * + * @param array $recordSet + * @return array[] + * + */ + public function breakoutWrkFromAdHocGroups(array $recordSet): array + { + $wgData = []; + $notWGData = []; + //Parse out the Working Groups from the Ad-hoc groups + foreach ($recordSet as $record) { + if (isset($record['WGName'])) { + $wgData[] = $record; + } else { + $notWGData[] = $record; } + } - return $this->getFriendlyWorkingGroupName($memberOfGroups, 'member'); + return [ + 'adhoc' => $notWGData, + 'working' => $wgData + ]; + } + + /** + * Create a new Grouper Group using the Template methodology in Grouper + * + * @param string $userId Id of User + * @param array $groupData Data needed to create new Grouper Group via Template + * @return array status and error message, if applicable + * @throws GrouperLiteWidgetException + * + * @since COmanage Registry v4.4.0 + */ + public function createGroupWithTemplate(string $userId, array $groupData, array $cfg) + { + $this->initApi($cfg); + //Need to massage incoming data to meet Grouper Template requirements + $fields = array( + 'gsh_input_isSympa', + 'gsh_input_isSympaModerated', + 'gsh_input_isOptin', + 'gsh_input_isConfluence', + 'gsh_input_isJira' + ); + // Template does not except true/false, so convert to string and send that way + foreach ($fields as $field) { + ($groupData[$field] == '0') ? $groupData[$field] = 'false' : $groupData[$field] = 'true'; + } + $args = array(); + $args['userId'] = $userId; + $args['data'] = $groupData; + try { + return $this->grouperAPI->createGroupWithTemplate($args); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } } - /** - * Find People based on mode and term + * Construct picker(like) response data based on mode and term * * @param integer $coId CO ID * @param string $mode Search mode to apply filters for - * @param string|null $term Search block + * @param array $coPersonIds List of PersonIds * * @return array Array of CO Person records * @since COmanage Registry v4.4.0 - * - * XXX Remove this as soon as the change is present in COmanage core - * */ - public function findForPicker(int $coId, string $mode, ?string $term): array + public function dataConstructForPicker(int $coId, string $mode, array $coPersonIds): array { - $coPersonIds = []; - $this->CoPerson = ClassRegistry::init('CoPerson'); - $this->Co = ClassRegistry::init('Co'); - - // jquery Autocomplete sends the search as url?term=foo - if(!empty($term)) { - // Leverage model specific keyword search - - // Note EmailAddress and Identifier don't support substring search - foreach(array('Name', 'EmailAddress', 'Identifier') as $m) { - $hits = $this->CoPerson->$m->search($coId, $term, 25); - - $coPersonIds = array_merge($coPersonIds, Hash::extract($hits, '{n}.CoPerson.id')); - } + if(empty($coPersonIds)) { + return []; } - $coPersonIds = array_unique($coPersonIds); - - // Look up additional information to provide hints as to which person is which. - // We only do this when there are relatively small numbers of results to - // avoid making a bunch of database queries early in the search. + $this->CoPerson = ClassRegistry::init('CoPerson'); + $this->Co = ClassRegistry::init('Co'); - $matches = array(); + $matches = []; if(count($coPersonIds) > 100) { // We don't return large sets to avoid slow performance @@ -275,313 +308,386 @@ public function findForPicker(int $coId, string $mode, ?string $term): array } /** - * Internal process used by other functions to fetch Groups the User is a member of + * Return all Groups that a User belongs to in Grouper. + * Will also add OptOut Groups and flag them as joined so can display an Optout option in the UI. * - * @param string $actorUserId * @param string $userId + * @param string $actorUserId * @param array $cfg * * @return array Records of Groups from Grouper that the User belongs to * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ - private function memberOfGroups(string $actorUserId, string $userId, array $cfg) + public function filteredMemberOfGroups(string $userId, string $actorUserId, array $cfg): array { $this->initApi($cfg); try { - return $this->grouperAPI->getUserGroups($actorUserId, $userId); - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - } + $memberOfGroups = $this->memberOfGroups($actorUserId, $userId, $cfg); + // Determine which groups can be left by user, if wanted. + $optOutGroups = $this->grouperAPI->getUserMemberships($userId, $actorUserId, GrouperGroupTypeEnum::OPTOUTS); + $optOutGroupsNames = Hash::combine($optOutGroups, '{n}.name', '{n}.displayExtension'); - /** - * Return all Grouper Groups that the User has a role of owner/admin - * - * @param string $userId - * @param array $cfg - * - * @return array - * @throws GrouperLiteWidgetException - * @since COmanage Registry v4.4.0 - */ - public function getOwnedGroups(string $userId, array $cfg): array - { - if(empty($userId)) { - return false; - } + foreach ($memberOfGroups as &$memberOfGroup) { + $memberOfGroup['optOut'] = isset($optOutGroupsNames[$memberOfGroup['name']]); + } - $this->initApi($cfg); + return $this->getFriendlyWorkingGroupName($memberOfGroups, 'member'); - try { - $resultsAdmin = $this->grouperAPI->getUserMemberships($userId, $userId, GrouperGroupTypeEnum::ADMIN); - $resultsUpdate = $this->grouperAPI->getUserMemberships($userId, $userId, GrouperGroupTypeEnum::UPDATE); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - - return $this->removeDuplicates($resultsAdmin, $resultsUpdate); } /** - * Return all Grouper Groups that - * - the User(me) has a role of owner/admin - * - the User(member User) is a member + * Find People based on mode and term * - * @param string $managerId - * @param string $memberId - * @param array $cfg + * @param integer $coId CO ID + * @param string $mode Search mode to apply filters for + * @param string|null $term Search block * - * @return array - * @throws GrouperLiteWidgetException + * @return array Array of CO Person records * @since COmanage Registry v4.4.0 */ - public function getManagedUsers(string $managerId, string $memberId, array $cfg): array { - if(empty($managerId) || empty($memberId)) { - return false; - } + public function findForPicker(int $coId, string $mode, ?string $term): array + { + $coPersonIds = []; + $this->CoPerson = ClassRegistry::init('CoPerson'); - $this->initApi($cfg); + // jquery Autocomplete sends the search as url?term=foo + if(!empty($term)) { + // Leverage-model-specific keyword search - try { - $resultsManagerAdmin = $this->grouperAPI->getUserMemberships($managerId, $managerId, GrouperGroupTypeEnum::ADMIN); - $resultsManagerUpdate = $this->grouperAPI->getUserMemberships($managerId, $managerId, GrouperGroupTypeEnum::UPDATE); - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; + // Note EmailAddress and Identifier don't support substring search + foreach(array('Name', 'EmailAddress', 'Identifier') as $m) { + $hits = $this->CoPerson->$m->search($coId, $term, 25); + + $coPersonIds = array_merge($coPersonIds, Hash::extract($hits, '{n}.CoPerson.id')); + } } - $managerGroupSet = $this->removeDuplicates($resultsManagerAdmin, $resultsManagerUpdate); + $coPersonIds = array_unique($coPersonIds); - try { - // Groups the user is a member of - $membersGroup = $this->grouperAPI->getUserGroups($managerId, $memberId); - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } + // Look up additional information to provide hints as to which person is which. + // We only do this when there are relatively small numbers of results to + // avoid making a bunch of database queries early in the search. - // Extract the names of the Groups the member-user is a member of - $memberGroupNames = Hash::extract($membersGroup, '{n}.name'); - // Return the groups the user can join and is not a member of - return array_values( // Restart indexing from 0(zero) on the final array - array_filter( // Return the groups the member-user is a member - $managerGroupSet, - static fn($value) => in_array($value['name'], $memberGroupNames) - ) - ); + return $this->dataConstructForPicker($coId, $term,$coPersonIds); } /** - * Potential use was for creating adhoc group by a user, not associated to WG. - * - * Gets all Stems/Folders where User is admin/owner + * Return array of Working Groups for display on COManage site. + * Logic is for each WG to have one key=>value of main WG name, then array of all associated + * Groups. * - * @param string $userId + * NOTE: This is a major hack due to Grouper not giving us the right logic for displaying, so have to run all + * groups through a mapped listing of types of Groups in a WG to see if match and then parse and massage to display * - * @return array Array of Stems/Folders from Grouper - * @throws GrouperLiteWidgetException - */ - public function getOwnedStems(string $userId): array - { - try { - return $this->grouperAPI->getUserMemberships($userId, $userId, GrouperGroupTypeEnum::ADMIN); - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - } + * @param array $groups Listing of Groups + * @return array Listing of Groups in WG format for display - /** - * Get members associated to a specific Grouper Group - * NOTE: This list only shows members, it does not pull in other groups that may be attached in Grouper as - * members - * - * @param string $actorUserId Id of User - * @param string $groupName - * @param array $cfg - * - * @return array Listing of members in requested Grouper Group - * @throws GrouperLiteWidgetException Captured in Controller - * @throws JsonException * @since COmanage Registry v4.4.0 */ - public function getGroupMembers(string $actorUserId, string $groupName, array $cfg): array - { - $this->initApi($cfg); - - try { - $groupMembers = $this->grouperAPI->getGroupMembers($actorUserId, $groupName); - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - - if (count($groupMembers) < 1) { - return $groupMembers; - } + private function getFriendlyWorkingGroupName(array $groups, string $method) { + $arrayIndex = 0; + $workingGroups = array(); - $finalMembers = []; - foreach ($groupMembers as $member) { - if ($member['sourceId'] !== 'g:gsa') { - $finalMembers[] = $member; + //First need to loop through all groups and pull in all top levels + $topLevelWG = array(); + foreach ($groups as $group) { + foreach ($this->wgStemsTopLevel as $stem) { + $len = strlen($stem); + if (substr(strtolower($group['name']), 0, $len) === $stem) { + $stemSections = explode(':', $group['name']); + //Get a third section, since will always by after ref:something:here + if (in_array($stemSections[2], $topLevelWG) === false) { + $topLevelWG[] = $stemSections[2]; + } + } } - } - return $finalMembers; - } - /** - * Add a member to a specific Grouper Group + //Loop through groups to see if possibly part of a Working Group + foreach ($groups as &$group) { + foreach ($this->wgStemsAllGroups as $stem) { + $len = strlen($stem); + // if match to name of group within WG mapping then start making a WG group array + if (substr(strtolower($group['name']), 0, $len) === $stem) { + $tempGroup = $group; + if ($stem == 'ref:incommon-collab' || $stem == 'ref:internet2-collab') { + $mainGroup = true; + } else { + $mainGroup = false; + } + $stemSections = explode(':', $group['name']); + $displaySections = explode(':', $group['displayName']); + //Get second to last stem section + $sectionCount = 2; + //If group not part of a top level WG, then do not show! + if (in_array($stemSections[$sectionCount], $topLevelWG) === false) { + break; + } + $tempGroup['WGName'] = $stemSections[$sectionCount]; + $tempGroup['WGShowName'] = $displaySections[$sectionCount]; + // Get user type, which is after the WG name + if (isset($stemSections[$sectionCount + 1])) { + $tempGroup['WGRole'] = $stemSections[$sectionCount + 1]; + } else { + $tempGroup['WGRole'] = ''; + } + $appCount = 0; + $appName = ''; + foreach ($stemSections as $stemSection) { + //Skip first entry + if ($appCount > 0) { + if ($appCount < $sectionCount) { + if ($appCount == 1) { + $appName = $stemSection; + } else { + $appName = $appName . " - " . $stemSection; + } + } + } + $appCount += 1; + } + //changed the way email list are displayed to actually show lists email address. + if ($appName == 'sympa - internet2' || $appName == 'sympa - incommon') { + if ($appName == 'sympa - internet2') { + $appName = $tempGroup['WGName'] . '@lists.' . 'internet2.edu'; + } else { + $appName = $tempGroup['WGName'] . '@lists.' . 'incommon.org'; + } + + } + $tempGroup['WGApp'] = $appName; + if ($method == 'member') { + if(!$mainGroup) { + $workingGroups[] = $tempGroup; + unset($groups[$arrayIndex]); + } + } else { + $workingGroups[] = $tempGroup; + unset($groups[$arrayIndex]); + } + } + } + $arrayIndex += 1; + } + $finalWorkingGroups = array(); + + foreach ($workingGroups as $workingGroup) { + //Need to set first group in final Working Group array + if (count($finalWorkingGroups) == 0) { + $finalWorkingGroups[] = array( + 'WGName' => $workingGroup['WGName'], + 'WGShowName' => $workingGroup['WGShowName'], + 'Groups' => array($workingGroup) + ); + } else { + $foundMatch = false; + foreach ($finalWorkingGroups as &$finalWorkingGroup) { + if ($finalWorkingGroup['WGName'] == $workingGroup['WGName']) { + $finalWorkingGroup['WGShowName'] = $workingGroup['WGShowName']; + $finalWorkingGroup['Groups'][] = $workingGroup; + $foundMatch = true; + } + } + if (!$foundMatch) { + $finalWorkingGroups[] = array( + 'WGName' => $workingGroup['WGName'], + 'WGShowName' => $workingGroup['WGShowName'], + 'Groups' => array($workingGroup) + ); + } + } + } + + $friendlyGroups = array_values($groups); + + //Now need to add the groups back together for one set + foreach ($friendlyGroups as $friendlyGroup) { + $finalWorkingGroups[] = $friendlyGroup; + } + + return $finalWorkingGroups; + } + + /** + * Retrieve the identifier for a CO Person * - * @param string $actAsUserId Id of User - * @param string $groupName - * @param string $addUserId - * @param array $cfg + * @param int $co_person_id + * @param string $ident_type * - * @return string success of Request - * @throws GrouperLiteWidgetException Captured in Controller - * @throws JsonException - * @since COmanage Registry v4.4.0 + * @return string|null */ - public function addGroupMember(string $actAsUserId, string $groupName, string $addUserId, array $cfg) + public function getIdentifierFromPersonId(int $co_person_id, string $ident_type): ?string { - $this->initApi($cfg); + if(empty($co_person_id) || empty($ident_type)) { + return null; + } - return $this->grouperAPI->addGroupMember($actAsUserId, $groupName, $addUserId); + // Find the identifier + $args = array(); + $args['conditions']['Identifier.type'] = $ident_type; + $args['conditions']['Identifier.status'] = SuspendableStatusEnum::Active; + $args['conditions']['Identifier.co_person_id'] = $co_person_id; + $args['contain'] = false; + + $this->Identifier = ClassRegistry::init('Identifier'); + + $identifier = $this->Identifier->find('first', $args); + if (!empty($identifier)) { + return $identifier['Identifier']['identifier']; + } + + return null; } /** - * Remove a member from a specific Grouper Group + * Get members associated to a specific Grouper Group + * NOTE: This list only shows members, it does not pull in other groups that may be attached in Grouper as + * members * - * @param string $userId Id of User + * @param string $actorUserId Id of User * @param string $groupName - * @param string $removeUserId * @param array $cfg * - * @return bool success of Request + * @return array Listing of members in requested Grouper Group * @throws GrouperLiteWidgetException Captured in Controller - * @throws JsonException Captured in Controller + * @throws JsonException * @since COmanage Registry v4.4.0 */ - public function removeGroupMember(string $userId, - string $groupName, - string $removeUserId, - array $cfg): bool + public function getGroupMembers(string $actorUserId, string $groupName, array $cfg): array { $this->initApi($cfg); - return $this->grouperAPI->removeGroupMember($userId, $groupName, $removeUserId); - } + try { + $groupMembers = $this->grouperAPI->getGroupMembers($actorUserId, $groupName); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; + } + if (count($groupMembers) < 1) { + return $groupMembers; + } + + $finalMembers = []; + foreach ($groupMembers as $member) { + if ($member['sourceId'] !== 'g:gsa') { + $finalMembers[] = $member; + } + + } + return $finalMembers; + } /** - * Return all Groups the User can JOIN - * Get all Groups with Optin attribute set and display ones User can join. - * Will Match up with Groups User is already a member of to determine which Optin groups to not display + * Return all Grouper Groups that + * - the User(me) has a role of owner/admin + * - the User (member User) is a member * + * @param string $actorUserId * @param string $userId * @param array $cfg * - * @return array Listing of Optin groups available in Grouper - * @throws GrouperLiteWidgetException Captured in Controller + * @return array + * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ - public function optinGroups(string $userId, array $cfg): array - { + public function getManagedUsers(string $actorUserId, string $userId, array $cfg): array { + if(empty($userId) || empty($actorUserId)) { + return false; + } + $this->initApi($cfg); try { - // Groups the user can join or leave - $joinOrLeave = $this->grouperAPI->getUserMemberships($userId, - $userId, - GrouperGroupTypeEnum::OPTINS); + $resultsManagerAdmin = $this->grouperAPI->getUserMemberships($userId, + $actorUserId, + GrouperGroupTypeEnum::ADMIN); + $resultsManagerUpdate = $this->grouperAPI->getUserMemberships($userId, + $actorUserId, + GrouperGroupTypeEnum::UPDATE); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } + $managerGroupSet = $this->removeDuplicates($resultsManagerAdmin, $resultsManagerUpdate); + try { // Groups the user is a member of - $userGroups = $this->grouperAPI->getUserGroups($userId, $userId); + $membersGroup = $this->grouperAPI->getUserGroups($actorUserId, $userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - // I am currently not a member to any Group. Return everything - if(empty($userGroups)) { - return $joinOrLeave; - } - - // Extract the names of the Groups the user is a member of - $userGroupsNames = Hash::extract($userGroups, '{n}.name'); + // Extract the names of the Groups the member-user is a member of + $memberGroupNames = Hash::extract($membersGroup, '{n}.name'); // Return the groups the user can join and is not a member of return array_values( // Restart indexing from 0(zero) on the final array - array_filter( // Return the groups I am currently not a member - $joinOrLeave, - static fn($value) => !in_array($value['name'], $userGroupsNames) + array_filter( // Return the groups the member-user is a member + $managerGroupSet, + static fn($value) => in_array($value['name'], $memberGroupNames) ) ); } /** - * Determine if User can use the Grouper Template to create a Working Group. + * Return all Grouper Groups that the User has a role of owner/admin * - * @param string $userId Id of User - * @return string T for True and F for False - * @throws GrouperLiteWidgetException + * @param string $userId + * @param string $actorUserId + * @param array $cfg * + * @return array + * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ - public function isTemplateUser(string $userId, array $cfg) + public function getOwnedGroups(string $userId, string $actorUserId, array $cfg): array { + if(empty($userId) || empty($actorUserId)) { + return false; + } + $this->initApi($cfg); try { - $isMember = $this->grouperAPI->isMemberOfGroup(GrouperSpecialGroups::TEMPLATE_CREATION_GROUP, $userId); + $resultsAdmin = $this->grouperAPI->getUserMemberships($userId, + $actorUserId, + GrouperGroupTypeEnum::ADMIN); + $resultsUpdate = $this->grouperAPI->getUserMemberships($userId, + $actorUserId, + GrouperGroupTypeEnum::UPDATE); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return $isMember ? 'T' : 'F'; + return $this->removeDuplicates($resultsAdmin, $resultsUpdate); } /** - * Create a new Grouper Group using the Template methodology in Grouper + * Potential use was for creating an adhoc group by a user, not associated to WG. * - * @param string $userId Id of User - * @param array $groupData Data needed to create new Grouper Group via Template - * @return array status and error message, if applicable - * @throws GrouperLiteWidgetException + * Gets all Stems/Folders where User is admin/owner * - * @since COmanage Registry v4.4.0 + * @param string $userId + * @param string $actorUserId + * + * @return array Array of Stems/Folders from Grouper + * @throws GrouperLiteWidgetException */ - public function createGroupWithTemplate(string $userId, array $groupData, array $cfg) + public function getOwnedStems(string $userId, string $actorUserId): array { - $this->initApi($cfg); - //Need to massage incoming data to meet Grouper Template requirements - $fields = array( - 'gsh_input_isSympa', - 'gsh_input_isSympaModerated', - 'gsh_input_isOptin', - 'gsh_input_isConfluence', - 'gsh_input_isJira' - ); - // Template does not except true/false, so convert to string and send that way - foreach ($fields as $field) { - ($groupData[$field] == '0') ? $groupData[$field] = 'false' : $groupData[$field] = 'true'; - } - - $args = array(); - $args['userId'] = $userId; - $args['data'] = $groupData; try { - return $this->grouperAPI->createGroupWithTemplate($args); + return $this->grouperAPI->getUserMemberships($userId, + $actorUserId, + GrouperGroupTypeEnum::ADMIN); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; @@ -592,22 +698,27 @@ public function createGroupWithTemplate(string $userId, array $groupData, array * Search for Groups/Lists related to Search term. * * Will import all records the user can see and then do search in this code rather than call Grouper WS Search - * functionality. This is due to the fact that the grouperName is autogenerated and this app needs to search + * functionality. This is because the grouperName is autogenerated and this app needs to search * attributes which the Grouper WS does not do. * - * @param array $conditions Listing of conditions for display of records, including UserId + * @param string $userId + * @param string $searchCriteria + * @param string $searchPage + * @param array $cfg + * * @return array Records that meet search criteria - * @throws Exception Captured in Controller - + * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ - public function getSearchedGroups(string $userId, string $searchCriteria, string $searchPage, array $cfg) + public function getSearchedGroups(string $userId, string $actorUserId, string $searchCriteria, string $searchPage, array $cfg): array { $this->initApi($cfg); try { // Breakout page where search was called and forward to appropriate method for processing - $pageResults = isset($searchPage) ? $this->$searchPage($userId, $cfg) : []; + $pageResults = isset($searchPage) ? $this->$searchPage(userId: $userId, + actorUserId: $actorUserId, + cfg: $cfg) : []; $returnResults = []; @@ -625,7 +736,7 @@ public function getSearchedGroups(string $userId, string $searchCriteria, string } return $searchCriteria == 'getSearchedGroups' ? $this->getFriendlyWorkingGroupName($returnResults, 'member') - : $returnResults; + : $returnResults; } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); @@ -634,142 +745,148 @@ public function getSearchedGroups(string $userId, string $searchCriteria, string } /** - * Return array of Working Groups for display on COManage site. - * Logic is for each WG to have one key=>value of main WG name, then array of all associated - * Groups. + * Determine if a User can use the Grouper Template to create a Working Group. * - * NOTE: This is a major hack due to Grouper not giving us the right logic for displaying, so have to run all - * groups through a mapped listing of types of Groups in a WG to see if match and then parse and massage to display + * @param string $userId User ID + * @param string $groupName Group Name + * @param array $cfg * - * @param array $groups Listing of Groups - * @return array Listing of Groups in WG format for display + * @return bool T for True and F for False + * @throws GrouperLiteWidgetException + * @since COmanage Registry v4.4.0 + */ + public function isGroupMember(string $userId, string $groupName, array $cfg): bool + { + $this->initApi($cfg); + + try { + $isMember = $this->grouperAPI->isMemberOfGroup($groupName, $userId); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; + } + return (bool)$isMember; + } + + /** + * Determine if a User can use the Grouper Template to create a Working Group. + * + * @param string $userId User ID + * @param array $cfg + * + * @return bool + * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ - private function getFriendlyWorkingGroupName(array $groups, string $method) { - $arrayIndex = 0; - $workingGroups = array(); + public function isTemplateUser(string $userId, array $cfg): bool + { + $this->initApi($cfg); - //First need to loop through all groups and pull in all top levels - $topLevelWG = array(); - foreach ($groups as $group) { - foreach ($this->wgStemsTopLevel as $stem) { - $len = strlen($stem); - if (substr(strtolower($group['name']), 0, $len) === $stem) { - $stemSections = explode(':', $group['name']); - //Get third section, since will always by after ref:something:here - if (in_array($stemSections[2], $topLevelWG) === false) { - $topLevelWG[] = $stemSections[2]; - } - } - } + try { + return $this->grouperAPI->isMemberOfGroup(GrouperSpecialGroups::TEMPLATE_CREATION_GROUP, $userId); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; } + } - //Loop through groups to see if possibly part of a Working Group - foreach ($groups as &$group) { - foreach ($this->wgStemsAllGroups as $stem) { - $len = strlen($stem); - // if match to name of group within WG mapping then start making a WG group array - if (substr(strtolower($group['name']), 0, $len) === $stem) { - $tempGroup = $group; - if ($stem == 'ref:incommon-collab' || $stem == 'ref:internet2-collab') { - $mainGroup = true; - } else { - $mainGroup = false; - } - $stemSections = explode(':', $group['name']); - $displaySections = explode(':', $group['displayName']); - //Get second to last stem section - $sectionCount = 2; - //If group not part of a top level WG, then do not show! - if (in_array($stemSections[$sectionCount], $topLevelWG) === false) { - break; - } - $tempGroup['WGName'] = $stemSections[$sectionCount]; - $tempGroup['WGShowName'] = $displaySections[$sectionCount]; - // Get user type, which is after the WG name - if (isset($stemSections[$sectionCount + 1])) { - $tempGroup['WGRole'] = $stemSections[$sectionCount + 1]; - } else { - $tempGroup['WGRole'] = ''; - } - $appCount = 0; - $appName = ''; - foreach ($stemSections as $stemSection) { - //Skip first entry - if ($appCount > 0) { - if ($appCount < $sectionCount) { - if ($appCount == 1) { - $appName = $stemSection; - } else { - $appName = $appName . " - " . $stemSection; - } - } - } - $appCount += 1; - } - //changed the way email list are displayed to actually show lists email address. - if ($appName == 'sympa - internet2' || $appName == 'sympa - incommon') { - if ($appName == 'sympa - internet2') { - $appName = $tempGroup['WGName'] . '@lists.' . 'internet2.edu'; - } else { - $appName = $tempGroup['WGName'] . '@lists.' . 'incommon.org'; - } + /** + * Internal process used by other functions to fetch Groups the User is a member of + * + * @param string $actorUserId + * @param string $userId + * @param array $cfg + * + * @return array Records of Groups from Grouper that the User belongs to + * @throws GrouperLiteWidgetException + * @since COmanage Registry v4.4.0 + */ + private function memberOfGroups(string $actorUserId, string $userId, array $cfg) + { + $this->initApi($cfg); - } - $tempGroup['WGApp'] = $appName; - if ($method == 'member') { - if(!$mainGroup) { - $workingGroups[] = $tempGroup; - unset($groups[$arrayIndex]); - } - } else { - $workingGroups[] = $tempGroup; - unset($groups[$arrayIndex]); - } - } - } - $arrayIndex += 1; + try { + return $this->grouperAPI->getUserGroups($actorUserId, $userId); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; } - $finalWorkingGroups = array(); + } - foreach ($workingGroups as $workingGroup) { - //Need to set first group in final Working Group array - if (count($finalWorkingGroups) == 0) { - $finalWorkingGroups[] = array( - 'WGName' => $workingGroup['WGName'], - 'WGShowName' => $workingGroup['WGShowName'], - 'Groups' => array($workingGroup) - ); - } else { - $foundMatch = false; - foreach ($finalWorkingGroups as &$finalWorkingGroup) { - if ($finalWorkingGroup['WGName'] == $workingGroup['WGName']) { - $finalWorkingGroup['WGShowName'] = $workingGroup['WGShowName']; - $finalWorkingGroup['Groups'][] = $workingGroup; - $foundMatch = true; - } - } - if (!$foundMatch) { - $finalWorkingGroups[] = array( - 'WGName' => $workingGroup['WGName'], - 'WGShowName' => $workingGroup['WGShowName'], - 'Groups' => array($workingGroup) - ); - } - } + /** + * Return all Groups the User can JOIN + * Get all Groups with Optin attribute set and display ones User can join. + * Will Match up with Groups User is already a member of to determine which Optin groups to not display + * + * @param string $userId + * @param string $actorUserId + * @param array $cfg + * + * @return array Listing of Optin groups available in Grouper + * @throws GrouperLiteWidgetException Captured in Controller + * @since COmanage Registry v4.4.0 + */ + public function optinGroups(string $userId, string $actorUserId, array $cfg): array + { + $this->initApi($cfg); + + try { + // Groups the user can join or leave + $joinOrLeave = $this->grouperAPI->getUserMemberships($userId, + $actorUserId, + GrouperGroupTypeEnum::OPTINS); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; } - $friendlyGroups = array_values($groups); + try { + // Groups the user is a member of + $userGroups = $this->grouperAPI->getUserGroups($actorUserId, $userId); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; + } - //Now need to add the groups back together for one set - foreach ($friendlyGroups as $friendlyGroup) { - $finalWorkingGroups[] = $friendlyGroup; + // I am currently not a member to any Group. Return everything + if(empty($userGroups)) { + return $joinOrLeave; } - return $finalWorkingGroups; + // Extract the names of the Groups the user is a member of + $userGroupsNames = Hash::extract($userGroups, '{n}.name'); + // Return the groups the user can join and is not a member of + return array_values( // Restart indexing from 0(zero) on the final array + array_filter( // Return the groups I am currently not a member + $joinOrLeave, + static fn($value) => !in_array($value['name'], $userGroupsNames) + ) + ); } + /** + * Remove a member from a specific Grouper Group + * + * @param string $actorUserId + * @param string $groupName + * @param string $removeUserId + * @param array $cfg + * + * @return bool success of Request + * @throws GrouperLiteWidgetException Captured in Controller + * @throws JsonException Captured in Controller + * @since COmanage Registry v4.4.0 + */ + public function removeGroupMember(string $actorUserId, + string $groupName, + string $removeUserId, + array $cfg): bool + { + $this->initApi($cfg); + + return $this->grouperAPI->removeGroupMember($actorUserId, $groupName, $removeUserId); + } /** * Removes duplicates where the user is the owner and the updater of the group. Just one line instead of two. @@ -798,31 +915,4 @@ public function removeDuplicates(array $arrOne, array $arrTwo) return $uniqueArr; } - - - /** - * Breakout Working Groups from AdHoc Groups. - * - * @param array $recordSet - * @return array[] - * - */ - public function breakoutWrkFromAdHocGroups(array $recordSet): array - { - $wgData = []; - $notWGData = []; - //Parse out the Working Groups from the Ad-hoc groups - foreach ($recordSet as $record) { - if (isset($record['WGName'])) { - $wgData[] = $record; - } else { - $notWGData[] = $record; - } - } - - return [ - 'adhoc' => $notWGData, - 'working' => $wgData - ]; - } } diff --git a/Model/GrouperLiteWidget.php b/Model/GrouperLiteWidget.php index bfa3d4a..d5b8aa3 100644 --- a/Model/GrouperLiteWidget.php +++ b/Model/GrouperLiteWidget.php @@ -27,25 +27,25 @@ class GrouperLiteWidget extends AppModel { // Define class name for cake - public $name = "GrouperLiteWidget"; + public $name = 'GrouperLiteWidget'; // Required by COmanage Plugins - public $cmPluginType = "dashboardwidget"; + public $cmPluginType = 'dashboardwidget'; // Document foreign keys - public $cmPluginHasMany = array(); + public $cmPluginHasMany = []; // Association rules from this model to other models - public $belongsTo = array(); + public $belongsTo = []; - public $hasMany = array(); + public $hasMany = [ + 'ActAsPerson' => ['dependent' => true] + ]; // Validation rules for table elements - public $validate = array(); + public $validate = []; public function cmPluginMenus() { - return array(); + return []; } - - } \ No newline at end of file diff --git a/View/CoGrouperLiteWidgets/display.ctp b/View/CoGrouperLiteWidgets/display.ctp index 8cb6201..0cfb893 100644 --- a/View/CoGrouperLiteWidgets/display.ctp +++ b/View/CoGrouperLiteWidgets/display.ctp @@ -29,25 +29,19 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -// Figure out the widget ID so we can overwrite the dashboard's widget div - -$divid = $vv_config['CoGrouperLiteWidget']['co_dashboard_widget_id']; - -$plugin = filter_var($vv_codw["CoDashboardWidget"]["plugin"],FILTER_SANITIZE_SPECIAL_CHARS); -$pl = Inflector::underscore($plugin); -$plcmodel = Inflector::pluralize($pl); - $this->extend('/GrouperGroups/base'); -echo $this->element('GrouperLiteWidget.base-styles'); +// Figure out the widget ID so we can overwrite the dashboard's widget div +$divid = $vv_config['CoGrouperLiteWidget']['co_dashboard_widget_id']; +$pl = Inflector::underscore(filter_var($vv_codw['CoDashboardWidget']['plugin'], FILTER_SANITIZE_SPECIAL_CHARS)); $idsuffix = rand(); ?> -
+
-
+
Html->image('GrouperLiteWidget.grouper-logo.png', array('class' => 'logo-fluid')); ?>
-

 

+

 

diff --git a/View/Elements/ActAsPeopleAutocomplete.ctp b/View/Elements/ActAsPeopleAutocomplete.ctp new file mode 100644 index 0000000..ed71650 --- /dev/null +++ b/View/Elements/ActAsPeopleAutocomplete.ctp @@ -0,0 +1,184 @@ + 0 ? '?time=' . time() : ''; + + +?> + + + + +
diff --git a/View/Elements/ActionItem.ctp b/View/Elements/ActionItem.ctp new file mode 100644 index 0000000..73d45f9 --- /dev/null +++ b/View/Elements/ActionItem.ctp @@ -0,0 +1,186 @@ + 0 ? '?time=' . time() : ''; + +?> + + + + + diff --git a/View/Elements/ActionSideBar.ctp b/View/Elements/ActionSideBar.ctp new file mode 100644 index 0000000..339bcfb --- /dev/null +++ b/View/Elements/ActionSideBar.ctp @@ -0,0 +1,41 @@ + + + + + diff --git a/View/Elements/base-styles.ctp b/View/Elements/base-styles.ctp deleted file mode 100644 index 48ea5e2..0000000 --- a/View/Elements/base-styles.ctp +++ /dev/null @@ -1,140 +0,0 @@ - \ No newline at end of file diff --git a/View/GrouperGroups/base.ctp b/View/GrouperGroups/base.ctp index ef2c37d..85df70d 100644 --- a/View/GrouperGroups/base.ctp +++ b/View/GrouperGroups/base.ctp @@ -1,39 +1,41 @@ Html->meta( - array('http-equiv' => 'Cache-Control', 'content' => 'no-cache, no-store, must-revalidate'), - null, - array('inline' => false) +print $this->Html->meta( + ['http-equiv' => 'Cache-Control', 'content' => 'no-cache, no-store, must-revalidate'], + null, + ['inline' => false] ); -echo $this->Html->meta( - array('http-equiv' => 'Pragma', 'content' => 'no-cache'), - null, - array('inline' => false) +print $this->Html->meta( + ['http-equiv' => 'Pragma', 'content' => 'no-cache'], + null, + ['inline' => false] ); -echo $this->Html->meta( - array('http-equiv' => 'Expires', 'content' => '0'), - null, - array('inline' => false) +print $this->Html->meta( + ['http-equiv' => 'Expires', 'content' => '0'], + null, + ['inline' => false] ); print $this->Html->script('GrouperLiteWidget.autocomplete.grouperplugin') . PHP_EOL; -print $this->element('GrouperLiteWidget.base-styles'); +print $this->Html->css('GrouperLiteWidget.co-grouper-base') . PHP_EOL; print $this->Html->css('GrouperLiteWidget.co-grouper-plugin') . PHP_EOL; - -?> - -Html->addCrumb(_txt('pl.grouperlite.crumb.root'), - array( - 'controller' => 'grouper_groups', - 'action' => 'groupmember' - ), - array('prepend' => true)); +if(isset($this->viewVars['vv_coid'])) { + $this->Html->addCrumb(_txt('pl.grouperlite.crumb.root'), + [ + 'plugin' => Inflector::underscore(filter_var($this->plugin, FILTER_SANITIZE_SPECIAL_CHARS)), + 'controller' => 'grouper_groups', + 'action' => 'groupmember', + 'co' => $this->viewVars['vv_coid'], + 'glid' => $this->viewVars['vv_config']['CoGrouperLiteWidget']['id'] + ], + ['prepend' => true] + ); +} ?>
- fetch('content'); ?> + fetch('content') ?>
\ No newline at end of file diff --git a/View/GrouperGroups/index.ctp b/View/GrouperGroups/index.ctp index 138e8a3..16261ea 100644 --- a/View/GrouperGroups/index.ctp +++ b/View/GrouperGroups/index.ctp @@ -1,28 +1,35 @@ -extend('/GrouperGroups/base'); ?> -Html->script('GrouperLiteWidget.vue-router.js') ?> -Html->addCrumb(_txt('pl.grouperlite.nav.memberships')); ?> +extend('/GrouperGroups/base'); + +// Add javascript +print $this->Html->script('GrouperLiteWidget.vue-router.js'); + +// Add Breadcrumb +$this->Html->addCrumb(_txt('pl.grouperlite.title.dashboard')); + + +$baseUrl = $vv_config['CoGrouperLiteWidget']['grouper_url']; +$path = '/grouper/grouperUi/app/UiV2Main.index'; +$groupOperation = '?operation=UiV2Group.viewGroup&groupId='; +$grouperUrlBase = $baseUrl . $path . $groupOperation; + +$htmlId = 'act-as-picker'; +$suffix = Configure::read('debug') > 0 ? '?time=' . time() : ''; +?>
- Html->image('GrouperLiteWidget.Grouper.jpg', array('class' => 'img-fluid mr-2', 'style' => 'height: 50px')); ?> -

+ Html->image('GrouperLiteWidget.Grouper.jpg', ['class' => 'img-fluid mr-2', 'style' => 'height: 50px']) ?> +

- - -
-
+ +
+
+ + + element('ActionSideBar', + compact('vv_config', + 'vv_act_as_people', + 'vv_coid', + 'vv_is_user_owner', + 'htmlId') + ) ?> +
\ No newline at end of file diff --git a/webroot/css/co-grouper-base.css b/webroot/css/co-grouper-base.css index 8080b7f..d2e2e5b 100644 --- a/webroot/css/co-grouper-base.css +++ b/webroot/css/co-grouper-base.css @@ -1,28 +1,153 @@ :root { - --red: #CC3333; + --red: #B32D2D; --blue: #1D7AB4; - --green: #22AA22; + --green: #1D871D; + --orange: #faa732; --teal: #1c6070; + --darkteal: #003f59; + --link: #0076a5; + --link-hover: #003f59; --danger: var(--red); --success: var(--green); --primary: var(--teal); + --warning: var(--orange); + --secondary: var(--darkteal) +} + +.text-grouper { + color: var(--primary); +} + +.text-sml { + font-size: 0.8rem; +} + +#content .material-icons.lg { + font-size: 1.2rem; +} + +.grouper .btn:not(.btn-link) { + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); + max-width: 220px; +} + +.grouper .btn:not([disabled]):hover { + text-decoration: none !important; + filter: brightness(1.1); } .grouper .btn.btn-primary { background-color: var(--primary); border-color: var(--primary); - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); } -.grouper .btn.btn-success.btn-grouper { +.grouper .btn.btn-success { background-color: var(--success); border-color: var(--success); - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); } -.grouper .btn.btn-danger.btn-grouper { +.grouper .btn.btn-danger { background-color: var(--danger); border-color: var(--danger); - color: white; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); -} \ No newline at end of file +} + +.grouper .btn.btn-secondary { + background-color: var(--secondary); + border-color: var(--secondary); +} + +.grouper .btn.btn-link, +.grouper a:not(.btn) { + color: var(--link); +} + +.grouper .btn.btn-link:hover, +.grouper a:not(.btn):hover { + color: var(--link-hover); +} + +.btn-grouper { + min-width: fit-content; +} + +#subscribers.loading .loader { + display: block; +} + +#subscribers.loading .subs, +#subscribers.error .subs { + display: none; +} + +#subscribers.error .msg { + display: block; +} + +#subscribers .loader, +#subscribers .msg { + display: none; +} + +.modal-open .ui-menu { + z-index: 2000; +} + +#grouper-loader { + pointer-events: none; + width: 100px; + display: block; + margin: 0 auto; + overflow: visible; + padding: 15px; + max-width: 100%; +} + +#grouper-loader circle { + transform-origin: center; + fill: var(--primary); + transition: ease; + animation-timing-function: ease-in-out !important; +} + +#search { + background-color: #F6F6F6; +} + +/* fade out half */ + +#grouper-loader.fade-out-half circle:nth-child(1) { + animation: fadeInHalf 0.9s -2.3s infinite reverse; +} + +#grouper-loader.fade-out-half circle:nth-child(2) { + animation: fadeInHalf 0.9s -1.3s infinite reverse; +} + +#grouper-loader.fade-out-half circle:nth-child(3) { + animation: fadeInHalf 0.9s -0.3s infinite reverse; +} + +.grouper_groups .person-item { + overflow-wrap: anywhere; +} + +.popover { + max-width: 400px; +} + +.ui-autocomplete { + max-height: 240px; + overflow-y: auto; + overflow-x: hidden; + padding-right: 20px; +} + +@keyframes fadeInHalf { + from { + opacity: 0.5; + } + + to { + opacity: 1; + } +} diff --git a/webroot/css/co-grouper-plugin.css b/webroot/css/co-grouper-plugin.css index 9b1cebf..6e8f27e 100644 --- a/webroot/css/co-grouper-plugin.css +++ b/webroot/css/co-grouper-plugin.css @@ -14,6 +14,10 @@ border-radius: .2rem; } +.btn-fit { + height: fit-content; +} + a { color: var(--primary); } @@ -335,4 +339,4 @@ a.list-group-item-action:hover .fa { animation: 1.2s linear infinite both loading; background-color: var(--teal); display: inline-block; -} \ No newline at end of file +} diff --git a/webroot/js/autocomplete.js b/webroot/js/autocomplete.js index 368fec1..3baf2b4 100644 --- a/webroot/js/autocomplete.js +++ b/webroot/js/autocomplete.js @@ -10,9 +10,9 @@ export default { type: String, default: 'add' }, - activeBtn: { + forceDisableBtn: { type: Boolean, - default: true + default: false }, renderBtn: { type: Boolean, @@ -23,10 +23,11 @@ export default { default: '' } }, - inject: ['txt', 'api'], + inject: ['txt', 'api', 'all'], data() { return { search: '', + enableBtn: false, val: '', item: null, limit: 15, @@ -40,39 +41,42 @@ export default { }, toKebabCase(str) { return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); - } + }, + enableBtnFunc() { + // The minimum length that i start search is 3. So we only enable the button when + // the input text value has at least three characters + return this.enableBtn && !this.forceDisableBtn + }, }, computed: { btnTxt() { return eval(`this.txt.${this.action}`) ?? eval(`this.txt.${this.icon}`) }, - isBtnDisabled() { - // The minimum length that i start search is 3. So we only enable the button when - // the input text value has at least three characters - return this.activeBtn || (this.search.length < 3) + autcompleteId() { + return `autocomplete-search-container-${this.action}` } }, mounted(el) { const input = $(this.$el).find(`#${this.toKebabCase(this.action)}-input`); + const action = this.action this.url = `${this.api.find}?co=${this.api.co}&mode=${this.api.mode}&page=${this.page}&limit=${this.limit}` input.autocomplete({ source: ( request, response ) => { - $("#grouper-search-container .co-loading-mini").show(); + $(`#autocomplete-search-container-${this.action} .co-loading-mini`).show(); $.ajax({ url: this.url, type: 'GET', dataType: "json", data: { - // XXX Change the term key to any other query key that fits your needs. + // XXX Change the term key to any other query key that fit your needs. term: request.term }, success: function (data) { - $("#grouper-search-container .co-loading-mini").hide(); - // If i have more data from before append at the end + $(`#autocomplete-search-container-${action} .co-loading-mini`).hide(); response( data ); }, error: function(data) { - $("#grouper-search-container .co-loading-mini").hide(); + $(`#autocomplete-search-container-${action} .co-loading-mini`).hide(); console.log('Autocomplete ajax error:', data) generateFlash('Find action failed', 'error'); } @@ -91,16 +95,17 @@ export default { this.item = ui.item; this.search = `${ui.item.label} (${ui.item.value})`; if(this.renderBtn) { - $(`#${this.toKebabCase(this.action)}-btn`).prop('disabled', false).focus(); + this.enableBtn = true; + $(`#${this.toKebabCase(this.action)}-btn`).focus(); } else { - // Since we are not rendering any button we will trigger the search + // Since we are not rendering any button, we will trigger the search this.performAction() } }, }) }, template: /*html*/` -
+
@@ -120,8 +125,8 @@ export default { class=" btn btn-grouper btn-primary px-4 border-0" type="button" @click="performAction()" - :disabled="isBtnDisabled"> - + :disabled="!enableBtnFunc()"> + {{ btnTxt }}
diff --git a/webroot/js/groups-table.js b/webroot/js/groups-table.js index 89ba9fd..1449197 100644 --- a/webroot/js/groups-table.js +++ b/webroot/js/groups-table.js @@ -30,7 +30,8 @@ export default { {{ group.description || txt.descrZeroState }} {{ groupStatus(group?.enabled) }} - +