Skip to content

Commit 3b0577b

Browse files
authored
Merge branch 'main' into main
2 parents b965025 + b26da89 commit 3b0577b

349 files changed

Lines changed: 13858 additions & 5540 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.

config/config.example.yml

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ languages:
165165
- code: bn
166166
label: বাংলা
167167
active: true
168+
- code: el
169+
label: Ελληνικά
170+
active: true
168171

169172
# Browse-By Pages
170173
browseBy:
@@ -174,6 +177,27 @@ browseBy:
174177
fiveYearLimit: 30
175178
# The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
176179
defaultLowerLimit: 1900
180+
# If true, thumbnail images for items will be added to BOTH search and browse result lists.
181+
showThumbnails: true
182+
# The number of entries in a paginated browse results list.
183+
# Rounded to the nearest size in the list of selectable sizes on the
184+
# settings menu.
185+
pageSize: 20
186+
187+
communityList:
188+
# No. of communities to list per expansion (show more)
189+
pageSize: 20
190+
191+
homePage:
192+
recentSubmissions:
193+
# The number of item showing in recent submission components
194+
pageSize: 5
195+
# Sort record of recent submission
196+
sortField: 'dc.date.accessioned'
197+
topLevelCommunityList:
198+
# No. of communities to list per page on the home page
199+
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
200+
pageSize: 5
177201

178202
# Item Config
179203
item:
@@ -249,7 +273,7 @@ themes:
249273

250274
# The default bundles that should always be displayed as suggestions when you upload a new bundle
251275
bundle:
252-
- standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ]
276+
standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ]
253277

254278
# Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video').
255279
# For images, this enables a gallery viewer where you can zoom or page through images.
@@ -264,10 +288,3 @@ mediaViewer:
264288
info:
265289
enableEndUserAgreement: true
266290
enablePrivacyStatement: true
267-
# Home Page
268-
homePage:
269-
recentSubmissions:
270-
# The number of item showing in recent submission components
271-
pageSize: 5
272-
# Sort record of recent submission
273-
sortField: 'dc.date.accessioned'

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@
104104
"jwt-decode": "^3.1.2",
105105
"klaro": "^0.7.10",
106106
"lodash": "^4.17.21",
107+
"markdown-it": "^13.0.1",
108+
"markdown-it-mathjax3": "^4.3.1",
107109
"mirador": "^3.3.0",
108110
"mirador-dl-plugin": "^0.13.0",
109111
"mirador-share-plugin": "^0.11.0",
@@ -116,20 +118,21 @@
116118
"ngx-moment": "^5.0.0",
117119
"ngx-pagination": "5.0.0",
118120
"ngx-sortablejs": "^11.1.0",
121+
"ngx-ui-switch": "^11.0.1",
119122
"nouislider": "^14.6.3",
120123
"pem": "1.14.4",
121124
"postcss-cli": "^9.1.0",
122125
"prop-types": "^15.7.2",
123126
"react-copy-to-clipboard": "^5.0.1",
124127
"reflect-metadata": "^0.1.13",
125128
"rxjs": "^7.5.5",
129+
"sanitize-html": "^2.7.2",
126130
"sortablejs": "1.13.0",
127131
"tslib": "^2.0.0",
128132
"url-parse": "^1.5.6",
129133
"uuid": "^8.3.2",
130134
"webfontloader": "1.6.28",
131-
"zone.js": "~0.11.5",
132-
"ngx-ui-switch": "^11.0.1"
135+
"zone.js": "~0.11.5"
133136
},
134137
"devDependencies": {
135138
"@angular-builders/custom-webpack": "~13.1.0",
@@ -155,6 +158,7 @@
155158
"@types/js-cookie": "2.2.6",
156159
"@types/lodash": "^4.14.165",
157160
"@types/node": "^14.14.9",
161+
"@types/sanitize-html": "^2.6.2",
158162
"@typescript-eslint/eslint-plugin": "5.11.0",
159163
"@typescript-eslint/parser": "5.11.0",
160164
"axe-core": "^4.3.3",

server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ export function app() {
7676
*/
7777
const server = express();
7878

79+
// Tell Express to trust X-FORWARDED-* headers from proxies
80+
// See https://expressjs.com/en/guide/behind-proxies.html
81+
server.set('trust proxy', environment.ui.useProxies);
82+
7983
/*
8084
* If production mode is enabled in the environment file:
8185
* - Enable Angular's production mode

src/app/access-control/epeople-registry/epeople-registry.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
238238
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
239239
if (restResponse.hasSucceeded) {
240240
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name}));
241-
this.reset();
242241
} else {
243242
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
244243
}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
combineLatest as observableCombineLatest,
1111
ObservedValueOf,
1212
} from 'rxjs';
13-
import { map, mergeMap, switchMap, take } from 'rxjs/operators';
13+
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
1414
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
1515
import { RemoteData } from '../../../../core/data/remote-data';
1616
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
@@ -144,7 +144,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
144144
}
145145
}),
146146
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
147-
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
147+
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
148148
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
149149
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
150150
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
@@ -153,8 +153,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
153153
return epersonDtoModel;
154154
});
155155
return dto$;
156-
}));
157-
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
156+
})]);
157+
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
158158
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
159159
}));
160160
}))
@@ -174,7 +174,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
174174
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
175175
currentPage: 1,
176176
elementsPerPage: 9999
177-
}, false)
177+
})
178178
.pipe(
179179
getFirstSucceededRemoteData(),
180180
getRemoteDataPayload(),
@@ -274,7 +274,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
274274
}
275275
}),
276276
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
277-
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
277+
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
278278
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
279279
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
280280
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
@@ -283,8 +283,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
283283
return epersonDtoModel;
284284
});
285285
return dto$;
286-
}));
287-
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
286+
})]);
287+
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
288288
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
289289
}));
290290
}))

src/app/access-control/group-registry/groups-registry.component.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { TranslateService } from '@ngx-translate/core';
55
import {
66
BehaviorSubject,
77
combineLatest as observableCombineLatest,
8+
EMPTY,
89
Observable,
910
of as observableOf,
1011
Subscription
1112
} from 'rxjs';
12-
import { catchError, map, switchMap, tap } from 'rxjs/operators';
13+
import { catchError, defaultIfEmpty, map, switchMap, tap } from 'rxjs/operators';
1314
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
1415
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
1516
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
@@ -144,7 +145,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
144145
}
145146
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
146147
switchMap((isSiteAdmin: boolean) => {
147-
return observableCombineLatest(groups.page.map((group: Group) => {
148+
return observableCombineLatest([...groups.page.map((group: Group) => {
148149
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
149150
return observableCombineLatest([
150151
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
@@ -165,8 +166,10 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
165166
}
166167
)
167168
);
169+
} else {
170+
return EMPTY;
168171
}
169-
})).pipe(map((dtos: GroupDtoModel[]) => {
172+
})]).pipe(defaultIfEmpty([]), map((dtos: GroupDtoModel[]) => {
170173
return buildPaginatedList(groups.pageInfo, dtos);
171174
}));
172175
})
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<div class="container">
2+
<h2 id="header">{{'admin.batch-import.page.header' | translate}}</h2>
3+
<p>{{'admin.batch-import.page.help' | translate}}</p>
4+
<p *ngIf="dso">
5+
selected collection: <b>{{getDspaceObjectName()}}</b>&nbsp;
6+
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
7+
</p>
8+
<p>
9+
<button class="btn btn-primary" (click)="this.selectCollection();">{{'admin.metadata-import.page.button.select-collection' | translate}}</button>
10+
</p>
11+
<div class="form-group">
12+
<div class="form-check">
13+
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
14+
<label class="form-check-label" for="validateOnly">
15+
{{'admin.metadata-import.page.validateOnly' | translate}}
16+
</label>
17+
</div>
18+
<small id="validateOnlyHelpBlock" class="form-text text-muted">
19+
{{'admin.batch-import.page.validateOnly.hint' | translate}}
20+
</small>
21+
</div>
22+
23+
<ds-file-dropzone-no-uploader
24+
(onFileAdded)="setFile($event)"
25+
[dropMessageLabel]="'admin.batch-import.page.dropMsg'"
26+
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
27+
</ds-file-dropzone-no-uploader>
28+
29+
<div class="space-children-mr">
30+
<button class="btn btn-secondary" id="backButton"
31+
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
32+
<button class="btn btn-primary" id="proceedButton"
33+
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
34+
</div>
35+
</div>
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
2+
import { BatchImportPageComponent } from './batch-import-page.component';
3+
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
4+
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
5+
import { FormsModule } from '@angular/forms';
6+
import { TranslateModule } from '@ngx-translate/core';
7+
import { RouterTestingModule } from '@angular/router/testing';
8+
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
9+
import { FileValidator } from '../../shared/utils/require-file.validator';
10+
import { NotificationsService } from '../../shared/notifications/notifications.service';
11+
import {
12+
BATCH_IMPORT_SCRIPT_NAME,
13+
ScriptDataService
14+
} from '../../core/data/processes/script-data.service';
15+
import { Router } from '@angular/router';
16+
import { Location } from '@angular/common';
17+
import { NO_ERRORS_SCHEMA } from '@angular/core';
18+
import { By } from '@angular/platform-browser';
19+
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
20+
21+
describe('BatchImportPageComponent', () => {
22+
let component: BatchImportPageComponent;
23+
let fixture: ComponentFixture<BatchImportPageComponent>;
24+
25+
let notificationService: NotificationsServiceStub;
26+
let scriptService: any;
27+
let router;
28+
let locationStub;
29+
30+
function init() {
31+
notificationService = new NotificationsServiceStub();
32+
scriptService = jasmine.createSpyObj('scriptService',
33+
{
34+
invoke: createSuccessfulRemoteDataObject$({ processId: '46' })
35+
}
36+
);
37+
router = jasmine.createSpyObj('router', {
38+
navigateByUrl: jasmine.createSpy('navigateByUrl')
39+
});
40+
locationStub = jasmine.createSpyObj('location', {
41+
back: jasmine.createSpy('back')
42+
});
43+
}
44+
45+
beforeEach(waitForAsync(() => {
46+
init();
47+
TestBed.configureTestingModule({
48+
imports: [
49+
FormsModule,
50+
TranslateModule.forRoot(),
51+
RouterTestingModule.withRoutes([])
52+
],
53+
declarations: [BatchImportPageComponent, FileValueAccessorDirective, FileValidator],
54+
providers: [
55+
{ provide: NotificationsService, useValue: notificationService },
56+
{ provide: ScriptDataService, useValue: scriptService },
57+
{ provide: Router, useValue: router },
58+
{ provide: Location, useValue: locationStub },
59+
],
60+
schemas: [NO_ERRORS_SCHEMA]
61+
}).compileComponents();
62+
}));
63+
64+
beforeEach(() => {
65+
fixture = TestBed.createComponent(BatchImportPageComponent);
66+
component = fixture.componentInstance;
67+
fixture.detectChanges();
68+
});
69+
70+
it('should create', () => {
71+
expect(component).toBeTruthy();
72+
});
73+
74+
describe('if back button is pressed', () => {
75+
beforeEach(fakeAsync(() => {
76+
const proceed = fixture.debugElement.query(By.css('#backButton')).nativeElement;
77+
proceed.click();
78+
fixture.detectChanges();
79+
}));
80+
it('should do location.back', () => {
81+
expect(locationStub.back).toHaveBeenCalled();
82+
});
83+
});
84+
85+
describe('if file is set', () => {
86+
let fileMock: File;
87+
88+
beforeEach(() => {
89+
fileMock = new File([''], 'filename.zip', { type: 'application/zip' });
90+
component.setFile(fileMock);
91+
});
92+
93+
describe('if proceed button is pressed without validate only', () => {
94+
beforeEach(fakeAsync(() => {
95+
component.validateOnly = false;
96+
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
97+
proceed.click();
98+
fixture.detectChanges();
99+
}));
100+
it('metadata-import script is invoked with --zip fileName and the mockFile', () => {
101+
const parameterValues: ProcessParameter[] = [
102+
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
103+
];
104+
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' }));
105+
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
106+
});
107+
it('success notification is shown', () => {
108+
expect(notificationService.success).toHaveBeenCalled();
109+
});
110+
it('redirected to process page', () => {
111+
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
112+
});
113+
});
114+
115+
describe('if proceed button is pressed with validate only', () => {
116+
beforeEach(fakeAsync(() => {
117+
component.validateOnly = true;
118+
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
119+
proceed.click();
120+
fixture.detectChanges();
121+
}));
122+
it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => {
123+
const parameterValues: ProcessParameter[] = [
124+
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
125+
Object.assign(new ProcessParameter(), { name: '--add' }),
126+
Object.assign(new ProcessParameter(), { name: '-v', value: true }),
127+
];
128+
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
129+
});
130+
it('success notification is shown', () => {
131+
expect(notificationService.success).toHaveBeenCalled();
132+
});
133+
it('redirected to process page', () => {
134+
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
135+
});
136+
});
137+
138+
describe('if proceed is pressed; but script invoke fails', () => {
139+
beforeEach(fakeAsync(() => {
140+
jasmine.getEnv().allowRespy(true);
141+
spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500));
142+
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
143+
proceed.click();
144+
fixture.detectChanges();
145+
}));
146+
it('error notification is shown', () => {
147+
expect(notificationService.error).toHaveBeenCalled();
148+
});
149+
});
150+
});
151+
});

0 commit comments

Comments
 (0)