Skip to content

Commit a0e7e2c

Browse files
authored
Merge branch 'DSpace:main' into patch-21-squashed
2 parents 96748b9 + f7a14a1 commit a0e7e2c

207 files changed

Lines changed: 6231 additions & 11911 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cypress/e2e/item-edit.cy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ describe('Edit Item > Edit Metadata tab', () => {
1818
// <ds-edit-item-page> tag must be loaded
1919
cy.get('ds-edit-item-page').should('be.visible');
2020

21+
// wait for all the ds-dso-edit-metadata-value components to be rendered
22+
cy.get('ds-dso-edit-metadata-value div[role="row"]').each(($row: HTMLDivElement) => {
23+
cy.wrap($row).find('div[role="cell"]').should('be.visible');
24+
});
25+
2126
// Analyze <ds-edit-item-page> for accessibility issues
2227
testA11y('ds-edit-item-page');
2328
});

cypress/e2e/submission.cy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ describe('New Submission page', () => {
137137

138138
// Upload our DSpace logo via drag & drop onto submission form
139139
// cy.get('div#section_upload')
140-
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.png', {
140+
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.svg', {
141141
action: 'drag-drop',
142142
});
143143

src/app/access-control/access-control-routes.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import { AbstractControl } from '@angular/forms';
2-
import {
3-
mapToCanActivate,
4-
Route,
5-
} from '@angular/router';
2+
import { Route } from '@angular/router';
63
import {
74
DYNAMIC_ERROR_MESSAGES_MATCHER,
85
DynamicErrorMessagesMatcher,
96
} from '@ng-dynamic-forms/core';
107

118
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
12-
import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
13-
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
9+
import { groupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
10+
import { siteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
1411
import {
1512
EPERSON_PATH,
1613
GROUP_PATH,
@@ -20,15 +17,15 @@ import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.co
2017
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
2118
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
2219
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
23-
import { GroupPageGuard } from './group-registry/group-page.guard';
20+
import { groupPageGuard } from './group-registry/group-page.guard';
2421
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
2522

2623
/**
2724
* Condition for displaying error messages on email form field
2825
*/
2926
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
3027
(control: AbstractControl, model: any, hasFocus: boolean) => {
31-
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
28+
return ( control.touched && !hasFocus ) || ( control.errors?.emailTaken && hasFocus );
3229
};
3330

3431
const providers = [
@@ -46,7 +43,7 @@ export const ROUTES: Route[] = [
4643
},
4744
providers,
4845
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
49-
canActivate: mapToCanActivate([SiteAdministratorGuard]),
46+
canActivate: [siteAdministratorGuard],
5047
},
5148
{
5249
path: `${EPERSON_PATH}/create`,
@@ -56,7 +53,7 @@ export const ROUTES: Route[] = [
5653
},
5754
providers,
5855
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
59-
canActivate: mapToCanActivate([SiteAdministratorGuard]),
56+
canActivate: [siteAdministratorGuard],
6057
},
6158
{
6259
path: `${EPERSON_PATH}/:id/edit`,
@@ -67,7 +64,7 @@ export const ROUTES: Route[] = [
6764
},
6865
providers,
6966
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
70-
canActivate: mapToCanActivate([SiteAdministratorGuard]),
67+
canActivate: [siteAdministratorGuard],
7168
},
7269
{
7370
path: GROUP_PATH,
@@ -77,7 +74,7 @@ export const ROUTES: Route[] = [
7774
},
7875
providers,
7976
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
80-
canActivate: mapToCanActivate([GroupAdministratorGuard]),
77+
canActivate: [groupAdministratorGuard],
8178
},
8279
{
8380
path: `${GROUP_PATH}/create`,
@@ -90,7 +87,7 @@ export const ROUTES: Route[] = [
9087
title: 'admin.access-control.groups.title.addGroup',
9188
breadcrumbKey: 'admin.access-control.groups.addGroup',
9289
},
93-
canActivate: mapToCanActivate([GroupAdministratorGuard]),
90+
canActivate: [groupAdministratorGuard],
9491
},
9592
{
9693
path: `${GROUP_PATH}/:groupId/edit`,
@@ -103,7 +100,7 @@ export const ROUTES: Route[] = [
103100
title: 'admin.access-control.groups.title.singleGroup',
104101
breadcrumbKey: 'admin.access-control.groups.singleGroup',
105102
},
106-
canActivate: mapToCanActivate([GroupPageGuard]),
103+
canActivate: [groupPageGuard],
107104
},
108105
{
109106
path: 'bulk-access',
@@ -112,6 +109,6 @@ export const ROUTES: Route[] = [
112109
breadcrumb: i18nBreadcrumbResolver,
113110
},
114111
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
115-
canActivate: mapToCanActivate([SiteAdministratorGuard]),
112+
canActivate: [siteAdministratorGuard],
116113
},
117114
];

src/app/access-control/group-registry/group-form/members-list/members-list.component.html

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,33 @@ <h3>{{messagePrefix + '.headMembers' | translate}}</h3>
2020
</tr>
2121
</thead>
2222
<tbody>
23-
<tr *ngFor="let eperson of (ePeopleMembersOfGroup | async)?.page">
24-
<td class="align-middle">{{eperson.id}}</td>
23+
<tr *ngFor="let epersonDTO of (ePeopleMembersOfGroup | async)?.page">
24+
<td class="align-middle">{{epersonDTO.eperson.id}}</td>
2525
<td class="align-middle">
26-
<a [routerLink]="getEPersonEditRoute(eperson.id)">
27-
{{ dsoNameService.getName(eperson) }}
26+
<a [routerLink]="getEPersonEditRoute(epersonDTO.eperson.id)">
27+
{{ dsoNameService.getName(epersonDTO.eperson) }}
2828
</a>
2929
</td>
3030
<td class="align-middle">
31-
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
32-
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
31+
{{messagePrefix + '.table.email' | translate}}: {{ epersonDTO.eperson.email ? epersonDTO.eperson.email : '-' }}<br/>
32+
{{messagePrefix + '.table.netid' | translate}}: {{ epersonDTO.eperson.netid ? epersonDTO.eperson.netid : '-' }}
3333
</td>
3434
<td class="align-middle">
3535
<div class="btn-group edit-field">
36-
<button (click)="deleteMemberFromGroup(eperson)"
36+
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
37+
*ngIf="epersonDTO.ableToDelete"
3738
[disabled]="actionConfig.remove.disabled"
3839
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
39-
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(eperson) } }}">
40+
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
4041
<i [ngClass]="actionConfig.remove.icon"></i>
4142
</button>
43+
<button *ngIf="!epersonDTO.ableToDelete"
44+
(click)="addMemberToGroup(epersonDTO.eperson)"
45+
[disabled]="actionConfig.add.disabled"
46+
[ngClass]="['btn btn-sm', actionConfig.add.css]"
47+
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
48+
<i [ngClass]="actionConfig.add.icon"></i>
49+
</button>
4250
</div>
4351
</td>
4452
</tr>

src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,13 +222,13 @@ describe('MembersListComponent', () => {
222222

223223
describe('if first delete button is pressed', () => {
224224
beforeEach(() => {
225+
spyOn(component, 'search').and.callThrough();
225226
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#ePeopleMembersOfGroup tbody .fa-trash-alt'));
226227
deleteButton.nativeElement.click();
227228
fixture.detectChanges();
228229
});
229-
it('then no ePerson remains as a member of the active group.', () => {
230-
const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr'));
231-
expect(epersonsFound.length).toEqual(0);
230+
it('should trigger the search to add the user back to the search table', () => {
231+
expect(component.search).toHaveBeenCalled();
232232
});
233233
});
234234
});
@@ -264,13 +264,13 @@ describe('MembersListComponent', () => {
264264

265265
describe('if first add button is pressed', () => {
266266
beforeEach(() => {
267+
spyOn(component, 'search').and.callThrough();
267268
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
268269
addButton.nativeElement.click();
269270
fixture.detectChanges();
270271
});
271-
it('then all (two) ePersons are member of the active group. No non-members left', () => {
272-
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
273-
expect(epersonsFound.length).toEqual(0);
272+
it('should trigger the search to remove the user from the search table', () => {
273+
expect(component.search).toHaveBeenCalled();
274274
});
275275
});
276276
});

src/app/access-control/group-registry/group-form/members-list/members-list.component.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,29 @@ import {
2424
} from '@ngx-translate/core';
2525
import {
2626
BehaviorSubject,
27+
combineLatest as observableCombineLatest,
2728
Observable,
29+
ObservedValueOf,
30+
of as observableOf,
2831
Subscription,
2932
} from 'rxjs';
3033
import {
34+
defaultIfEmpty,
3135
map,
3236
switchMap,
3337
take,
3438
} from 'rxjs/operators';
3539

3640
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
37-
import { PaginatedList } from '../../../../core/data/paginated-list.model';
41+
import {
42+
buildPaginatedList,
43+
PaginatedList,
44+
} from '../../../../core/data/paginated-list.model';
3845
import { RemoteData } from '../../../../core/data/remote-data';
3946
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
4047
import { GroupDataService } from '../../../../core/eperson/group-data.service';
4148
import { EPerson } from '../../../../core/eperson/models/eperson.model';
49+
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
4250
import { Group } from '../../../../core/eperson/models/group.model';
4351
import { PaginationService } from '../../../../core/pagination/pagination.service';
4452
import {
@@ -137,7 +145,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
137145
/**
138146
* List of EPeople members of currently active group being edited
139147
*/
140-
ePeopleMembersOfGroup: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject<PaginatedList<EPerson>>(undefined);
148+
ePeopleMembersOfGroup: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject(undefined);
141149

142150
/**
143151
* Pagination config used to display the list of EPeople that are result of EPeople search
@@ -226,10 +234,35 @@ export class MembersListComponent implements OnInit, OnDestroy {
226234
return rd;
227235
}
228236
}),
229-
getRemoteDataPayload())
230-
.subscribe((paginatedListOfEPersons: PaginatedList<EPerson>) => {
231-
this.ePeopleMembersOfGroup.next(paginatedListOfEPersons);
232-
}));
237+
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
238+
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
239+
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
240+
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
241+
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
242+
epersonDtoModel.eperson = member;
243+
epersonDtoModel.ableToDelete = isMember;
244+
return epersonDtoModel;
245+
});
246+
return dto$;
247+
})]);
248+
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
249+
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
250+
}));
251+
}),
252+
).subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
253+
this.ePeopleMembersOfGroup.next(paginatedListOfDTOs);
254+
}),
255+
);
256+
}
257+
258+
/**
259+
* We always return true since this is only used by the top section (which represents all the users part of the group
260+
* in {@link MembersListComponent})
261+
*
262+
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
263+
*/
264+
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
265+
return observableOf(true);
233266
}
234267

235268
/**

src/app/access-control/group-registry/group-page.guard.spec.ts

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1+
import {
2+
TestBed,
3+
waitForAsync,
4+
} from '@angular/core/testing';
15
import {
26
ActivatedRouteSnapshot,
37
Router,
8+
UrlTree,
49
} from '@angular/router';
5-
import { of as observableOf } from 'rxjs';
10+
import {
11+
Observable,
12+
of as observableOf,
13+
} from 'rxjs';
614

715
import { AuthService } from '../../core/auth/auth.service';
816
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
917
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
1018
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
11-
import { GroupPageGuard } from './group-page.guard';
19+
import { groupPageGuard } from './group-page.guard';
20+
21+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; // Increase timeout to 10 seconds
1222

1323
describe('GroupPageGuard', () => {
1424
const groupsEndpointUrl = 'https://test.org/api/eperson/groups';
@@ -20,42 +30,54 @@ describe('GroupPageGuard', () => {
2030
},
2131
} as unknown as ActivatedRouteSnapshot;
2232

23-
let guard: GroupPageGuard;
2433
let halEndpointService: HALEndpointService;
2534
let authorizationService: AuthorizationDataService;
2635
let router: Router;
2736
let authService: AuthService;
2837

29-
beforeEach(() => {
38+
function init() {
3039
halEndpointService = jasmine.createSpyObj(['getEndpoint']);
31-
(halEndpointService as any).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl));
40+
( halEndpointService as any ).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl));
3241

3342
authorizationService = jasmine.createSpyObj(['isAuthorized']);
3443
// NOTE: value is set in beforeEach
3544

3645
router = jasmine.createSpyObj(['parseUrl']);
37-
(router as any).parseUrl.and.returnValue = {};
46+
( router as any ).parseUrl.and.returnValue = {};
3847

3948
authService = jasmine.createSpyObj(['isAuthenticated']);
40-
(authService as any).isAuthenticated.and.returnValue(observableOf(true));
49+
( authService as any ).isAuthenticated.and.returnValue(observableOf(true));
4150

42-
guard = new GroupPageGuard(halEndpointService, authorizationService, router, authService);
43-
});
51+
TestBed.configureTestingModule({
52+
providers: [
53+
{ provide: AuthorizationDataService, useValue: authorizationService },
54+
{ provide: Router, useValue: router },
55+
{ provide: AuthService, useValue: authService },
56+
{ provide: HALEndpointService, useValue: halEndpointService },
57+
],
58+
});
59+
}
60+
61+
beforeEach(waitForAsync(() => {
62+
init();
63+
}));
4464

4565
it('should be created', () => {
46-
expect(guard).toBeTruthy();
66+
expect(groupPageGuard).toBeTruthy();
4767
});
4868

4969
describe('canActivate', () => {
5070
describe('when the current user can manage the group', () => {
5171
beforeEach(() => {
52-
(authorizationService as any).isAuthorized.and.returnValue(observableOf(true));
72+
( authorizationService as any ).isAuthorized.and.returnValue(observableOf(true));
5373
});
5474

5575
it('should return true', (done) => {
56-
guard.canActivate(
57-
routeSnapshotWithGroupId, { url: 'current-url' } as any,
58-
).subscribe((result) => {
76+
const result$ = TestBed.runInInjectionContext(() => {
77+
return groupPageGuard()(routeSnapshotWithGroupId, { url: 'current-url' } as any);
78+
}) as Observable<boolean | UrlTree>;
79+
80+
result$.subscribe((result) => {
5981
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
6082
FeatureID.CanManageGroup, groupEndpointUrl, undefined,
6183
);
@@ -71,15 +93,18 @@ describe('GroupPageGuard', () => {
7193
});
7294

7395
it('should not return true', (done) => {
74-
guard.canActivate(
75-
routeSnapshotWithGroupId, { url: 'current-url' } as any,
76-
).subscribe((result) => {
96+
const result$ = TestBed.runInInjectionContext(() => {
97+
return groupPageGuard()(routeSnapshotWithGroupId, { url: 'current-url' } as any);
98+
}) as Observable<boolean | UrlTree>;
99+
100+
result$.subscribe((result) => {
77101
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
78102
FeatureID.CanManageGroup, groupEndpointUrl, undefined,
79103
);
80104
expect(result).not.toBeTrue();
81105
done();
82106
});
107+
83108
});
84109
});
85110
});

0 commit comments

Comments
 (0)