LP1904036 Patron editor field invalidate; secondary grps
authorBill Erickson <berickxx@gmail.com>
Fri, 19 Mar 2021 16:41:11 +0000 (12:41 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:27 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/group.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.ts [new file with mode: 0644]

index 6481774..8e562b7 100644 (file)
@@ -1,3 +1,18 @@
+<eg-string key="circ.patron.edit.test_notify.success" 
+  i18n-text text="Test Notification Sent"></eg-string>
+<eg-string key="circ.patron.edit.test_notify.fail" 
+  i18n-text text="Test Notification Failed to Send"></eg-string>
+<eg-string key="circ.patron.edit.invalidate.success" 
+  i18n-text text="Field Invalidation Succeeded"></eg-string>
+<eg-string key="circ.patron.edit.invalidate.fail" 
+  i18n-text text="Failed to Invalidate Field"></eg-string>
+<eg-string key="circ.patron.edit.grplink.success" 
+  i18n-text text="Group Link Succeeded"></eg-string>
+<eg-string key="circ.patron.edit.grplink.fail" 
+  i18n-text text="Group Link Failed"></eg-string>
+
+<eg-patron-secondary-groups [secondaryGroups]="secondaryGroups" #secondaryGroupsDialog>
+</eg-patron-secondary-groups>
 
 <div class="row" *ngIf="loading">
   <div class="col-lg-6 offset-lg-3">
@@ -25,7 +40,7 @@
       id="{{getClass(args.cls)}}-{{args.field}}-input"
       [ngModel]="objectFromPath(args.path)[args.field]()"
       (ngModelChange)="fieldValueChange(args.path, args.field, $event)"
-      (change)="postFieldChange(args.path, args.field)"
+      (change)="afterFieldChange(args.path, args.field)"
       [required]="fieldRequired(getClass(args.cls), args.field)"
       [pattern]="fieldPattern(getClass(args.cls), args.field)"
       [disabled]="args.disabled"
@@ -43,7 +58,7 @@
       id="{{getClass(args.cls)}}-{{args.field}}-input"
       [ngModel]="objectFromPath(args.path)[args.field]() == 't'"
       (ngModelChange)="fieldValueChange(args.path, args.field, $event)"
-      (change)="postFieldChange(args.path, args.field)"
+      (change)="afterFieldChange(args.path, args.field)"
       [required]="fieldRequired(getClass(args.cls), args.field)"
       [pattern]="fieldPattern(getClass(args.cls), args.field)"
       [disabled]="disabled"
@@ -60,7 +75,7 @@
       [startId]="getFieldValue(args.path, args.field)"
       (onChange)="
         fieldValueChange(args.path, args.field, $event ? $event.id : null); 
-        postFieldChange(args.path, args.field)"
+        afterFieldChange(args.path, args.field)"
       [required]="fieldRequired(getClass(args.cls), args.field)"
       [disabled]="args.disabled">
     </eg-combobox>
         id="au-name_keywords-input"
         [ngModel]="objectFromPath(null)['name_keywords']()"
         (ngModelChange)="fieldValueChange(null, 'name_keywords', $event)"
-        (change)="postFieldChange(null, 'name_keywords')"
+        (change)="afterFieldChange(null, 'name_keywords')"
         [required]="fieldRequired('au', 'name_keywords')"
         [pattern]="fieldPattern('au', 'name_keywords')">
       </textarea>
       <eg-date-select
         domId="au-dob-input"
         fieldName="au-dob-input"
+        [noMaxWidth]="true"
         [initialIso]="patron.dob()"
         (onChangeAsIso)="
           fieldValueChange(null, 'dob', $event); 
-          postFieldChange(null, 'dob')"
+          afterFieldChange(null, 'dob')"
         [required]="fieldRequired('au', 'dob')">
       </eg-date-select>
     </div>
   <ng-container *ngTemplateOutlet="fieldRow; context: 
     {args: {template: fieldInput, field: 'ident_value2'}}">
   </ng-container>
-  <ng-container *ngTemplateOutlet="fieldRow; context: 
-    {args: {template: fieldInput, field: 'email', type: 'email'}}">
-  </ng-container>
+
+
+  <div class="row pt-1 pb-1 mt-1">
+    <ng-container 
+      *ngTemplateOutlet="fieldLabel; context: {args: {field: 'email'}}">
+    </ng-container>
+    <ng-container 
+      *ngTemplateOutlet="fieldInput; context: {args: {field: 'email', type: 'email'}}">
+    </ng-container>
+    <div class="col-lg-6">
+      <button class="btn btn-outline-dark" 
+        (click)="sendTestMessage('au.email.test')" i18n>
+        Send Test Email
+      </button>
+      <button class="btn btn-outline-dark ml-2" 
+        (click)="invalidateField('email')" i18n>Invalidate</button>
+    </div>
+  </div>
+
   <ng-container 
     *ngIf="userSettingTypes['circ.send_email_checkout_receipts'] && showField('au', 'email')">
     <ng-container *ngTemplateOutlet="userSettingsCheckboxRow; context: 
       {args: {settingName: 'circ.send_email_checkout_receipts'}}">
     </ng-container>
   </ng-container>
-  <ng-container *ngTemplateOutlet="fieldRow; context: 
-    {args: {template: fieldInput, field: 'day_phone'}}">
-  </ng-container>
+
+  <div class="row pt-1 pb-1 mt-1">
+    <ng-container 
+      *ngTemplateOutlet="fieldLabel; context: {args: {field: 'day_phone'}}">
+    </ng-container>
+    <ng-container 
+      *ngTemplateOutlet="fieldInput; context: {args: {field: 'day_phone'}}">
+    </ng-container>
+    <div class="col-lg-6">
+      <button class="btn btn-outline-dark" 
+        (click)="invalidateField('day_phone')" i18n>Invalidate</button>
+    </div>
+  </div>
+
   <ng-container *ngTemplateOutlet="fieldRow; context: 
     {args: {template: fieldInput, field: 'evening_phone'}}">
   </ng-container>
   <ng-container *ngTemplateOutlet="fieldRow; context: 
     {args: {template: fieldInput, field: 'other_phone'}}">
   </ng-container>
+
   <div class="row pt-1 pb-1 mt-1" *ngIf="showField('au', 'home_ou')">
     <ng-container 
       *ngTemplateOutlet="fieldLabel; context: {args: {field: 'home_ou'}}">
         [disableOrgs]="cannotHaveUsersOrgs()"
         (onChange)="
           fieldValueChange(null, 'home_ou', $event ? $event.id() : null); 
-          postFieldChange(null, 'home_ou')">
+          afterFieldChange(null, 'home_ou')">
       </eg-org-select>
     </div>
   </div>
+
   <div class="row pt-1 pb-1 mt-1" *ngIf="showField('au', 'profile')">
     <ng-container 
       *ngTemplateOutlet="fieldLabel; context: {args: {field: 'profile'}}">
         [initialGroupId]="patron.profile()" 
         (profileChange)="
           fieldValueChange(null, 'profile', $event ? $event.id() : null); 
-          postFieldChange(null, 'profile')">
+          afterFieldChange(null, 'profile')">
       </eg-profile-select>
     </div>
+    <div class="col-lg-6">
+      <button class="btn btn-outline-dark" 
+        *ngIf="hasPerm.CREATE_USER_GROUP_LINK"
+        (click)="openGroupsDialog()" i18n>Secondary Groups</button>
+    </div>
   </div>
+
   <div class="row pt-1 pb-1 mt-1" *ngIf="showField('au', 'expire_date')">
     <ng-container 
       *ngTemplateOutlet="fieldLabel; context: {args: {field: 'expire_date'}}">
       <eg-date-select
         domId="au-expire_date-input"
         fieldName="au-expire_date-input"
+        [noMaxWidth]="true"
         [required]="fieldRequired('au', 'expire_date')"
         [(ngModel)]="expireDate">
       </eg-date-select>
     </div>
-    <div class="col-lg-3">
+    <div class="col-lg-6">
       <button class="btn btn-outline-dark" (click)="setExpireDate()" i18n>
         Update Expire Date
       </button>
index 884ed01..c758924 100644 (file)
@@ -1,5 +1,6 @@
 import {Component, OnInit, Input, ViewChild} from '@angular/core';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {concatMap, tap} from 'rxjs/operators';
 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 import {OrgService} from '@eg/core/org.service';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
@@ -11,6 +12,11 @@ import {PatronContextService} from './patron.service';
 import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {DateUtil} from '@eg/share/util/date';
 import {ProfileSelectComponent} from '@eg/staff/share/patron/profile-select.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringService} from '@eg/share/string/string.service';
+import {EventService} from '@eg/core/event.service';
+import {PermService} from '@eg/core/perm.service';
+import {SecondaryGroupsDialogComponent} from './secondary-groups.component';
 
 const COMMON_USER_SETTING_TYPES = [
   'circ.holds_behind_desk',
@@ -22,6 +28,18 @@ const COMMON_USER_SETTING_TYPES = [
   'opac.default_sms_notify'
 ];
 
+const PERMS_NEEDED = [
+    'EDIT_SELF_IN_CLIENT',
+    'UPDATE_USER',
+    'CREATE_USER',
+    'CREATE_USER_GROUP_LINK',
+    'UPDATE_PATRON_COLLECTIONS_EXEMPT',
+    'UPDATE_PATRON_CLAIM_RETURN_COUNT',
+    'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
+    'UPDATE_PATRON_ACTIVE_CARD',
+    'UPDATE_PATRON_PRIMARY_CARD'
+];
+
 const FLESH_PATRON_FIELDS = {
   flesh: 1,
   flesh_fields: {
@@ -40,7 +58,10 @@ export class EditComponent implements OnInit {
     @Input() cloneId: number;
     @Input() stageUsername: string;
 
-    @ViewChild('profileSelect') private profileSelect: ProfileSelectComponent;
+    @ViewChild('profileSelect')
+        private profileSelect: ProfileSelectComponent;
+    @ViewChild('secondaryGroupsDialog')
+        private secondaryGroupsDialog: SecondaryGroupsDialogComponent;
 
     patron: IdlObject;
     changeHandlerNeeded = false;
@@ -48,11 +69,18 @@ export class EditComponent implements OnInit {
     loading = false;
 
     identTypes: ComboboxEntry[];
-    profileGroups: ComboboxEntry[];
     userSettings: {[name: string]: any} = {};
     userSettingTypes: {[name: string]: IdlObject} = {};
     optInSettingTypes: {[name: string]: IdlObject} = {};
     expireDate: Date;
+    secondaryGroups: IdlObject[];
+
+    // All locations we have the specified permissions
+    permOrgs: {[name: string]: number[]};
+
+    // True if a given perm is grnated at the current home_ou of the
+    // patron we are editing.
+    hasPerm: {[name: string]: boolean} = {};
 
     constructor(
         private org: OrgService,
@@ -60,7 +88,11 @@ export class EditComponent implements OnInit {
         private auth: AuthService,
         private pcrud: PcrudService,
         private idl: IdlService,
-        public patronService: PatronService,
+        private strings: StringService,
+        private toast: ToastService,
+        private perms: PermService,
+        private evt: EventService,
+        private patronService: PatronService,
         public context: PatronContextService
     ) {}
 
@@ -71,11 +103,26 @@ export class EditComponent implements OnInit {
     load(): Promise<any> {
         this.loading = true;
         return this.loadPatron()
+        .then(_ => this.getSecondaryGroups())
+        .then(_ => this.applyPerms())
         .then(_ => this.setIdentTypes())
         .then(_ => this.setOptInSettings())
         .finally(() => this.loading = false);
     }
 
+    getSecondaryGroups(): Promise<any> {
+
+          return this.net.request(
+              'open-ils.actor',
+              'open-ils.actor.user.get_groups',
+              this.auth.token(), this.patronId
+
+          ).pipe(concatMap(maps => this.pcrud.search('pgt',
+              {id: maps.map(m => m.grp())}, {}, {atomic: true})
+
+          )).pipe(tap(grps => this.secondaryGroups = grps)).toPromise();
+    }
+
     setIdentTypes(): Promise<any> {
         return this.patronService.getIdentTypes()
         .then(types => {
@@ -83,6 +130,21 @@ export class EditComponent implements OnInit {
         });
     }
 
+    applyPerms(): Promise<any> {
+
+        const promise = this.permOrgs ?
+            Promise.resolve(this.permOrgs) :
+            this.perms.hasWorkPermAt(PERMS_NEEDED, true);
+
+        return promise.then(permOrgs => {
+            this.permOrgs = permOrgs;
+            Object.keys(permOrgs).forEach(perm =>
+                this.hasPerm[perm] =
+                  permOrgs[perm].includes(this.patron.home_ou())
+            );
+        });
+    }
+
     setOptInSettings(): Promise<any> {
 
         const orgIds = this.org.ancestors(this.auth.user().ws_ou(), true);
@@ -140,6 +202,8 @@ export class EditComponent implements OnInit {
     createNewPatron() {
         const patron = this.idl.create('au');
         patron.isnew(true);
+        patron.addresses([]);
+        patron.settings([]);
 
         const card = this.idl.create('ac');
         card.isnew(true);
@@ -175,7 +239,7 @@ export class EditComponent implements OnInit {
 
     // Called as the model changes.
     // This may be called many times before the final value is applied,
-    // so avoid any heavy lifting here.  See postFieldChange();
+    // so avoid any heavy lifting here.  See afterFieldChange();
     fieldValueChange(path: string, field: string, value: any) {
         if (typeof value === 'boolean') { value = value ? 't' : 'f'; }
         this.changeHandlerNeeded = true;
@@ -183,7 +247,7 @@ export class EditComponent implements OnInit {
     }
 
     // Called after a change operation has completed (e.g. on blur)
-    postFieldChange(path: string, field: string) {
+    afterFieldChange(path: string, field: string) {
         if (!this.changeHandlerNeeded) { return; } // no changes applied
         this.changeHandlerNeeded = false;
 
@@ -222,11 +286,11 @@ export class EditComponent implements OnInit {
 
     generatePassword() {
         this.fieldValueChange(null,
-          'passwd', Math.floor(Math.random()*9000) + 1000);
+          'passwd', Math.floor(Math.random() * 9000) + 1000);
 
         // Normally this is called on (blur), but the input is not
         // focused when using the generate button.
-        this.postFieldChange(null, 'passwd');
+        this.afterFieldChange(null, 'passwd');
     }
 
 
@@ -245,7 +309,110 @@ export class EditComponent implements OnInit {
         const newDate = new Date(nowEpoch + (seconds * 1000 /* millis */));
         this.expireDate = newDate;
         this.fieldValueChange(null, 'profile', newDate.toISOString());
-        this.postFieldChange(null, 'profile');
+        this.afterFieldChange(null, 'profile');
+    }
+
+    handleBoolResponse(success: boolean,
+        msg: string, errMsg?: string): Promise<boolean> {
+
+        if (success) {
+            return this.strings.interpolate(msg)
+            .then(str => this.toast.success(str))
+            .then(_ => true);
+        }
+
+      console.error(errMsg);
+
+      return this.strings.interpolate(msg)
+      .then(str => this.toast.danger(str))
+      .then(_ => false);
+    }
+
+    sendTestMessage(hook: string): Promise<boolean> {
+
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.event.test_notification',
+            this.auth.token(), {hook: hook, target: this.patronId}
+        ).toPromise().then(resp => {
+
+            if (resp && resp.template_output && resp.template_output() &&
+                resp.template_output().is_error() === 'f') {
+                return this.handleBoolResponse(
+                    true, 'circ.patron.edit.test_notify.success');
+
+            } else {
+                return this.handleBoolResponse(
+                    false, 'circ.patron.edit.test_notify.fail',
+                    'Test Notification Failed ' + resp);
+            }
+        });
+    }
+
+    invalidateField(field: string): Promise<boolean> {
+
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.invalidate.' + field,
+            this.auth.token(), this.patronId, null, this.patron.home_ou()
+
+        ).toPromise().then(resp => {
+            const evt = this.evt.parse(resp);
+
+            if (evt && evt.textcode !== 'SUCCESS') {
+                return this.handleBoolResponse(false,
+                    'circ.patron.edit.invalidate.fail',
+                    'Field Invalidation Failed: ' + resp);
+            }
+
+            this.patron[field](null);
+
+            // Keep this in sync for future updates.
+            this.patron.last_xact_id(resp.payload.last_xact_id[this.patronId]);
+
+            return this.handleBoolResponse(
+              true, 'circ.patron.edit.invalidate.success');
+        });
+    }
+
+    openGroupsDialog() {
+        this.secondaryGroupsDialog.open({size: 'lg'}).subscribe(groups => {
+            if (!groups) { return; }
+
+            this.secondaryGroups = groups;
+
+            if (this.patron.isnew()) {
+                // Links will be applied after the patron is created.
+                return;
+            }
+
+            // Apply the new links to an existing user in real time
+            this.applySecondaryGroups();
+        });
+    }
+
+    applySecondaryGroups(): Promise<boolean> {
+
+        const groupIds = this.secondaryGroups.map(grp => grp.id());
+
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.set_groups',
+            this.auth.token(), this.patronId, groupIds
+        ).toPromise().then(resp => {
+
+            if (Number(resp) === 1) {
+
+                return this.handleBoolResponse(
+                    true, 'circ.patron.edit.grplink.success');
+
+            } else {
+
+                return this.handleBoolResponse(
+                    false, 'circ.patron.edit.grplink.fail',
+                    'Failed to change group links: ' + resp);
+            }
+        });
     }
 }
 
index 1cbd00a..1221995 100644 (file)
@@ -54,7 +54,7 @@ export class PatronGroupComponent implements OnInit {
 
         this.cellTextGenerator = {
             barcode: row => row.card().barcode()
-        }
+        };
 
         this.dataSource.getRows = (pager: Pager, sort: any[]) =>
             from(this.patrons.slice(pager.offset, pager.offset + pager.limit));
@@ -127,7 +127,7 @@ export class PatronGroupComponent implements OnInit {
                         this.auth.token(), user
                     );
                 })).subscribe(
-                    resp => { if (this.evt.parse(resp)) { allOk = false; } },
+                    resp2 => { if (this.evt.parse(resp2)) { allOk = false; } },
                     err => console.error(err),
                     () => { if (allOk) { this.refresh(); } }
                 );
index dc31125..e4bed3b 100644 (file)
@@ -27,6 +27,7 @@ import {PatronSurveyResponsesComponent} from './surveys.component';
 import {PatronStatCatsComponent} from './statcats.component';
 import {PatronGroupComponent} from './group.component';
 import {RegisterPatronComponent} from './register.component';
+import {SecondaryGroupsDialogComponent} from './secondary-groups.component';
 
 @NgModule({
   declarations: [
@@ -45,7 +46,8 @@ import {RegisterPatronComponent} from './register.component';
     PatronSurveyResponsesComponent,
     PatronGroupComponent,
     RegisterPatronComponent,
-    PatronStatCatsComponent
+    PatronStatCatsComponent,
+    SecondaryGroupsDialogComponent
   ],
   imports: [
     StaffCommonModule,
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.html
new file mode 100644 (file)
index 0000000..1f2e7de
--- /dev/null
@@ -0,0 +1,56 @@
+<eg-string #successMsg text="Successfully Added Group" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Add Group" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Secondary Permission Groups</h4>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <h5 i18n>
+      Assign additional permission groups to users here. 
+      This does not affect circulation policy.
+    </h5>
+
+    <div class="row mt-2">
+      <div class="col-lg-6 font-weight-bold" i18n>Group</div>
+      <div class="col-lg-6 font-weight-bold" i18n>Action</div>
+    </div>
+
+    <div class="row mt-2" *ngFor="let grp of secondaryGroups">
+      <div class="col-lg-6">{{grp.name()}}</div>
+      <div class="col-lg-6">
+        <button class="btn btn-danger" *ngIf="!grp.isdeleted()"
+          (click)="grp.isdeleted(true)" i18n>Remove</button>
+        <button class="btn btn-info" *ngIf="grp.isdeleted()"
+          (click)="grp.isdeleted(false)" i18n>Un-Delete</button>
+      </div>
+    </div>
+
+    <div class="row mt-2" *ngFor="let grp of pendingGroups">
+      <div class="col-lg-6">{{grp.name()}}</div>
+      <div class="col-lg-6">
+        <button class="btn btn-warning" 
+          (click)="removePending(grp)" i18n>Remove Pending</button>
+      </div>
+    </div>
+
+    <div class="row mt-2">
+      <div class="col-lg-6">
+        <eg-profile-select [(ngModel)]="selectedProfile"></eg-profile-select>  
+      </div>
+      <div class="col-lg-6">
+        <button class="btn btn-success" (click)="add()" i18n>Add</button>
+      </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success"
+      (click)="applyChanges()" i18n>Apply Changes</button>
+    <button type="button" class="btn btn-warning"
+      (click)="close()" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.ts
new file mode 100644 (file)
index 0000000..bac6b1a
--- /dev/null
@@ -0,0 +1,78 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable, empty} from 'rxjs';
+import {switchMap, tap} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+
+/* Add/Remove Secondary Groups */
+
+@Component({
+  selector: 'eg-patron-secondary-groups',
+  templateUrl: 'secondary-groups.component.html'
+})
+
+export class SecondaryGroupsDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() secondaryGroups: IdlObject[] = []; // pgt
+    selectedProfile: IdlObject;
+    pendingGroups: IdlObject[];
+
+    constructor(
+        private modal: NgbModal,
+        private toast: ToastService,
+        private net: NetService,
+        private idl: IdlService,
+        private evt: EventService,
+        private pcrud: PcrudService,
+        private org: OrgService,
+        private auth: AuthService) {
+        super(modal);
+    }
+
+    ngOnInit() {
+        this.onOpen$.subscribe(_ => {
+            this.pendingGroups = [];
+            this.selectedProfile = null;
+        });
+    }
+
+    add() {
+        if (this.selectedProfile) {
+            this.pendingGroups.push(this.selectedProfile);
+            this.selectedProfile = null;
+        }
+    }
+
+    removePending(grp: IdlObject) {
+        this.pendingGroups =
+            this.pendingGroups.filter(p => p.id() !== grp.id());
+    }
+
+    remove(grp: IdlObject) {
+        grp.deleted(true);
+    }
+
+    applyChanges() {
+        this.close(
+            this.pendingGroups.concat(
+                this.secondaryGroups.filter(g => !g.isdeleted()))
+        );
+
+        // Reset the flags on the group objects so there's no
+        // unintended side effects.
+        this.secondaryGroups.concat(this.pendingGroups).forEach(grp => {
+            grp.isnew(null);
+            grp.isdeleted(null);
+        });
+    }
+}
+