Skip to content

Commit 022c684

Browse files
authored
Merge branch 'main' into portuguese_pt-PT-message-keys
2 parents adf8cc7 + 5c8828f commit 022c684

140 files changed

Lines changed: 4312 additions & 737 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.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ package-lock.json
3939
/nbproject/
4040

4141
junit.xml
42+
43+
/src/mirador-viewer/config.local.js

cypress/e2e/homepage.cy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ describe('Homepage', () => {
66
cy.visit('/');
77
});
88

9-
it('should display translated title "DSpace Angular :: Home"', () => {
10-
cy.title().should('eq', 'DSpace Angular :: Home');
9+
it('should display translated title "DSpace Repository :: Home"', () => {
10+
cy.title().should('eq', 'DSpace Repository :: Home');
1111
});
1212

1313
it('should contain a news section', () => {

server.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,14 @@ import * as ejs from 'ejs';
2626
import * as compression from 'compression';
2727
import * as expressStaticGzip from 'express-static-gzip';
2828
/* eslint-enable import/no-namespace */
29-
3029
import axios from 'axios';
3130
import LRU from 'lru-cache';
3231
import isbot from 'isbot';
3332
import { createCertificate } from 'pem';
3433
import { createServer } from 'https';
3534
import { json } from 'body-parser';
3635

37-
import { existsSync, readFileSync } from 'fs';
36+
import { readFileSync } from 'fs';
3837
import { join } from 'path';
3938

4039
import { enableProdMode } from '@angular/core';
@@ -54,7 +53,7 @@ import { buildAppConfig } from './src/config/config.server';
5453
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
5554
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
5655
import { logStartupMessage } from './startup-message';
57-
import { TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
56+
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
5857

5958

6059
/*
@@ -180,6 +179,15 @@ export function app() {
180179
changeOrigin: true
181180
}));
182181

182+
/**
183+
* Proxy the linksets
184+
*/
185+
router.use('/signposting**', createProxyMiddleware({
186+
target: `${environment.rest.baseUrl}`,
187+
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
188+
changeOrigin: true
189+
}));
190+
183191
/**
184192
* Checks if the rateLimiter property is present
185193
* When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled.
@@ -366,9 +374,19 @@ function cacheCheck(req, res, next) {
366374
}
367375

368376
// If cached copy exists, return it to the user.
369-
if (cachedCopy) {
377+
if (cachedCopy && cachedCopy.page) {
378+
if (cachedCopy.headers) {
379+
Object.keys(cachedCopy.headers).forEach((header) => {
380+
if (cachedCopy.headers[header]) {
381+
if (environment.cache.serverSide.debug) {
382+
console.log(`Restore cached ${header} header`);
383+
}
384+
res.setHeader(header, cachedCopy.headers[header]);
385+
}
386+
});
387+
}
370388
res.locals.ssr = true; // mark response as SSR-generated (enables text compression)
371-
res.send(cachedCopy);
389+
res.send(cachedCopy.page);
372390

373391
// Tell Express to skip all other handlers for this path
374392
// This ensures we don't try to re-render the page since we've already returned the cached copy
@@ -444,21 +462,38 @@ function saveToCache(req, page: any) {
444462
// Avoid caching "/reload/[random]" paths (these are hard refreshes after logout)
445463
if (key.startsWith('/reload')) { return; }
446464

465+
// Retrieve response headers to save, if any
466+
const headers = retrieveHeaders(req.res);
447467
// If bot cache is enabled, save it to that cache if it doesn't exist or is expired
448468
// (NOTE: has() will return false if page is expired in cache)
449469
if (botCacheEnabled() && !botCache.has(key)) {
450-
botCache.set(key, page);
470+
botCache.set(key, { page, headers });
451471
if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in bot cache.`); }
452472
}
453473

454474
// If anonymous cache is enabled, save it to that cache if it doesn't exist or is expired
455475
if (anonymousCacheEnabled() && !anonymousCache.has(key)) {
456-
anonymousCache.set(key, page);
476+
anonymousCache.set(key, { page, headers });
457477
if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in anonymous cache.`); }
458478
}
459479
}
460480
}
461481

482+
function retrieveHeaders(response) {
483+
const headers = Object.create({});
484+
if (Array.isArray(environment.cache.serverSide.headers) && environment.cache.serverSide.headers.length > 0) {
485+
environment.cache.serverSide.headers.forEach((header) => {
486+
if (response.hasHeader(header)) {
487+
if (environment.cache.serverSide.debug) {
488+
console.log(`Save ${header} header to cache`);
489+
}
490+
headers[header] = response.getHeader(header);
491+
}
492+
});
493+
}
494+
495+
return headers;
496+
}
462497
/**
463498
* Whether a user is authenticated or not
464499
*/

src/app/access-control/access-control-routing.module.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
66
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
77
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
88
import { GroupPageGuard } from './group-registry/group-page.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';
9+
import {
10+
GroupAdministratorGuard
11+
} from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
12+
import {
13+
SiteAdministratorGuard
14+
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
15+
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
1116

1217
@NgModule({
1318
imports: [
@@ -47,7 +52,16 @@ import { SiteAdministratorGuard } from '../core/data/feature-authorization/featu
4752
},
4853
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' },
4954
canActivate: [GroupPageGuard]
50-
}
55+
},
56+
{
57+
path: 'bulk-access',
58+
component: BulkAccessComponent,
59+
resolve: {
60+
breadcrumb: I18nBreadcrumbResolver
61+
},
62+
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
63+
canActivate: [SiteAdministratorGuard]
64+
},
5165
])
5266
]
5367
})

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
1212
import { FormModule } from '../shared/form/form.module';
1313
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
1414
import { AbstractControl } from '@angular/forms';
15+
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
16+
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
17+
import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component';
18+
import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component';
19+
import { SearchModule } from '../shared/search/search.module';
20+
import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module';
1521

1622
/**
1723
* Condition for displaying error messages on email form field
@@ -28,6 +34,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
2834
RouterModule,
2935
AccessControlRoutingModule,
3036
FormModule,
37+
NgbAccordionModule,
38+
SearchModule,
39+
AccessControlFormModule,
3140
],
3241
exports: [
3342
MembersListComponent,
@@ -39,6 +48,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
3948
GroupFormComponent,
4049
SubgroupsListComponent,
4150
MembersListComponent,
51+
BulkAccessComponent,
52+
BulkAccessBrowseComponent,
53+
BulkAccessSettingsComponent,
4254
],
4355
providers: [
4456
{
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<ngb-accordion #acc="ngbAccordion" [activeIds]="'browse'">
2+
<ngb-panel [id]="'browse'">
3+
<ng-template ngbPanelHeader>
4+
<div class="w-100 d-flex justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('browse')"
5+
data-test="browse">
6+
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()"
7+
[attr.aria-expanded]="!acc.isExpanded('browse')"
8+
aria-controls="collapsePanels">
9+
{{ 'admin.access-control.bulk-access-browse.header' | translate }}
10+
</button>
11+
<div class="text-right d-flex">
12+
<div class="ml-3 d-inline-block">
13+
<span *ngIf="acc.isExpanded('browse')" class="fas fa-chevron-up fa-fw"></span>
14+
<span *ngIf="!acc.isExpanded('browse')" class="fas fa-chevron-down fa-fw"></span>
15+
</div>
16+
</div>
17+
</div>
18+
</ng-template>
19+
<ng-template ngbPanelContent>
20+
<ul ngbNav #nav="ngbNav" [(activeId)]="activateId" class="nav-pills">
21+
<li [ngbNavItem]="'search'">
22+
<a ngbNavLink>{{'admin.access-control.bulk-access-browse.search.header' | translate}}</a>
23+
<ng-template ngbNavContent>
24+
<div class="mx-n3">
25+
<ds-themed-search [configuration]="'administrativeBulkAccess'"
26+
[selectable]="true"
27+
[selectionConfig]="{ repeatable: true, listId: listId }"
28+
[showThumbnails]="false"></ds-themed-search>
29+
</div>
30+
</ng-template>
31+
</li>
32+
<li [ngbNavItem]="'selected'">
33+
<a ngbNavLink>
34+
{{'admin.access-control.bulk-access-browse.selected.header' | translate: {number: ((objectsSelected$ | async)?.payload?.totalElements) ? (objectsSelected$ | async)?.payload?.totalElements : '0'} }}
35+
</a>
36+
<ng-template ngbNavContent>
37+
<ds-pagination
38+
[paginationOptions]="(paginationOptions$ | async)"
39+
[pageInfoState]="(objectsSelected$|async)?.payload.pageInfo"
40+
[collectionSize]="(objectsSelected$|async)?.payload?.totalElements"
41+
[objects]="(objectsSelected$|async)"
42+
[showPaginator]="false"
43+
(prev)="pagePrev()"
44+
(next)="pageNext()">
45+
<ul *ngIf="(objectsSelected$|async)?.hasSucceeded" class="list-unstyled ml-4">
46+
<li *ngFor='let object of (objectsSelected$|async)?.payload?.page | paginate: { itemsPerPage: (paginationOptions$ | async).pageSize,
47+
currentPage: (paginationOptions$ | async).currentPage, totalItems: (objectsSelected$|async)?.payload?.page.length }; let i = index; let last = last '
48+
class="mt-4 mb-4 d-flex"
49+
[attr.data-test]="'list-object' | dsBrowserOnly">
50+
<ds-selectable-list-item-control [index]="i"
51+
[object]="object"
52+
[selectionConfig]="{ repeatable: true, listId: listId }"></ds-selectable-list-item-control>
53+
<ds-listable-object-component-loader [listID]="listId"
54+
[index]="i"
55+
[object]="object"
56+
[showThumbnails]="false"
57+
[viewMode]="'list'"></ds-listable-object-component-loader>
58+
</li>
59+
</ul>
60+
</ds-pagination>
61+
</ng-template>
62+
</li>
63+
</ul>
64+
<div [ngbNavOutlet]="nav" class="mt-5"></div>
65+
</ng-template>
66+
</ngb-panel>
67+
</ngb-accordion>

src/app/access-control/bulk-access/browse/bulk-access-browse.component.scss

Whitespace-only changes.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2+
import { NO_ERRORS_SCHEMA } from '@angular/core';
3+
4+
import { of } from 'rxjs';
5+
import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
6+
import { TranslateModule } from '@ngx-translate/core';
7+
8+
import { BulkAccessBrowseComponent } from './bulk-access-browse.component';
9+
import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service';
10+
import { SelectableObject } from '../../../shared/object-list/selectable-list/selectable-list.service.spec';
11+
import { PageInfo } from '../../../core/shared/page-info.model';
12+
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
13+
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
14+
15+
describe('BulkAccessBrowseComponent', () => {
16+
let component: BulkAccessBrowseComponent;
17+
let fixture: ComponentFixture<BulkAccessBrowseComponent>;
18+
19+
const listID1 = 'id1';
20+
const value1 = 'Selected object';
21+
const value2 = 'Another selected object';
22+
23+
const selected1 = new SelectableObject(value1);
24+
const selected2 = new SelectableObject(value2);
25+
26+
const testSelection = { id: listID1, selection: [selected1, selected2] } ;
27+
28+
const selectableListService = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']);
29+
beforeEach(waitForAsync(() => {
30+
TestBed.configureTestingModule({
31+
imports: [
32+
NgbAccordionModule,
33+
NgbNavModule,
34+
TranslateModule.forRoot()
35+
],
36+
declarations: [BulkAccessBrowseComponent],
37+
providers: [ { provide: SelectableListService, useValue: selectableListService }, ],
38+
schemas: [
39+
NO_ERRORS_SCHEMA
40+
]
41+
}).compileComponents();
42+
}));
43+
44+
beforeEach(() => {
45+
fixture = TestBed.createComponent(BulkAccessBrowseComponent);
46+
component = fixture.componentInstance;
47+
(component as any).selectableListService.getSelectableList.and.returnValue(of(testSelection));
48+
fixture.detectChanges();
49+
});
50+
51+
afterEach(() => {
52+
fixture.destroy();
53+
component = null;
54+
});
55+
56+
it('should create the component', () => {
57+
expect(component).toBeTruthy();
58+
});
59+
60+
it('should have an initial active nav id of "search"', () => {
61+
expect(component.activateId).toEqual('search');
62+
});
63+
64+
it('should have an initial pagination options object with default values', () => {
65+
expect(component.paginationOptions$.getValue().id).toEqual('bas');
66+
expect(component.paginationOptions$.getValue().pageSize).toEqual(5);
67+
expect(component.paginationOptions$.getValue().currentPage).toEqual(1);
68+
});
69+
70+
it('should have an initial remote data with a paginated list as value', () => {
71+
const list = buildPaginatedList(new PageInfo({
72+
'elementsPerPage': 5,
73+
'totalElements': 2,
74+
'totalPages': 1,
75+
'currentPage': 1
76+
}), [selected1, selected2]) ;
77+
const rd = createSuccessfulRemoteDataObject(list);
78+
79+
expect(component.objectsSelected$.value).toEqual(rd);
80+
});
81+
82+
});

0 commit comments

Comments
 (0)