From 70d448096344d8bf355e46a35f1d9888d10ffcba Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Wed, 6 Mar 2024 20:36:49 +0200 Subject: [PATCH 1/9] Add the Users i manage tab --- Controller/GrouperGroupsController.php | 10 ++++++++++ View/CoGrouperLiteWidgets/display.ctp | 12 ++++++++++++ View/GrouperGroups/index.ctp | 6 ++++++ webroot/js/grouper-groups-view.js | 4 ++++ 4 files changed, 32 insertions(+) diff --git a/Controller/GrouperGroupsController.php b/Controller/GrouperGroupsController.php index 76bedd8..ae3e336 100644 --- a/Controller/GrouperGroupsController.php +++ b/Controller/GrouperGroupsController.php @@ -596,6 +596,16 @@ public function restResponse(int $status, $this->response->send(); } + /** + * UserManager vue route for rendering + * + * @return void + */ + public function userManager(): void + { + $this->render('index'); + } + /** * Override the default sanity check performed in AppController * diff --git a/View/CoGrouperLiteWidgets/display.ctp b/View/CoGrouperLiteWidgets/display.ctp index ccf0c76..8cb6201 100644 --- a/View/CoGrouperLiteWidgets/display.ctp +++ b/View/CoGrouperLiteWidgets/display.ctp @@ -91,6 +91,18 @@ $idsuffix = rand(); 'glid' => $glid ) ); ?>" + }, + { + label: "", + url: "Html->url( + array( + 'plugin' => $pl, + 'controller' => 'grouper_groups', + 'action' => 'usermanager', + 'co' => $coid, + 'glid' => $glid + ) + ); ?>" } ] } diff --git a/View/GrouperGroups/index.ctp b/View/GrouperGroups/index.ctp index 48ed270..376f9e3 100644 --- a/View/GrouperGroups/index.ctp +++ b/View/GrouperGroups/index.ctp @@ -14,6 +14,7 @@ import GroupMember from 'webroot ?>grouper_lite_widget/js/page/GroupMember.js'; import GroupOptin from 'webroot ?>grouper_lite_widget/js/page/GroupOptin.js'; import GroupOwner from 'webroot ?>grouper_lite_widget/js/page/GroupOwner.js'; + import UserManager from 'webroot ?>grouper_lite_widget/js/page/UserManager.js'; ", optin: "", owner: "", + manager: "", }, columns: _txt('pl.grouperlite.table.name'), diff --git a/webroot/js/grouper-groups-view.js b/webroot/js/grouper-groups-view.js index e2a508d..7d75678 100644 --- a/webroot/js/grouper-groups-view.js +++ b/webroot/js/grouper-groups-view.js @@ -25,6 +25,10 @@ export default { { path: `/groupowner/co:${this.api.co}/glid:${this.api.glid}`, label: this.txt.tabs.owner + }, + { + path: `/usermanager/co:${this.api.co}/glid:${this.api.glid}`, + label: this.txt.tabs.manager } ] } From a3da55edac9e2dd942866cd91675e79a8de36ef4 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Thu, 7 Mar 2024 16:56:29 +0200 Subject: [PATCH 2/9] Make autocomplete component fully dynamic --- webroot/js/autocomplete.js | 49 ++++++++++++++++++++++++++------------ webroot/js/members.js | 2 +- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/webroot/js/autocomplete.js b/webroot/js/autocomplete.js index a0a5958..978bd08 100644 --- a/webroot/js/autocomplete.js +++ b/webroot/js/autocomplete.js @@ -5,6 +5,14 @@ export default { group: { type: String, default: "" + }, + action: { + type: String, + default: "addUser" + }, + icon: { + type: String, + default: 'add' } }, inject: ['txt', 'api'], @@ -16,8 +24,18 @@ export default { }; }, methods: { - addUser() { - this.$emit('add', this.item); + performAction() { + this.$emit(this.action, this.item); + }, + toKebabCase(str) { + console.log('to kebab') + console.log('str', str) + return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); + } + }, + computed: { + btnTxt() { + return eval(`this.txt.${this.action}`) } }, mounted(el) { @@ -27,20 +45,21 @@ export default { minLength: 3, maxShowItems: 15, focus: ( event, ui ) => { - $("#grouper-add-member-search-container .co-loading-mini").hide(); + $("#grouper-search-container .co-loading-mini").hide(); return false; }, open: function (event, ui) { - $("#grouper-add-member-search-container .co-loading-mini").hide(); + $("#grouper-search-container .co-loading-mini").hide(); }, - create: function (event, ui) { - $("#add-user-input").focus(); + create: (event, ui) => { + // debugger + $(`#${this.toKebabCase(this.action)}-input`).focus(); }, close: function (event, ui) { - $("#grouper-add-member-search-container .co-loading-mini").hide(); + $("#grouper-search-container .co-loading-mini").hide(); }, search: function (event, ui) { - $("#grouper-add-member-search-container .co-loading-mini").show(); + $("#grouper-search-container .co-loading-mini").show(); }, // XXX We need access to the parent data object. // As a result we have to use arrow function syntax (ES6) @@ -48,15 +67,15 @@ export default { this.val = ui.item.identifier; this.item = ui.item; this.search = `${ui.item.label} (${ui.item.value})`; - $("#addUserbutton").prop('disabled', false).focus(); + $(`#${this.toKebabCase(this.action)}-btn`).prop('disabled', false).focus(); return false; }, }).autocomplete( "instance" )._renderItem = formatCoPersonAutoselectItem; }, template: /*html*/` -
+
-
-
diff --git a/webroot/js/members.js b/webroot/js/members.js index 2fa0a09..4900d03 100644 --- a/webroot/js/members.js +++ b/webroot/js/members.js @@ -163,7 +163,7 @@ export default {
- +
From a7e076f6e9986720ccc5e09a644a17be35ce21dd Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Thu, 7 Mar 2024 17:12:51 +0200 Subject: [PATCH 3/9] add people picker element --- Controller/GrouperGroupsController.php | 1 + Lib/lang.php | 2 +- View/GrouperGroups/index.ctp | 1 - webroot/js/autocomplete.js | 6 ++---- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Controller/GrouperGroupsController.php b/Controller/GrouperGroupsController.php index ae3e336..7800d23 100644 --- a/Controller/GrouperGroupsController.php +++ b/Controller/GrouperGroupsController.php @@ -439,6 +439,7 @@ function isAuthorized(): array|bool $p['groupSubscribers'] = true; $p['addSubscriber'] = true; $p['findSubscriber'] = true; + $p['usermanager'] = true; $p['removeSubscriber'] = true; $p['groupCreate'] = true; diff --git a/Lib/lang.php b/Lib/lang.php index 34bad66..43afa4e 100644 --- a/Lib/lang.php +++ b/Lib/lang.php @@ -163,7 +163,7 @@ 'pl.grouperlite.form.template.value.positive' => 'Yes', 'pl.grouperlite.form.template.value.negative' => 'No', - 'pl.grouperlite.search.tags.text' => 'Search', + 'pl.grouperlite.title.search' => 'Search', 'pl.grouperlite.pagination.counter' => 'Viewing {:start}-{:end} of {:count}', 'pl.grouperlite.attributes.zero-state' => 'No Attributes Associated to this Group.', diff --git a/View/GrouperGroups/index.ctp b/View/GrouperGroups/index.ctp index 376f9e3..f22b2e9 100644 --- a/View/GrouperGroups/index.ctp +++ b/View/GrouperGroups/index.ctp @@ -92,7 +92,6 @@ removeSubscriberError: "", removeSubscriberSuccess: "", getSubscriberError: "", - searchTag: "", peoplePickerPlaceHolder: "", noaccess: "", empty: "", diff --git a/webroot/js/autocomplete.js b/webroot/js/autocomplete.js index 978bd08..8fb8a54 100644 --- a/webroot/js/autocomplete.js +++ b/webroot/js/autocomplete.js @@ -28,18 +28,16 @@ export default { this.$emit(this.action, this.item); }, toKebabCase(str) { - console.log('to kebab') - console.log('str', str) return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); } }, computed: { btnTxt() { - return eval(`this.txt.${this.action}`) + return eval(`this.txt.${this.action}`) ?? eval(`this.txt.${this.icon}`) } }, mounted(el) { - const input = $(this.$el).find('#add-user-input'); + const input = $(this.$el).find(`#${this.toKebabCase(this.action)}-input`); input.autocomplete({ source: `${this.api.find}?co=${this.api.co}&mode=${this.api.mode}`, minLength: 3, From d2fe650d8d637bdd914cabbed22893f3156fd049 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Thu, 7 Mar 2024 17:13:04 +0200 Subject: [PATCH 4/9] add user manager view --- webroot/js/page/UserManager.js | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 webroot/js/page/UserManager.js diff --git a/webroot/js/page/UserManager.js b/webroot/js/page/UserManager.js new file mode 100644 index 0000000..2c14fb8 --- /dev/null +++ b/webroot/js/page/UserManager.js @@ -0,0 +1,70 @@ +import Groups from '../groups.js'; +import PageCount from '../pagecount.js'; +import Pagination from '../pagination.js'; +import GroupsTable from '../groups-table.js'; +import Members from '../members.js'; +import Autocomplete from '../autocomplete.js'; +import Loader from '../loader.js'; + +export default { + components: { + Groups, + PageCount, + Pagination, + GroupsTable, + Members, + Autocomplete + }, + inject: ['api', 'txt'], + methods: { + showSubscribers(group) { + this.$refs.members.show(group); + }, + async findUserMemberships(user) { + const { identifier: id, label } = user; + this.loading = true; + const { displayExtension, name } = this.group; + const formData = new FormData(); + formData.append("userId", id); + formData.append("group", name); + const resp = await fetch(`${this.api.add}?group=${name}&userId=${id}`, { + method: "POST", + headers: { + "Accept": "application/json", + }, + body: formData + }); + if (resp.ok) { + await this.loadGroupSubscribers(this.group); + generateFlash(`${label} ${this.txt.findUserMembershipsSuccess} ${(displayExtension)}`, 'success'); + } else { + generateFlash(`${this.txt.findUserMembershipsError}`, 'error'); + let errorResponse = await resp.json(); + generateFlash(`${errorResponse.message}`, 'error'); + } + + this.loading = false; + } + }, + template: /*html*/` + + + + + + + + + + + + + + + + + ` +} \ No newline at end of file From 83727b0cca1a53bb024d77b35c02423302fab80d Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Thu, 7 Mar 2024 21:44:56 +0200 Subject: [PATCH 5/9] Backend work, frontend loader and disable action button. --- Controller/GrouperGroupsController.php | 101 ++++++++++++++++++------- Lib/lang.php | 1 + Model/GrouperGroup.php | 49 ++++++++++++ View/GrouperGroups/index.ctp | 2 + webroot/js/autocomplete.js | 10 +-- webroot/js/members.js | 2 +- webroot/js/page/UserManager.js | 50 +++++------- 7 files changed, 150 insertions(+), 65 deletions(-) diff --git a/Controller/GrouperGroupsController.php b/Controller/GrouperGroupsController.php index 7800d23..1657acd 100644 --- a/Controller/GrouperGroupsController.php +++ b/Controller/GrouperGroupsController.php @@ -40,7 +40,7 @@ class GrouperGroupsController extends GrouperLiteWidgetAppController public $helpers = array('Html', 'Form', 'Flash'); // Dynamic properties are deprecated, so we will define the property here - protected $userId = null; + private $userId = null; public $uses = array( 'GrouperLiteWidget.GrouperGroup', @@ -146,6 +146,14 @@ public function beforeRender() { $this->set('vv_coid', $this->cur_co['Co']['id']); } + /** + * @return null + */ + public function getUserId() + { + return $this->userId; + } + /** * Perform a "keyword" search for CO People, sort of like the CO Dashboard * cross controller search, but intended specifically for "people finder" @@ -234,7 +242,7 @@ public function groupSubscribers(): void /** * Listing of all Grouper Groups owned/admin by User Or search those Grouper Groups */ - public function groupOwnerApi() { + public function groupOwnerApi(): void { //Set initial setting $arguments = [ 'userId' => $this->userId, @@ -261,7 +269,7 @@ public function groupOwnerApi() { CakeLog::write('error', __METHOD__ . "::{$errorHint}: " . var_export($e->getMessage(), true)); $this->restResponse(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, ErrorsEnum::Exception); - $this->set('groupsowners', []); + $this->set('groupowners', []); $this->Flash->set(_txt('pl.grouperlite.message.flash.owner-group-failed'), array('key' => 'error')); return; } @@ -275,7 +283,7 @@ public function groupOwnerApi() { * This includes self-joined Optin Groups, as well as required Groups User cannot leave * */ - public function groupMemberApi() { + public function groupMemberApi(): void { //Set initial setting $arguments = [ 'userId' => $this->userId, @@ -404,7 +412,7 @@ public function index(): void * @return array|bool Permissions * @since COmanage Registry v3.2.0 */ - function isAuthorized(): array|bool + public function isAuthorized(): array|bool { $roles = $this->Role->calculateCMRoles(); $cfg = $this->CoGrouperLiteWidget->getConfig(); @@ -420,32 +428,32 @@ function isAuthorized(): array|bool && is_array($identifiers) && isset($identifiers['Identifier']['identifier']) ) { - $this->userId = $identifiers['Identifier']['identifier']; + $this->setUserId($identifiers['Identifier']['identifier']); } // Determine what operations this user can perform // Construct the permission set for this user, which will also be passed to the view. - //Note: Leaving in current format, in case need to restrict certain pages, can just remove true and add params. - $p = array(); - - $p['index'] = true; - $p['groupowner'] = true; - $p['groupownerapi'] = true; - $p['groupoptin'] = true; - $p['groupoptinapi'] = true; - $p['groupmember'] = true; - $p['groupmemberapi'] = true; - $p['getBaseConfig'] = true; - $p['groupSubscribers'] = true; - $p['addSubscriber'] = true; - $p['findSubscriber'] = true; - $p['usermanager'] = true; - $p['removeSubscriber'] = true; - - $p['groupCreate'] = true; - $p['joinGroup'] = true; - $p['leaveGroup'] = true; - $p['groupcreatetemplate'] = true; + $p = []; + + $p['index'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupowner'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupownerapi'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupoptin'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupoptinapi'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupmember'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupmemberapi'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['getBaseConfig'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupSubscribers'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['addSubscriber'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['findSubscriber'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['usermanager'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['usermanagerapi'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['removeSubscriber'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + + $p['groupCreate'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['joinGroup'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['leaveGroup'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupcreatetemplate'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); $this->set('permissions', $p); @@ -597,6 +605,14 @@ public function restResponse(int $status, $this->response->send(); } + /** + * @param null $userId + */ + private function setUserId($userId): void + { + $this->userId = $userId; + } + /** * UserManager vue route for rendering * @@ -607,6 +623,37 @@ public function userManager(): void $this->render('index'); } + /** + * Display all Groups for the user i manage + */ + public function userManagerApi(): void + { + //Set initial setting + $cfg = $this->CoGrouperLiteWidget->getConfig(); + + if (!isset($this->request->query['memberid'])) { + $this->restResponse(HttpStatusCodesEnum::HTTP_BAD_REQUEST, ErrorsEnum::Exception); + } + + $memberId = urldecode($this->request->query['memberid']); + + try { + $groupsimanage = $this->GrouperGroup->getManagedUsers($this->userId, + $memberId, + $cfg); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . '::get Managed Users: ' . var_export($e->getMessage(), true)); + $this->restResponse(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, ErrorsEnum::Exception); + + $this->set('groupsimanage', []); + return; + } + + $this->set(compact('groupsimanage')); + $this->set('_serialize', 'groupsimanage'); + + } + /** * Override the default sanity check performed in AppController * diff --git a/Lib/lang.php b/Lib/lang.php index 43afa4e..d5e490b 100644 --- a/Lib/lang.php +++ b/Lib/lang.php @@ -121,6 +121,7 @@ 'pl.grouperlite.action.view-members' => 'View members', 'pl.grouperlite.action.grouper' => 'Grouper', 'pl.grouperlite.action.members' => 'Members', + 'pl.grouperlite.action.memberships' => 'Memberships', 'pl.grouperlite.action.close' => 'Close', 'pl.grouperlite.action.clear' => 'Clear', 'pl.grouperlite.action.add-user' => 'Add', diff --git a/Model/GrouperGroup.php b/Model/GrouperGroup.php index 49172c6..e32c589 100644 --- a/Model/GrouperGroup.php +++ b/Model/GrouperGroup.php @@ -318,6 +318,55 @@ public function getOwnedGroups(string $userId, array $cfg): array 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 + * + * @param string $userId + * @param string $memberId + * @param array $cfg + * + * @return array + * @throws GrouperLiteWidgetException + * @since COmanage Registry v4.4.0 + */ + public function getManagedUsers(string $managerId, string $memberId, array $cfg): array { + if(empty($managerId) || empty($memberId)) { + return false; + } + + $this->initApi($cfg); + + 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; + } + + $managerGroupSet = $this->removeDuplicates($resultsManagerAdmin, $resultsManagerUpdate); + + try { + // Groups the user is a member of + $membersGroup = $this->grouperAPI->getUserGroups($memberId, $memberId); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': An error occurred'); + throw $e; + } + + // 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) + ) + ); + } + /** * Potential use was for creating adhoc group by a user, not associated to WG. * diff --git a/View/GrouperGroups/index.ctp b/View/GrouperGroups/index.ctp index f22b2e9..138e8a3 100644 --- a/View/GrouperGroups/index.ctp +++ b/View/GrouperGroups/index.ctp @@ -93,6 +93,7 @@ removeSubscriberSuccess: "", getSubscriberError: "", peoplePickerPlaceHolder: "", + memberships: "", noaccess: "", empty: "", join: "", @@ -129,6 +130,7 @@ add: "webroot ?>grouper_lite_widget/grouper_groups/addSubscriber/co:/glid:", group: "webroot ?>grouper_lite_widget/grouper_groups/groupSubscribers/co:/glid:", memberships: "webroot ?>grouper_lite_widget/grouper_groups/groupmemberapi/co:/glid:", + managing: "webroot ?>grouper_lite_widget/grouper_groups/usermanagerapi/co:/glid:", optin: "webroot ?>grouper_lite_widget/grouper_groups/groupoptinapi/co:/glid:", owner: "webroot ?>grouper_lite_widget/grouper_groups/groupownerapi/co:/glid:", }, diff --git a/webroot/js/autocomplete.js b/webroot/js/autocomplete.js index 8fb8a54..c86d92f 100644 --- a/webroot/js/autocomplete.js +++ b/webroot/js/autocomplete.js @@ -2,10 +2,6 @@ export default { props: { - group: { - type: String, - default: "" - }, action: { type: String, default: "addUser" @@ -13,6 +9,10 @@ export default { icon: { type: String, default: 'add' + }, + activeBtn: { + type: Boolean, + default: true } }, inject: ['txt', 'api'], @@ -86,7 +86,7 @@ export default { class=" btn btn-grouper btn-primary px-4 border-0" type="button" @click="performAction()" - :disabled="true"> + :disabled="activeBtn"> {{ btnTxt }} diff --git a/webroot/js/members.js b/webroot/js/members.js index 4900d03..c031bd1 100644 --- a/webroot/js/members.js +++ b/webroot/js/members.js @@ -163,7 +163,7 @@ export default {
- +
diff --git a/webroot/js/page/UserManager.js b/webroot/js/page/UserManager.js index 2c14fb8..cbc874d 100644 --- a/webroot/js/page/UserManager.js +++ b/webroot/js/page/UserManager.js @@ -13,32 +13,31 @@ export default { Pagination, GroupsTable, Members, - Autocomplete + Autocomplete, + Loader + }, + data() { + return { + loading: false, + result: [], + } }, inject: ['api', 'txt'], methods: { - showSubscribers(group) { - this.$refs.members.show(group); - }, - async findUserMemberships(user) { + async findManagedUsers(user) { const { identifier: id, label } = user; this.loading = true; - const { displayExtension, name } = this.group; - const formData = new FormData(); - formData.append("userId", id); - formData.append("group", name); - const resp = await fetch(`${this.api.add}?group=${name}&userId=${id}`, { - method: "POST", + const resp = await fetch(`${this.api.managing}?memberid=${id}`, { headers: { "Accept": "application/json", }, - body: formData + method: "GET" }); if (resp.ok) { - await this.loadGroupSubscribers(this.group); - generateFlash(`${label} ${this.txt.findUserMembershipsSuccess} ${(displayExtension)}`, 'success'); + this.result = await resp.json(); + console.log('result', this.result) + generateFlash(`${id} has ${this.result.length} ${this.txt.memberships}`, 'success'); } else { - generateFlash(`${this.txt.findUserMembershipsError}`, 'error'); let errorResponse = await resp.json(); generateFlash(`${errorResponse.message}`, 'error'); } @@ -47,24 +46,11 @@ export default { } }, template: /*html*/` - - - - - - - - - - - - - - - - + ` } \ No newline at end of file From cf2042555c0ad186a69eded488f132414e874c74 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Fri, 8 Mar 2024 10:46:19 +0200 Subject: [PATCH 6/9] autocomplete people picker improvements --- webroot/js/autocomplete.js | 9 +++++++-- webroot/js/members.js | 2 +- webroot/js/page/UserManager.js | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/webroot/js/autocomplete.js b/webroot/js/autocomplete.js index c86d92f..ae8235a 100644 --- a/webroot/js/autocomplete.js +++ b/webroot/js/autocomplete.js @@ -25,7 +25,7 @@ export default { }, methods: { performAction() { - this.$emit(this.action, this.item); + this.$emit('callback', this.item); }, toKebabCase(str) { return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); @@ -34,6 +34,11 @@ export default { 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) } }, mounted(el) { @@ -86,7 +91,7 @@ export default { class=" btn btn-grouper btn-primary px-4 border-0" type="button" @click="performAction()" - :disabled="activeBtn"> + :disabled="isBtnDisabled"> {{ btnTxt }} diff --git a/webroot/js/members.js b/webroot/js/members.js index c031bd1..7c900ac 100644 --- a/webroot/js/members.js +++ b/webroot/js/members.js @@ -163,7 +163,7 @@ export default {
- +
diff --git a/webroot/js/page/UserManager.js b/webroot/js/page/UserManager.js index cbc874d..0677a2c 100644 --- a/webroot/js/page/UserManager.js +++ b/webroot/js/page/UserManager.js @@ -46,7 +46,7 @@ export default { } }, template: /*html*/` - Date: Fri, 8 Mar 2024 21:11:05 +0200 Subject: [PATCH 7/9] autocomplete improvements.Other improvements and minor fixes. --- Controller/GrouperGroupsController.php | 4 +-- Model/GrouperGroup.php | 12 +++++-- View/GrouperGroups/base.ctp | 1 + webroot/js/autocomplete.grouperplugin.js | 21 +++++++++++ webroot/js/autocomplete.js | 46 +++++++++++++++--------- webroot/js/members.js | 2 +- webroot/js/page/GroupMember.js | 6 +--- webroot/js/page/UserManager.js | 14 ++++++-- 8 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 webroot/js/autocomplete.grouperplugin.js diff --git a/Controller/GrouperGroupsController.php b/Controller/GrouperGroupsController.php index 1657acd..b9749a0 100644 --- a/Controller/GrouperGroupsController.php +++ b/Controller/GrouperGroupsController.php @@ -67,8 +67,8 @@ public function addSubscriber(): void $this->layout = null; $this->autoRender = false; - $groupName = urldecode($this->request->query['group']); - $addUserId = urldecode($this->request->query['userId']); + $groupName = $this->request->data['group']; + $addUserId = $this->request->data['userId']; // Need to see if coming from AdHoc or from a WG (Working Group) $groupNameFormatted = strpos($groupName, ':') === false ? 'ref:incommon-collab:' . $groupName . ':users' diff --git a/Model/GrouperGroup.php b/Model/GrouperGroup.php index e32c589..e90fb0b 100644 --- a/Model/GrouperGroup.php +++ b/Model/GrouperGroup.php @@ -217,8 +217,10 @@ public function findForPicker(int $coId, string $mode, ?string $term): array $idArr = $p['Identifier']; $emailArr = $p['EmailAddress']; $email = ''; + $email_short = ''; $emailLabel = ''; $id = ''; + $id_short = ''; $idLabel = ''; // Iterate over the email array @@ -228,6 +230,7 @@ public function findForPicker(int $coId, string $mode, ?string $term): array foreach($emailArr as $e) { if($e['type'] == $pickerEmailType) { $email = $e['mail']; + $email_short = mb_strimwidth($e['mail'], 0, 30, '...'); break; } } @@ -243,7 +246,8 @@ public function findForPicker(int $coId, string $mode, ?string $term): array } foreach($idArr as $i) { if($i['type'] == $pickerIdentifierType) { - $id = mb_strimwidth($i['identifier'], 0, 30, '...'); + $id_short = mb_strimwidth($i['identifier'], 0, 30, '...'); + $id = $i['identifier']; break; } } @@ -255,9 +259,13 @@ public function findForPicker(int $coId, string $mode, ?string $term): array 'value' => $p['CoPerson']['id'], 'label' => $label, 'email' => $email, + 'emailShort' => $email_short, 'emailLabel' => $emailLabel, + 'emailType' => $pickerEmailType, 'identifier' => $id, - 'identifierLabel' => $idLabel + 'identifierShort' => $id_short, + 'identifierLabel' => $idLabel, + 'identifierType' => $pickerIdentifierType ); } } diff --git a/View/GrouperGroups/base.ctp b/View/GrouperGroups/base.ctp index 8301abc..ef2c37d 100644 --- a/View/GrouperGroups/base.ctp +++ b/View/GrouperGroups/base.ctp @@ -16,6 +16,7 @@ echo $this->Html->meta( array('inline' => false) ); +print $this->Html->script('GrouperLiteWidget.autocomplete.grouperplugin') . PHP_EOL; print $this->element('GrouperLiteWidget.base-styles'); print $this->Html->css('GrouperLiteWidget.co-grouper-plugin') . PHP_EOL; diff --git a/webroot/js/autocomplete.grouperplugin.js b/webroot/js/autocomplete.grouperplugin.js new file mode 100644 index 0000000..0b37dc9 --- /dev/null +++ b/webroot/js/autocomplete.grouperplugin.js @@ -0,0 +1,21 @@ +$.widget( "ui.autocomplete", $.ui.autocomplete, { + _renderMenu: function( ul, items ) { + var that = this; + $.each( items, function( index, item ) { + that._renderItemData( ul, item ); + }); + }, + _renderItem: function( ul, item ) { + let itemMarkup = '
'; + itemMarkup += '
' + item.label + '
'; + if(item?.emailShort != '') { + itemMarkup += ''; + } + if(item?.identifierShort != '') { + itemMarkup += '
' + item.identifierLabel + '' + item.identifierShort + '
'; + } + itemMarkup += '
'; + + return $("
  • ").append(itemMarkup).appendTo(ul); + } +}); \ No newline at end of file diff --git a/webroot/js/autocomplete.js b/webroot/js/autocomplete.js index ae8235a..52d81c7 100644 --- a/webroot/js/autocomplete.js +++ b/webroot/js/autocomplete.js @@ -21,6 +21,9 @@ export default { search: '', val: '', item: null, + limit: 15, + minLength: 3, + url: '' }; }, methods: { @@ -43,27 +46,36 @@ export default { }, mounted(el) { const input = $(this.$el).find(`#${this.toKebabCase(this.action)}-input`); + this.url = `${this.api.find}?co=${this.api.co}&mode=${this.api.mode}&page=${this.page}&limit=${this.limit}` input.autocomplete({ - source: `${this.api.find}?co=${this.api.co}&mode=${this.api.mode}`, - minLength: 3, - maxShowItems: 15, - focus: ( event, ui ) => { - $("#grouper-search-container .co-loading-mini").hide(); - return false; - }, - open: function (event, ui) { - $("#grouper-search-container .co-loading-mini").hide(); + source: ( request, response ) => { + $("#grouper-search-container .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. + term: request.term + }, + success: function (data) { + $("#grouper-search-container .co-loading-mini").hide(); + // If i have more data from before append at the end + response( data ); + }, + error: function(data) { + $("#grouper-search-container .co-loading-mini").hide(); + console.log('Autocomplete ajax error:', data) + generateFlash('Find action failed', 'error'); + } + }); }, + delay: 1000, + minLength: this.minLength, + maxShowItems: this.limit, create: (event, ui) => { - // debugger $(`#${this.toKebabCase(this.action)}-input`).focus(); }, - close: function (event, ui) { - $("#grouper-search-container .co-loading-mini").hide(); - }, - search: function (event, ui) { - $("#grouper-search-container .co-loading-mini").show(); - }, // XXX We need access to the parent data object. // As a result we have to use arrow function syntax (ES6) select: (event, ui) => { @@ -73,7 +85,7 @@ export default { $(`#${this.toKebabCase(this.action)}-btn`).prop('disabled', false).focus(); return false; }, - }).autocomplete( "instance" )._renderItem = formatCoPersonAutoselectItem; + }) }, template: /*html*/`
    diff --git a/webroot/js/members.js b/webroot/js/members.js index 7c900ac..9c3edac 100644 --- a/webroot/js/members.js +++ b/webroot/js/members.js @@ -116,7 +116,7 @@ export default { const formData = new FormData(); formData.append("userId", id); formData.append("group", name); - const resp = await fetch(`${this.api.add}?group=${name}&userId=${id}`, { + const resp = await fetch(`${this.api.add}`, { method: "POST", headers: { "Accept": "application/json", diff --git a/webroot/js/page/GroupMember.js b/webroot/js/page/GroupMember.js index a6f781b..09da695 100644 --- a/webroot/js/page/GroupMember.js +++ b/webroot/js/page/GroupMember.js @@ -23,6 +23,7 @@ export default { }, methods: { showSubscribers(group) { + // Create and show the modal this.$refs.members.show(group); }, }, @@ -31,11 +32,6 @@ export default { setQueryParam('view', newValue); } }, - computed: { - routePath() { - return `/groupmember/co:${this.api.co}/glid:${this.api.glid}`; - } - }, mounted() { let view = hasQueryParam('view') ? getQueryParam('view') : 'adhoc'; if (view !== 'working' && view !== 'adhoc') { diff --git a/webroot/js/page/UserManager.js b/webroot/js/page/UserManager.js index 0677a2c..7d9cd80 100644 --- a/webroot/js/page/UserManager.js +++ b/webroot/js/page/UserManager.js @@ -1,4 +1,3 @@ -import Groups from '../groups.js'; import PageCount from '../pagecount.js'; import Pagination from '../pagination.js'; import GroupsTable from '../groups-table.js'; @@ -8,7 +7,6 @@ import Loader from '../loader.js'; export default { components: { - Groups, PageCount, Pagination, GroupsTable, @@ -24,6 +22,9 @@ export default { }, inject: ['api', 'txt'], methods: { + dummy() { + console.log('hi from dummy') + }, async findManagedUsers(user) { const { identifier: id, label } = user; this.loading = true; @@ -52,5 +53,14 @@ export default { :activeBtn="loading" /> + + + ` } \ No newline at end of file From 6af044c35185a212485486114111717b87a151ad Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Wed, 13 Mar 2024 20:34:17 +0200 Subject: [PATCH 8/9] pass userManagerId to getUserGroups API Call --- Model/GrouperGroup.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Model/GrouperGroup.php b/Model/GrouperGroup.php index e90fb0b..1468b07 100644 --- a/Model/GrouperGroup.php +++ b/Model/GrouperGroup.php @@ -331,7 +331,7 @@ public function getOwnedGroups(string $userId, array $cfg): array * - the User(me) has a role of owner/admin * - the User(member User) is a member * - * @param string $userId + * @param string $managerId * @param string $memberId * @param array $cfg * @@ -358,7 +358,7 @@ public function getManagedUsers(string $managerId, string $memberId, array $cfg) try { // Groups the user is a member of - $membersGroup = $this->grouperAPI->getUserGroups($memberId, $memberId); + $membersGroup = $this->grouperAPI->getUserGroups($managerId, $memberId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; From 060b668765d5bb00eba52b977a6ba34db774c734 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Thu, 14 Mar 2024 12:56:21 +0200 Subject: [PATCH 9/9] Present data in table.Add and test action. --- webroot/css/co-grouper-plugin.css | 8 ++++ webroot/js/groups-table.js | 29 +++++++++++--- webroot/js/page/UserManager.js | 64 ++++++++++++++++++++++++------- webroot/js/pagecount.js | 6 ++- 4 files changed, 87 insertions(+), 20 deletions(-) diff --git a/webroot/css/co-grouper-plugin.css b/webroot/css/co-grouper-plugin.css index 4736499..9b1cebf 100644 --- a/webroot/css/co-grouper-plugin.css +++ b/webroot/css/co-grouper-plugin.css @@ -327,4 +327,12 @@ a.list-group-item-action:hover .fa { right: 38px; bottom: 10px; margin-right: -26px; +} + +.grouper_groups #co-loading span, +.grouper_groups #co-loading-redirect span, +.grouper_groups .co-loading-mini span { + 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/groups-table.js b/webroot/js/groups-table.js index 49fe26a..89ba9fd 100644 --- a/webroot/js/groups-table.js +++ b/webroot/js/groups-table.js @@ -6,7 +6,10 @@ export default { Table ], data() {}, - computed: { + methods: { + groupStatus(status) { + return status === 'T' ? 'Enabled' : 'Disabled' + } }, created() {}, template: /*html*/` @@ -25,9 +28,10 @@ export default { — {{ group.description || txt.descrZeroState }} - {{ group.enabled && group.enabled === 'T' ? 'Enabled' : 'Disabled' }} + {{ groupStatus(group?.enabled) }} + + - + + + + {{ txt.grouper }}   diff --git a/webroot/js/page/UserManager.js b/webroot/js/page/UserManager.js index 7d9cd80..256f868 100644 --- a/webroot/js/page/UserManager.js +++ b/webroot/js/page/UserManager.js @@ -17,17 +17,51 @@ export default { data() { return { loading: false, - result: [], + results: [], + user: {}, + rowLoading: false, + searched: false } }, inject: ['api', 'txt'], methods: { - dummy() { - console.log('hi from dummy') + reset() { + this.searched = false + // If you are in the middle of a row action we do not want to clear + // the results because we are breaking the user experience + if(!this.rowLoading) { + this.results = [] + } + }, + async removeUser(group) { + const {name, displayExtension} = group; + const { identifier: id, label: username, email } = this.user; + // This will trigger the inline loader + group.loading = true; + this.rowLoading = true; + const resp = await fetch(`${this.api.remove}?group=${(name)}&userId=${id}`, { + method: "DELETE", + headers: { + "Accept": "application/json" + } + }); + if (resp.ok) { + this.subscribers = []; + await this.findGroupsForManagedUser(this.user); + generateFlash(`${username ?? email} ${this.txt.removeSubscriberSuccess} ${(displayExtension)}`, 'success'); + } else { + this.disabled = [ ...this.disabled, id ]; + generateFlash(this.txt.removeSubscriberError, 'error'); + let errorResponse = await resp.json(); + generateFlash(`${errorResponse.message}`, 'error'); + } }, - async findManagedUsers(user) { - const { identifier: id, label } = user; - this.loading = true; + async findGroupsForManagedUser(user) { + this.user = user + // Reset + this.reset() + const { identifier: id } = user; + this.loading = !this.rowLoading && true; const resp = await fetch(`${this.api.managing}?memberid=${id}`, { headers: { "Accept": "application/json", @@ -35,31 +69,35 @@ export default { method: "GET" }); if (resp.ok) { - this.result = await resp.json(); - console.log('result', this.result) - generateFlash(`${id} has ${this.result.length} ${this.txt.memberships}`, 'success'); + this.results = await resp.json(); + generateFlash(`${id} has ${this.results.length} ${this.txt.memberships}`, 'success'); } else { let errorResponse = await resp.json(); generateFlash(`${errorResponse.message}`, 'error'); } + this.searched = true; this.loading = false; } }, template: /*html*/` - ` diff --git a/webroot/js/pagecount.js b/webroot/js/pagecount.js index 1b41421..5f9c83e 100644 --- a/webroot/js/pagecount.js +++ b/webroot/js/pagecount.js @@ -11,12 +11,16 @@ export default { total: { type: Number, default: 100 + }, + searched: { + type: Boolean, + default: true } }, inject: ['txt'], template: /*html*/`
    -
    +