mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
[DURACOM-191] align with main branch and migrated to be standalone new components
This commit is contained in:
@@ -208,6 +208,9 @@ languages:
|
|||||||
- code: pt-BR
|
- code: pt-BR
|
||||||
label: Português do Brasil
|
label: Português do Brasil
|
||||||
active: true
|
active: true
|
||||||
|
- code: sr-lat
|
||||||
|
label: Srpski (lat)
|
||||||
|
active: true
|
||||||
- code: fi
|
- code: fi
|
||||||
label: Suomi
|
label: Suomi
|
||||||
active: true
|
active: true
|
||||||
@@ -232,6 +235,9 @@ languages:
|
|||||||
- code: el
|
- code: el
|
||||||
label: Ελληνικά
|
label: Ελληνικά
|
||||||
active: true
|
active: true
|
||||||
|
- code: sr-cyr
|
||||||
|
label: Српски
|
||||||
|
active: true
|
||||||
- code: uk
|
- code: uk
|
||||||
label: Yкраї́нська
|
label: Yкраї́нська
|
||||||
active: true
|
active: true
|
||||||
@@ -379,4 +385,4 @@ vocabularies:
|
|||||||
# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
|
# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
|
||||||
comcolSelectionSort:
|
comcolSelectionSort:
|
||||||
sortField: 'dc.title'
|
sortField: 'dc.title'
|
||||||
sortDirection: 'ASC'
|
sortDirection: 'ASC'
|
||||||
|
@@ -177,6 +177,8 @@ function generateViewEvent(uuid: string, dsoType: string): void {
|
|||||||
[XSRF_REQUEST_HEADER] : csrfToken,
|
[XSRF_REQUEST_HEADER] : csrfToken,
|
||||||
// use a known public IP address to avoid being seen as a "bot"
|
// use a known public IP address to avoid being seen as a "bot"
|
||||||
'X-Forwarded-For': '1.1.1.1',
|
'X-Forwarded-For': '1.1.1.1',
|
||||||
|
// Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot"
|
||||||
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
|
||||||
},
|
},
|
||||||
//form: true, // indicates the body should be form urlencoded
|
//form: true, // indicates the body should be form urlencoded
|
||||||
body: { targetId: uuid, targetType: dsoType },
|
body: { targetId: uuid, targetType: dsoType },
|
||||||
|
@@ -15,14 +15,14 @@
|
|||||||
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
||||||
"build": "ng build --configuration development",
|
"build": "ng build --configuration development",
|
||||||
"build:stats": "ng build --stats-json",
|
"build:stats": "ng build --stats-json",
|
||||||
"build:prod": "yarn run build:ssr",
|
"build:prod": "cross-env NODE_ENV=production yarn run build:ssr",
|
||||||
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
||||||
"test": "ng test --source-map=true --watch=false --configuration test",
|
"test": "ng test --source-map=true --watch=false --configuration test",
|
||||||
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
|
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
|
||||||
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"lint-fix": "ng lint --fix=true",
|
"lint-fix": "ng lint --fix=true",
|
||||||
"e2e": "ng e2e",
|
"e2e": "cross-env NODE_ENV=production ng e2e",
|
||||||
"clean:dev:config": "rimraf src/assets/config.json",
|
"clean:dev:config": "rimraf src/assets/config.json",
|
||||||
"clean:coverage": "rimraf coverage",
|
"clean:coverage": "rimraf coverage",
|
||||||
"clean:dist": "rimraf dist",
|
"clean:dist": "rimraf dist",
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"@types/grecaptcha": "^3.0.4",
|
"@types/grecaptcha": "^3.0.4",
|
||||||
"angular-idle-preload": "3.0.0",
|
"angular-idle-preload": "3.0.0",
|
||||||
"angulartics2": "^12.2.0",
|
"angulartics2": "^12.2.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^1.6.0",
|
||||||
"bootstrap": "^4.6.1",
|
"bootstrap": "^4.6.1",
|
||||||
"cerialize": "0.1.18",
|
"cerialize": "0.1.18",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
|
@@ -1,12 +1,22 @@
|
|||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getAccessControlModuleRoute } from '../app-routing-paths';
|
import { getAccessControlModuleRoute } from '../app-routing-paths';
|
||||||
|
|
||||||
export const GROUP_EDIT_PATH = 'groups';
|
export const EPERSON_PATH = 'epeople';
|
||||||
|
|
||||||
|
export function getEPersonsRoute(): string {
|
||||||
|
return new URLCombiner(getAccessControlModuleRoute(), EPERSON_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEPersonEditRoute(id: string): string {
|
||||||
|
return new URLCombiner(getEPersonsRoute(), id, 'edit').toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GROUP_PATH = 'groups';
|
||||||
|
|
||||||
export function getGroupsRoute() {
|
export function getGroupsRoute() {
|
||||||
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH).toString();
|
return new URLCombiner(getAccessControlModuleRoute(), GROUP_PATH).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGroupEditRoute(id: string) {
|
export function getGroupEditRoute(id: string) {
|
||||||
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString();
|
return new URLCombiner(getGroupsRoute(), id, 'edit').toString();
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||||
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
||||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||||
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
|
import { EPERSON_PATH, GROUP_PATH } from './access-control-routing-paths';
|
||||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { GroupPageGuard } from './group-registry/group-page.guard';
|
import { GroupPageGuard } from './group-registry/group-page.guard';
|
||||||
import {
|
import {
|
||||||
@@ -13,12 +13,14 @@ import {
|
|||||||
SiteAdministratorGuard
|
SiteAdministratorGuard
|
||||||
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
||||||
|
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
|
||||||
|
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: 'epeople',
|
path: EPERSON_PATH,
|
||||||
component: EPeopleRegistryComponent,
|
component: EPeopleRegistryComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
@@ -27,7 +29,26 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
|||||||
canActivate: [SiteAdministratorGuard]
|
canActivate: [SiteAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: GROUP_EDIT_PATH,
|
path: `${EPERSON_PATH}/create`,
|
||||||
|
component: EPersonFormComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
|
||||||
|
canActivate: [SiteAdministratorGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${EPERSON_PATH}/:id/edit`,
|
||||||
|
component: EPersonFormComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
ePerson: EPersonResolver,
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
|
||||||
|
canActivate: [SiteAdministratorGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: GROUP_PATH,
|
||||||
component: GroupsRegistryComponent,
|
component: GroupsRegistryComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
@@ -36,7 +57,7 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
|||||||
canActivate: [GroupAdministratorGuard]
|
canActivate: [GroupAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_EDIT_PATH}/newGroup`,
|
path: `${GROUP_PATH}/create`,
|
||||||
component: GroupFormComponent,
|
component: GroupFormComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
@@ -45,7 +66,7 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
|||||||
canActivate: [GroupAdministratorGuard]
|
canActivate: [GroupAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_EDIT_PATH}/:groupId`,
|
path: `${GROUP_PATH}/:groupId/edit`,
|
||||||
component: GroupFormComponent,
|
component: GroupFormComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
@@ -4,96 +4,91 @@
|
|||||||
<div class="d-flex justify-content-between border-bottom mb-3">
|
<div class="d-flex justify-content-between border-bottom mb-3">
|
||||||
<h2 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h2>
|
<h2 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h2>
|
||||||
|
|
||||||
<div *ngIf="!isEPersonFormShown">
|
<div>
|
||||||
<button class="mr-auto btn btn-success addEPerson-button"
|
<button class="mr-auto btn btn-success addEPerson-button"
|
||||||
(click)="isEPersonFormShown = true">
|
[routerLink]="'create'">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="reset()"
|
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
|
||||||
(cancelForm)="isEPersonFormShown = false"></ds-eperson-form>
|
|
||||||
|
|
||||||
<div *ngIf="!isEPersonFormShown">
|
</h3>
|
||||||
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
|
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
</h3>
|
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
|
||||||
<div>
|
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
|
||||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
</select>
|
||||||
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
|
</div>
|
||||||
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
|
<div class="flex-grow-1 mr-3 ml-3">
|
||||||
</select>
|
<div class="form-group input-group">
|
||||||
</div>
|
<input type="text" name="query" id="query" formControlName="query"
|
||||||
<div class="flex-grow-1 mr-3 ml-3">
|
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
|
||||||
<div class="form-group input-group">
|
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
||||||
<input type="text" name="query" id="query" formControlName="query"
|
<span class="input-group-append">
|
||||||
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
|
|
||||||
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
|
||||||
<span class="input-group-append">
|
|
||||||
<button type="submit" class="search-button btn btn-primary">
|
<button type="submit" class="search-button btn btn-primary">
|
||||||
<i class="fas fa-search"></i> {{ labelPrefix + 'search.button' | translate }}
|
<i class="fas fa-search"></i> {{ labelPrefix + 'search.button' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<button (click)="clearFormAndResetResult();"
|
|
||||||
class="search-button btn btn-secondary">{{labelPrefix + 'button.see-all' | translate}}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
|
|
||||||
<ds-pagination
|
|
||||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
|
|
||||||
[paginationOptions]="config"
|
|
||||||
[pageInfoState]="pageInfoState$"
|
|
||||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
|
||||||
[hideGear]="true"
|
|
||||||
[hidePagerWhenSinglePage]="true">
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table id="epeople" class="table table-striped table-hover table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
|
|
||||||
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
|
|
||||||
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
|
|
||||||
<th>{{labelPrefix + 'table.edit' | translate}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
|
||||||
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
|
|
||||||
<td>{{epersonDto.eperson.id}}</td>
|
|
||||||
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
|
||||||
<td>{{epersonDto.eperson.email}}</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group edit-field">
|
|
||||||
<button (click)="toggleEditEPerson(epersonDto.eperson)"
|
|
||||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
|
||||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
|
||||||
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
|
||||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ds-pagination>
|
|
||||||
|
|
||||||
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
|
||||||
{{labelPrefix + 'no-items' | translate}}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<button (click)="clearFormAndResetResult();"
|
||||||
|
class="search-button btn btn-secondary">{{labelPrefix + 'button.see-all' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
|
||||||
|
<ds-pagination
|
||||||
|
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
|
||||||
|
[paginationOptions]="config"
|
||||||
|
[pageInfoState]="pageInfoState$"
|
||||||
|
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||||
|
[hideGear]="true"
|
||||||
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
|
||||||
|
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
|
||||||
|
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
|
||||||
|
<th>{{labelPrefix + 'table.edit' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
||||||
|
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
|
||||||
|
<td>{{epersonDto.eperson.id}}</td>
|
||||||
|
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
||||||
|
<td>{{epersonDto.eperson.email}}</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group edit-field">
|
||||||
|
<button [routerLink]="getEditEPeoplePage(epersonDto.eperson.id)"
|
||||||
|
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||||
|
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||||
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
<button *ngIf="epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
||||||
|
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||||
|
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||||
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ds-pagination>
|
||||||
|
|
||||||
|
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||||
|
{{labelPrefix + 'no-items' | translate}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -214,36 +214,6 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toggleEditEPerson', () => {
|
|
||||||
describe('when you click on first edit eperson button', () => {
|
|
||||||
beforeEach(fakeAsync(() => {
|
|
||||||
const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton'));
|
|
||||||
editButtons[0].triggerEventHandler('click', {
|
|
||||||
preventDefault: () => {/**/
|
|
||||||
}
|
|
||||||
});
|
|
||||||
tick();
|
|
||||||
fixture.detectChanges();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('editEPerson form is toggled', () => {
|
|
||||||
const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
|
||||||
ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
|
||||||
if (ePeopleIds[0] && activeEPerson === ePeopleIds[0].nativeElement.textContent) {
|
|
||||||
expect(component.isEPersonFormShown).toEqual(false);
|
|
||||||
} else {
|
|
||||||
expect(component.isEPersonFormShown).toEqual(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('EPerson search section is hidden', () => {
|
|
||||||
expect(fixture.debugElement.query(By.css('#search'))).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteEPerson', () => {
|
describe('deleteEPerson', () => {
|
||||||
describe('when you click on first delete eperson button', () => {
|
describe('when you click on first delete eperson button', () => {
|
||||||
let ePeopleIdsFoundBeforeDelete;
|
let ePeopleIdsFoundBeforeDelete;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
|
import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router, RouterModule } from '@angular/router';
|
||||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
|
||||||
import { map, switchMap, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
@@ -26,12 +26,14 @@ import { AsyncPipe, NgClass, NgForOf, NgIf } from '@angular/common';
|
|||||||
import { EPersonFormComponent } from './eperson-form/eperson-form.component';
|
import { EPersonFormComponent } from './eperson-form/eperson-form.component';
|
||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||||
import { PaginationComponent } from '../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../shared/pagination/pagination.component';
|
||||||
|
import { getEPersonEditRoute, getEPersonsRoute } from '../access-control-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-epeople-registry',
|
selector: 'ds-epeople-registry',
|
||||||
templateUrl: './epeople-registry.component.html',
|
templateUrl: './epeople-registry.component.html',
|
||||||
imports: [
|
imports: [
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
|
RouterModule,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
NgIf,
|
NgIf,
|
||||||
EPersonFormComponent,
|
EPersonFormComponent,
|
||||||
@@ -80,11 +82,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
currentPage: 1
|
currentPage: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not to show the EPerson form
|
|
||||||
*/
|
|
||||||
isEPersonFormShown: boolean;
|
|
||||||
|
|
||||||
// The search form
|
// The search form
|
||||||
searchForm;
|
searchForm;
|
||||||
|
|
||||||
@@ -130,17 +127,11 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.searching$.next(true);
|
this.searching$.next(true);
|
||||||
this.isEPersonFormShown = false;
|
|
||||||
this.search({scope: this.currentSearchScope, query: this.currentSearchQuery});
|
this.search({scope: this.currentSearchScope, query: this.currentSearchQuery});
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
|
||||||
if (eperson != null && eperson.id) {
|
|
||||||
this.isEPersonFormShown = true;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
this.subs.push(this.ePeople$.pipe(
|
this.subs.push(this.ePeople$.pipe(
|
||||||
switchMap((epeople: PaginatedList<EPerson>) => {
|
switchMap((epeople: PaginatedList<EPerson>) => {
|
||||||
if (epeople.pageInfo.totalElements > 0) {
|
if (epeople.pageInfo.totalElements > 0) {
|
||||||
return combineLatest([...epeople.page.map((eperson: EPerson) => {
|
return combineLatest(epeople.page.map((eperson: EPerson) => {
|
||||||
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe(
|
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe(
|
||||||
map((authorized) => {
|
map((authorized) => {
|
||||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||||
@@ -149,7 +140,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
return epersonDtoModel;
|
return epersonDtoModel;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})]).pipe(map((dtos: EpersonDtoModel[]) => {
|
})).pipe(map((dtos: EpersonDtoModel[]) => {
|
||||||
return buildPaginatedList(epeople.pageInfo, dtos);
|
return buildPaginatedList(epeople.pageInfo, dtos);
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
@@ -176,14 +167,14 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
const query: string = data.query;
|
const query: string = data.query;
|
||||||
const scope: string = data.scope;
|
const scope: string = data.scope;
|
||||||
if (query != null && this.currentSearchQuery !== query) {
|
if (query != null && this.currentSearchQuery !== query) {
|
||||||
this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], {
|
void this.router.navigate([getEPersonsRoute()], {
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
this.currentSearchQuery = query;
|
this.currentSearchQuery = query;
|
||||||
this.paginationService.resetPage(this.config.id);
|
this.paginationService.resetPage(this.config.id);
|
||||||
}
|
}
|
||||||
if (scope != null && this.currentSearchScope !== scope) {
|
if (scope != null && this.currentSearchScope !== scope) {
|
||||||
this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], {
|
void this.router.navigate([getEPersonsRoute()], {
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
this.currentSearchScope = scope;
|
this.currentSearchScope = scope;
|
||||||
@@ -221,23 +212,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
return this.epersonService.getActiveEPerson();
|
return this.epersonService.getActiveEPerson();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Start editing the selected EPerson
|
|
||||||
* @param ePerson
|
|
||||||
*/
|
|
||||||
toggleEditEPerson(ePerson: EPerson) {
|
|
||||||
this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => {
|
|
||||||
if (ePerson === activeEPerson) {
|
|
||||||
this.epersonService.cancelEditEPerson();
|
|
||||||
this.isEPersonFormShown = false;
|
|
||||||
} else {
|
|
||||||
this.epersonService.editEPerson(ePerson);
|
|
||||||
this.isEPersonFormShown = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.scrollToTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
||||||
*/
|
*/
|
||||||
@@ -258,7 +232,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
if (restResponse.hasSucceeded) {
|
if (restResponse.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)}));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)}));
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { id: ePerson.id, statusCode: restResponse.statusCode, errorMessage: restResponse.errorMessage }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -280,16 +254,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToTop() {
|
|
||||||
(function smoothscroll() {
|
|
||||||
const currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
|
|
||||||
if (currentScroll > 0) {
|
|
||||||
window.requestAnimationFrame(smoothscroll);
|
|
||||||
window.scrollTo(0, currentScroll - (currentScroll / 8));
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset all input-fields to be empty and search all search
|
* Reset all input-fields to be empty and search all search
|
||||||
*/
|
*/
|
||||||
@@ -300,20 +264,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
this.search({query: ''});
|
this.search({query: ''});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getEditEPeoplePage(id: string): string {
|
||||||
* This method will set everything to stale, which will cause the lists on this page to update.
|
return getEPersonEditRoute(id);
|
||||||
*/
|
|
||||||
reset(): void {
|
|
||||||
this.epersonService.getBrowseEndpoint().pipe(
|
|
||||||
take(1),
|
|
||||||
switchMap((href: string) => {
|
|
||||||
return this.requestService.setStaleByHrefSubstring(href).pipe(
|
|
||||||
take(1),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
).subscribe(()=>{
|
|
||||||
this.epersonService.cancelEditEPerson();
|
|
||||||
this.isEPersonFormShown = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,89 +1,97 @@
|
|||||||
<div *ngIf="epersonService.getActiveEPerson() | async; then editheader; else createHeader"></div>
|
<div class="container">
|
||||||
|
<div class="group-form row">
|
||||||
|
<div class="col-12">
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<div *ngIf="epersonService.getActiveEPerson() | async; then editHeader; else createHeader"></div>
|
||||||
<h4>{{messagePrefix + '.create' | translate}}</h4>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #editheader>
|
<ng-template #createHeader>
|
||||||
<h4>{{messagePrefix + '.edit' | translate}}</h4>
|
<h2 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h2>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ds-form [formId]="formId"
|
<ng-template #editHeader>
|
||||||
[formModel]="formModel"
|
<h2 class="border-bottom pb-2">{{messagePrefix + '.edit' | translate}}</h2>
|
||||||
[formGroup]="formGroup"
|
</ng-template>
|
||||||
[formLayout]="formLayout"
|
|
||||||
[displayCancel]="false"
|
|
||||||
[submitLabel]="submitLabel"
|
|
||||||
(submitForm)="onSubmit()">
|
|
||||||
<div before class="btn-group">
|
|
||||||
<button (click)="onCancel()" type="button"
|
|
||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="displayResetPassword" between class="btn-group">
|
|
||||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
|
|
||||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div between class="btn-group ml-1">
|
|
||||||
<button *ngIf="!isImpersonated" class="btn btn-primary" type="button" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (click)="impersonate()">
|
|
||||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
|
||||||
</button>
|
|
||||||
<button *ngIf="isImpersonated" class="btn btn-primary" type="button" (click)="stopImpersonating()">
|
|
||||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button after class="btn btn-danger delete-button" type="button" [disabled]="!(canDelete$ | async)" (click)="delete()">
|
|
||||||
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
|
||||||
</button>
|
|
||||||
</ds-form>
|
|
||||||
|
|
||||||
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
<ds-form [formId]="formId"
|
||||||
|
[formModel]="formModel"
|
||||||
|
[formGroup]="formGroup"
|
||||||
|
[formLayout]="formLayout"
|
||||||
|
[displayCancel]="false"
|
||||||
|
[submitLabel]="submitLabel"
|
||||||
|
(submitForm)="onSubmit()">
|
||||||
|
<div before class="btn-group">
|
||||||
|
<button (click)="onCancel()" type="button" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||||
|
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
|
||||||
|
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="canImpersonate$ | async" between class="btn-group">
|
||||||
|
<button *ngIf="!isImpersonated" class="btn btn-primary" type="button" (click)="impersonate()">
|
||||||
|
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||||
|
</button>
|
||||||
|
<button *ngIf="isImpersonated" class="btn btn-primary" type="button" (click)="stopImpersonating()">
|
||||||
|
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button *ngIf="canDelete$ | async" after class="btn btn-danger delete-button" type="button" (click)="delete()">
|
||||||
|
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||||
|
</button>
|
||||||
|
</ds-form>
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
||||||
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
|
||||||
|
|
||||||
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||||
|
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
||||||
|
|
||||||
<ds-pagination
|
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
||||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
|
||||||
[paginationOptions]="config"
|
|
||||||
[pageInfoState]="(groups | async)?.payload"
|
|
||||||
[collectionSize]="(groups | async)?.payload?.totalElements"
|
|
||||||
[hideGear]="true"
|
|
||||||
[hidePagerWhenSinglePage]="true"
|
|
||||||
(pageChange)="onPageChange($event)">
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
<ds-pagination
|
||||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||||
<thead>
|
[paginationOptions]="config"
|
||||||
<tr>
|
[pageInfoState]="(groups | async)?.payload"
|
||||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
[collectionSize]="(groups | async)?.payload?.totalElements"
|
||||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
[hideGear]="true"
|
||||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
[hidePagerWhenSinglePage]="true"
|
||||||
</tr>
|
(pageChange)="onPageChange($event)">
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
|
||||||
<td class="align-middle">{{group.id}}</td>
|
|
||||||
<td class="align-middle">
|
|
||||||
<a (click)="groupsDataService.startEditingNewGroup(group)"
|
|
||||||
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
|
|
||||||
{{ dsoNameService.getName(group) }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ds-pagination>
|
<div class="table-responsive">
|
||||||
|
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
||||||
|
<td class="align-middle">{{group.id}}</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||||
|
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
|
||||||
|
{{ dsoNameService.getName(group) }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
</ds-pagination>
|
||||||
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
|
|
||||||
<div>
|
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||||
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
|
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
|
||||||
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
|
<div>
|
||||||
|
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
|
||||||
|
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -34,6 +34,10 @@ import { EpersonRegistrationService } from '../../../core/data/eperson-registrat
|
|||||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||||
import { FormComponent } from '../../../shared/form/form.component';
|
import { FormComponent } from '../../../shared/form/form.component';
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
|
|
||||||
describe('EPersonFormComponent', () => {
|
describe('EPersonFormComponent', () => {
|
||||||
let component: EPersonFormComponent;
|
let component: EPersonFormComponent;
|
||||||
@@ -46,6 +50,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let groupsDataService: GroupDataService;
|
let groupsDataService: GroupDataService;
|
||||||
let epersonRegistrationService: EpersonRegistrationService;
|
let epersonRegistrationService: EpersonRegistrationService;
|
||||||
|
let route: ActivatedRouteStub;
|
||||||
|
let router: RouterStub;
|
||||||
|
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
@@ -109,6 +115,9 @@ describe('EPersonFormComponent', () => {
|
|||||||
},
|
},
|
||||||
getEPersonByEmail(email): Observable<RemoteData<EPerson>> {
|
getEPersonByEmail(email): Observable<RemoteData<EPerson>> {
|
||||||
return createSuccessfulRemoteDataObject$(null);
|
return createSuccessfulRemoteDataObject$(null);
|
||||||
|
},
|
||||||
|
findById(_id: string, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<EPerson>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
builderService = Object.assign(getMockFormBuilderService(),{
|
builderService = Object.assign(getMockFormBuilderService(),{
|
||||||
@@ -185,6 +194,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
paginationService = new PaginationServiceStub();
|
paginationService = new PaginationServiceStub();
|
||||||
|
route = new ActivatedRouteStub();
|
||||||
|
router = new RouterStub();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
@@ -203,6 +214,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) },
|
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) },
|
||||||
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
|
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
|
||||||
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
EPeopleRegistryComponent
|
EPeopleRegistryComponent
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -268,24 +281,18 @@ describe('EPersonFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
describe('firstName, lastName and email should be required', () => {
|
describe('firstName, lastName and email should be required', () => {
|
||||||
it('form should be invalid because the firstName is required', waitForAsync(() => {
|
it('form should be invalid because the firstName is required', () => {
|
||||||
fixture.whenStable().then(() => {
|
expect(component.formGroup.controls.firstName.valid).toBeFalse();
|
||||||
expect(component.formGroup.controls.firstName.valid).toBeFalse();
|
expect(component.formGroup.controls.firstName.errors.required).toBeTrue();
|
||||||
expect(component.formGroup.controls.firstName.errors.required).toBeTrue();
|
});
|
||||||
});
|
it('form should be invalid because the lastName is required', () => {
|
||||||
}));
|
expect(component.formGroup.controls.lastName.valid).toBeFalse();
|
||||||
it('form should be invalid because the lastName is required', waitForAsync(() => {
|
expect(component.formGroup.controls.lastName.errors.required).toBeTrue();
|
||||||
fixture.whenStable().then(() => {
|
});
|
||||||
expect(component.formGroup.controls.lastName.valid).toBeFalse();
|
it('form should be invalid because the email is required', () => {
|
||||||
expect(component.formGroup.controls.lastName.errors.required).toBeTrue();
|
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||||
});
|
expect(component.formGroup.controls.email.errors.required).toBeTrue();
|
||||||
}));
|
});
|
||||||
it('form should be invalid because the email is required', waitForAsync(() => {
|
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
|
||||||
expect(component.formGroup.controls.email.errors.required).toBeTrue();
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('after inserting information firstName,lastName and email not required', () => {
|
describe('after inserting information firstName,lastName and email not required', () => {
|
||||||
@@ -295,24 +302,18 @@ describe('EPersonFormComponent', () => {
|
|||||||
component.formGroup.controls.email.setValue('test@test.com');
|
component.formGroup.controls.email.setValue('test@test.com');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('firstName should be valid because the firstName is set', waitForAsync(() => {
|
it('firstName should be valid because the firstName is set', () => {
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.firstName.valid).toBeTrue();
|
expect(component.formGroup.controls.firstName.valid).toBeTrue();
|
||||||
expect(component.formGroup.controls.firstName.errors).toBeNull();
|
expect(component.formGroup.controls.firstName.errors).toBeNull();
|
||||||
});
|
});
|
||||||
}));
|
it('lastName should be valid because the lastName is set', () => {
|
||||||
it('lastName should be valid because the lastName is set', waitForAsync(() => {
|
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.lastName.valid).toBeTrue();
|
expect(component.formGroup.controls.lastName.valid).toBeTrue();
|
||||||
expect(component.formGroup.controls.lastName.errors).toBeNull();
|
expect(component.formGroup.controls.lastName.errors).toBeNull();
|
||||||
});
|
});
|
||||||
}));
|
it('email should be valid because the email is set', () => {
|
||||||
it('email should be valid because the email is set', waitForAsync(() => {
|
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.email.valid).toBeTrue();
|
expect(component.formGroup.controls.email.valid).toBeTrue();
|
||||||
expect(component.formGroup.controls.email.errors).toBeNull();
|
expect(component.formGroup.controls.email.errors).toBeNull();
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -321,12 +322,10 @@ describe('EPersonFormComponent', () => {
|
|||||||
component.formGroup.controls.email.setValue('test@test');
|
component.formGroup.controls.email.setValue('test@test');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('email should not be valid because the email pattern', waitForAsync(() => {
|
it('email should not be valid because the email pattern', () => {
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||||
expect(component.formGroup.controls.email.errors.pattern).toBeTruthy();
|
expect(component.formGroup.controls.email.errors.pattern).toBeTruthy();
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('after already utilized email', () => {
|
describe('after already utilized email', () => {
|
||||||
@@ -341,12 +340,10 @@ describe('EPersonFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('email should not be valid because email is already taken', waitForAsync(() => {
|
it('email should not be valid because email is already taken', () => {
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||||
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -398,11 +395,9 @@ describe('EPersonFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit a new eperson using the correct values', waitForAsync(() => {
|
it('should emit a new eperson using the correct values', () => {
|
||||||
fixture.whenStable().then(() => {
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
});
|
||||||
});
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with an active eperson', () => {
|
describe('with an active eperson', () => {
|
||||||
@@ -433,11 +428,9 @@ describe('EPersonFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit the existing eperson using the correct values', waitForAsync(() => {
|
it('should emit the existing eperson using the correct values', () => {
|
||||||
fixture.whenStable().then(() => {
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
});
|
||||||
});
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -496,16 +489,16 @@ describe('EPersonFormComponent', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delete button should be active if the eperson can be deleted', () => {
|
it('the delete button should be visible if the ePerson can be deleted', () => {
|
||||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(false);
|
expect(deleteButton).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delete button should be disabled if the eperson cannot be deleted', () => {
|
it('the delete button should be hidden if the ePerson cannot be deleted', () => {
|
||||||
component.canDelete$ = observableOf(false);
|
component.canDelete$ = observableOf(false);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
expect(deleteButton).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the epersonFormComponent delete when clicked on the button', () => {
|
it('should call the epersonFormComponent delete when clicked on the button', () => {
|
||||||
|
@@ -43,6 +43,8 @@ import { AsyncPipe, NgClass, NgIf } from '@angular/common';
|
|||||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { getEPersonsRoute } from '../../access-control-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-eperson-form',
|
selector: 'ds-eperson-form',
|
||||||
@@ -210,6 +212,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
public requestService: RequestService,
|
public requestService: RequestService,
|
||||||
private epersonRegistrationService: EpersonRegistrationService,
|
private epersonRegistrationService: EpersonRegistrationService,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected router: Router,
|
||||||
) {
|
) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
@@ -229,7 +233,9 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* This method will initialise the page
|
* This method will initialise the page
|
||||||
*/
|
*/
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
|
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
|
||||||
|
this.epersonService.editEPerson(ePersonRD.payload);
|
||||||
|
}));
|
||||||
observableCombineLatest([
|
observableCombineLatest([
|
||||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
this.translateService.get(`${this.messagePrefix}.firstName`),
|
||||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
this.translateService.get(`${this.messagePrefix}.lastName`),
|
||||||
@@ -355,6 +361,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
onCancel() {
|
onCancel() {
|
||||||
this.epersonService.cancelEditEPerson();
|
this.epersonService.cancelEditEPerson();
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
|
void this.router.navigate([getEPersonsRoute()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -406,6 +413,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
||||||
this.submitForm.emit(ePersonToCreate);
|
this.submitForm.emit(ePersonToCreate);
|
||||||
|
this.epersonService.clearEPersonRequests();
|
||||||
|
void this.router.navigateByUrl(getEPersonsRoute());
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
@@ -445,6 +454,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: this.dsoNameService.getName(editedEperson) }));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: this.dsoNameService.getName(editedEperson) }));
|
||||||
this.submitForm.emit(editedEperson);
|
this.submitForm.emit(editedEperson);
|
||||||
|
void this.router.navigateByUrl(getEPersonsRoute());
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: this.dsoNameService.getName(editedEperson) }));
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: this.dsoNameService.getName(editedEperson) }));
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
@@ -511,6 +521,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
).subscribe(({ restResponse, eperson }: { restResponse: RemoteData<NoContent> | null, eperson: EPerson }) => {
|
).subscribe(({ restResponse, eperson }: { restResponse: RemoteData<NoContent> | null, eperson: EPerson }) => {
|
||||||
if (restResponse?.hasSucceeded) {
|
if (restResponse?.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
|
||||||
|
void this.router.navigate([getEPersonsRoute()]);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`);
|
this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`);
|
||||||
}
|
}
|
||||||
@@ -557,16 +568,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will ensure that the page gets reset and that the cache is cleared
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
|
||||||
this.requestService.removeByHrefSubstring(eperson.self);
|
|
||||||
});
|
|
||||||
this.initialisePage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for the given ePerson if there is already an ePerson in the system with that email
|
* Checks for the given ePerson if there is already an ePerson in the system with that email
|
||||||
* and shows notification if this is the case
|
* and shows notification if this is the case
|
||||||
|
@@ -0,0 +1,53 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { ResolvedAction } from '../../core/resolving/resolver.actions';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig<EPerson>[] = [
|
||||||
|
followLink('groups'),
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific {@link EPerson} before the route is activated
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class EPersonResolver implements Resolve<RemoteData<EPerson>> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected ePersonService: EPersonDataService,
|
||||||
|
protected store: Store<any>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a {@link EPerson} based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns `Observable<<RemoteData<EPerson>>` Emits the found {@link EPerson} based on the parameters in the current
|
||||||
|
* route, or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<EPerson>> {
|
||||||
|
const ePersonRD$: Observable<RemoteData<EPerson>> = this.ePersonService.findById(route.params.id,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
...EPERSON_EDIT_FOLLOW_LINKS,
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
);
|
||||||
|
|
||||||
|
ePersonRD$.subscribe((ePersonRD: RemoteData<EPerson>) => {
|
||||||
|
this.store.dispatch(new ResolvedAction(state.url, ePersonRD.payload));
|
||||||
|
});
|
||||||
|
|
||||||
|
return ePersonRD$;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -2,13 +2,13 @@
|
|||||||
<div class="group-form row">
|
<div class="group-form row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
||||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editheader; else createHeader"></div>
|
<div *ngIf="groupDataService.getActiveGroup() | async; then editHeader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h2>
|
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h2>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #editheader>
|
<ng-template #editHeader>
|
||||||
<h2 class="border-bottom pb-2">
|
<h2 class="border-bottom pb-2">
|
||||||
<span
|
<span
|
||||||
*dsContextHelp="{
|
*dsContextHelp="{
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<button (click)="onCancel()" type="button"
|
<button (click)="onCancel()" type="button"
|
||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div after *ngIf="groupBeingEdited != null" class="btn-group">
|
<div after *ngIf="(canEdit$ | async) && !groupBeingEdited.permanent" class="btn-group">
|
||||||
<button class="btn btn-danger delete-button" [disabled]="!(canEdit$ | async) || groupBeingEdited.permanent"
|
<button class="btn btn-danger delete-button" [disabled]="!(canEdit$ | async) || groupBeingEdited.permanent"
|
||||||
(click)="delete()" type="button">
|
(click)="delete()" type="button">
|
||||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||||
|
@@ -10,7 +10,6 @@ import {
|
|||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
ObservedValueOf,
|
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
@@ -54,6 +53,7 @@ import { AsyncPipe, NgIf } from '@angular/common';
|
|||||||
import { ContextHelpDirective } from '../../../shared/context-help.directive';
|
import { ContextHelpDirective } from '../../../shared/context-help.directive';
|
||||||
import { MembersListComponent } from './members-list/members-list.component';
|
import { MembersListComponent } from './members-list/members-list.component';
|
||||||
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
|
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
|
||||||
|
import { getGroupEditRoute, getGroupsRoute } from '../../access-control-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-group-form',
|
selector: 'ds-group-form',
|
||||||
@@ -182,19 +182,19 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
switchMap((group: Group) => {
|
switchMap((group: Group) => {
|
||||||
return observableCombineLatest(
|
return observableCombineLatest([
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
|
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
|
||||||
this.hasLinkedDSO(group),
|
this.hasLinkedDSO(group),
|
||||||
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<boolean>>) => {
|
]).pipe(
|
||||||
return isAuthorized && !hasLinkedDSO;
|
map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO),
|
||||||
});
|
);
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
observableCombineLatest(
|
observableCombineLatest([
|
||||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
this.translateService.get(`${this.messagePrefix}.groupName`),
|
||||||
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
||||||
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
||||||
).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
]).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
||||||
this.groupName = new DynamicInputModel({
|
this.groupName = new DynamicInputModel({
|
||||||
id: 'groupName',
|
id: 'groupName',
|
||||||
label: groupName,
|
label: groupName,
|
||||||
@@ -232,12 +232,12 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest(
|
observableCombineLatest([
|
||||||
this.groupDataService.getActiveGroup(),
|
this.groupDataService.getActiveGroup(),
|
||||||
this.canEdit$,
|
this.canEdit$,
|
||||||
this.groupDataService.getActiveGroup()
|
this.groupDataService.getActiveGroup()
|
||||||
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload())))
|
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload())))
|
||||||
).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
||||||
|
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
|
|
||||||
@@ -247,12 +247,14 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
this.groupBeingEdited = activeGroup;
|
this.groupBeingEdited = activeGroup;
|
||||||
|
|
||||||
if (linkedObject?.name) {
|
if (linkedObject?.name) {
|
||||||
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
|
if (!this.formGroup.controls.groupCommunity) {
|
||||||
this.formGroup.patchValue({
|
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
|
||||||
groupName: activeGroup.name,
|
this.formGroup.patchValue({
|
||||||
groupCommunity: linkedObject?.name ?? '',
|
groupName: activeGroup.name,
|
||||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
groupCommunity: linkedObject?.name ?? '',
|
||||||
});
|
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.formModel = [
|
this.formModel = [
|
||||||
this.groupName,
|
this.groupName,
|
||||||
@@ -280,7 +282,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
onCancel() {
|
onCancel() {
|
||||||
this.groupDataService.cancelEditGroup();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
this.router.navigate([this.groupDataService.getGroupRegistryRouterLink()]);
|
void this.router.navigate([getGroupsRoute()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -327,7 +329,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
const groupSelfLink = rd.payload._links.self.href;
|
const groupSelfLink = rd.payload._links.self.href;
|
||||||
this.setActiveGroupWithLink(groupSelfLink);
|
this.setActiveGroupWithLink(groupSelfLink);
|
||||||
this.groupDataService.clearGroupsRequests();
|
this.groupDataService.clearGroupsRequests();
|
||||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(rd.payload.uuid));
|
void this.router.navigateByUrl(getGroupEditRoute(rd.payload.uuid));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name }));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name }));
|
||||||
|
@@ -1,6 +1,60 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||||
|
|
||||||
|
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
|
||||||
|
|
||||||
|
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.totalElements > 0"
|
||||||
|
[paginationOptions]="config"
|
||||||
|
[pageInfoState]="(ePeopleMembersOfGroup | async)"
|
||||||
|
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
|
||||||
|
[hideGear]="true"
|
||||||
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||||
|
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let eperson of (ePeopleMembersOfGroup | async)?.page">
|
||||||
|
<td class="align-middle">{{eperson.id}}</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<a (click)="ePersonDataService.startEditingNewEPerson(eperson)"
|
||||||
|
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
|
||||||
|
{{ dsoNameService.getName(eperson) }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
|
||||||
|
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<div class="btn-group edit-field">
|
||||||
|
<button (click)="deleteMemberFromGroup(eperson)"
|
||||||
|
[disabled]="actionConfig.remove.disabled"
|
||||||
|
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||||
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||||
|
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ds-pagination>
|
||||||
|
|
||||||
|
<div *ngIf="(ePeopleMembersOfGroup | async) == undefined || (ePeopleMembersOfGroup | async)?.totalElements == 0" class="alert alert-info w-100 mb-2"
|
||||||
|
role="alert">
|
||||||
|
{{messagePrefix + '.no-members-yet' | translate}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4 id="search" class="border-bottom pb-2">
|
<h4 id="search" class="border-bottom pb-2">
|
||||||
<span
|
<span
|
||||||
*dsContextHelp="{
|
*dsContextHelp="{
|
||||||
@@ -15,14 +69,8 @@
|
|||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||||
<div>
|
<div class="flex-grow-1 mr-3">
|
||||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
<div class="form-group input-group mr-3">
|
||||||
<option value="metadata">{{messagePrefix + '.search.scope.metadata' | translate}}</option>
|
|
||||||
<option value="email">{{messagePrefix + '.search.scope.email' | translate}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow-1 mr-3 ml-3">
|
|
||||||
<div class="form-group input-group">
|
|
||||||
<input type="text" name="query" id="query" formControlName="query"
|
<input type="text" name="query" id="query" formControlName="query"
|
||||||
class="form-control" aria-label="Search input">
|
class="form-control" aria-label="Search input">
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
@@ -37,10 +85,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ds-pagination *ngIf="(ePeopleSearchDtos | async)?.totalElements > 0"
|
<ds-pagination *ngIf="(ePeopleSearch | async)?.totalElements > 0"
|
||||||
[paginationOptions]="configSearch"
|
[paginationOptions]="configSearch"
|
||||||
[pageInfoState]="(ePeopleSearchDtos | async)"
|
[pageInfoState]="(ePeopleSearch | async)"
|
||||||
[collectionSize]="(ePeopleSearchDtos | async)?.totalElements"
|
[collectionSize]="(ePeopleSearch | async)?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
|
||||||
@@ -55,33 +103,24 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page">
|
<tr *ngFor="let eperson of (ePeopleSearch | async)?.page">
|
||||||
<td class="align-middle">{{ePerson.eperson.id}}</td>
|
<td class="align-middle">{{eperson.id}}</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
<a (click)="ePersonDataService.startEditingNewEPerson(eperson)"
|
||||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
|
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
|
||||||
{{ dsoNameService.getName(ePerson.eperson) }}
|
{{ dsoNameService.getName(eperson) }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
|
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
|
||||||
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
|
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button *ngIf="ePerson.memberOfGroup"
|
<button (click)="addMemberToGroup(eperson)"
|
||||||
(click)="deleteMemberFromGroup(ePerson)"
|
|
||||||
[disabled]="actionConfig.remove.disabled"
|
|
||||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
|
||||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button *ngIf="!ePerson.memberOfGroup"
|
|
||||||
(click)="addMemberToGroup(ePerson)"
|
|
||||||
[disabled]="actionConfig.add.disabled"
|
[disabled]="actionConfig.add.disabled"
|
||||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||||
<i [ngClass]="actionConfig.add.icon"></i>
|
<i [ngClass]="actionConfig.add.icon"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,72 +132,10 @@
|
|||||||
|
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
|
|
||||||
<div *ngIf="(ePeopleSearchDtos | async)?.totalElements == 0 && searchDone"
|
<div *ngIf="(ePeopleSearch | async)?.totalElements == 0 && searchDone"
|
||||||
class="alert alert-info w-100 mb-2"
|
class="alert alert-info w-100 mb-2"
|
||||||
role="alert">
|
role="alert">
|
||||||
{{messagePrefix + '.no-items' | translate}}
|
{{messagePrefix + '.no-items' | translate}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
|
|
||||||
|
|
||||||
<ds-pagination *ngIf="(ePeopleMembersOfGroupDtos | async)?.totalElements > 0"
|
|
||||||
[paginationOptions]="config"
|
|
||||||
[pageInfoState]="(ePeopleMembersOfGroupDtos | async)"
|
|
||||||
[collectionSize]="(ePeopleMembersOfGroupDtos | async)?.totalElements"
|
|
||||||
[hideGear]="true"
|
|
||||||
[hidePagerWhenSinglePage]="true">
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
|
||||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
|
||||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
|
||||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page">
|
|
||||||
<td class="align-middle">{{ePerson.eperson.id}}</td>
|
|
||||||
<td class="align-middle">
|
|
||||||
<a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
|
||||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
|
|
||||||
{{ dsoNameService.getName(ePerson.eperson) }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="align-middle">
|
|
||||||
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
|
|
||||||
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
|
|
||||||
</td>
|
|
||||||
<td class="align-middle">
|
|
||||||
<div class="btn-group edit-field">
|
|
||||||
<button *ngIf="ePerson.memberOfGroup"
|
|
||||||
(click)="deleteMemberFromGroup(ePerson)"
|
|
||||||
[disabled]="actionConfig.remove.disabled"
|
|
||||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
|
||||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="!ePerson.memberOfGroup"
|
|
||||||
(click)="addMemberToGroup(ePerson)"
|
|
||||||
[disabled]="actionConfig.add.disabled"
|
|
||||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
|
||||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
|
||||||
<i [ngClass]="actionConfig.add.icon"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ds-pagination>
|
|
||||||
|
|
||||||
<div *ngIf="(ePeopleMembersOfGroupDtos | async) == undefined || (ePeopleMembersOfGroupDtos | async)?.totalElements == 0" class="alert alert-info w-100 mb-2"
|
|
||||||
role="alert">
|
|
||||||
{{messagePrefix + '.no-members-yet' | translate}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -17,7 +17,7 @@ import { Group } from '../../../../core/eperson/models/group.model';
|
|||||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
|
import { GroupMock } from '../../../../shared/testing/group-mock';
|
||||||
import { MembersListComponent } from './members-list.component';
|
import { MembersListComponent } from './members-list.component';
|
||||||
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock';
|
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
@@ -42,28 +42,26 @@ describe('MembersListComponent', () => {
|
|||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let activeGroup;
|
let activeGroup;
|
||||||
let allEPersons: EPerson[];
|
|
||||||
let allGroups: Group[];
|
|
||||||
let epersonMembers: EPerson[];
|
let epersonMembers: EPerson[];
|
||||||
let subgroupMembers: Group[];
|
let epersonNonMembers: EPerson[];
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
activeGroup = GroupMock;
|
activeGroup = GroupMock;
|
||||||
epersonMembers = [EPersonMock2];
|
epersonMembers = [EPersonMock2];
|
||||||
subgroupMembers = [GroupMock2];
|
epersonNonMembers = [EPersonMock];
|
||||||
allEPersons = [EPersonMock, EPersonMock2];
|
|
||||||
allGroups = [GroupMock, GroupMock2];
|
|
||||||
ePersonDataServiceStub = {
|
ePersonDataServiceStub = {
|
||||||
activeGroup: activeGroup,
|
activeGroup: activeGroup,
|
||||||
epersonMembers: epersonMembers,
|
epersonMembers: epersonMembers,
|
||||||
subgroupMembers: subgroupMembers,
|
epersonNonMembers: epersonNonMembers,
|
||||||
|
// This method is used to get all the current members
|
||||||
findListByHref(_href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
findListByHref(_href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
||||||
},
|
},
|
||||||
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
// This method is used to search across *non-members*
|
||||||
|
searchNonMembers(query: string, group: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
if (query === '') {
|
if (query === '') {
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allEPersons));
|
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), epersonNonMembers));
|
||||||
}
|
}
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
|
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
|
||||||
},
|
},
|
||||||
@@ -80,22 +78,22 @@ describe('MembersListComponent', () => {
|
|||||||
groupsDataServiceStub = {
|
groupsDataServiceStub = {
|
||||||
activeGroup: activeGroup,
|
activeGroup: activeGroup,
|
||||||
epersonMembers: epersonMembers,
|
epersonMembers: epersonMembers,
|
||||||
subgroupMembers: subgroupMembers,
|
epersonNonMembers: epersonNonMembers,
|
||||||
allGroups: allGroups,
|
|
||||||
getActiveGroup(): Observable<Group> {
|
getActiveGroup(): Observable<Group> {
|
||||||
return observableOf(activeGroup);
|
return observableOf(activeGroup);
|
||||||
},
|
},
|
||||||
getEPersonMembers() {
|
getEPersonMembers() {
|
||||||
return this.epersonMembers;
|
return this.epersonMembers;
|
||||||
},
|
},
|
||||||
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
addMemberToGroup(parentGroup, epersonToAdd: EPerson): Observable<RestResponse> {
|
||||||
if (query === '') {
|
// Add eperson to list of members
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), this.allGroups));
|
this.epersonMembers = [...this.epersonMembers, epersonToAdd];
|
||||||
}
|
// Remove eperson from list of non-members
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
|
this.epersonNonMembers.forEach( (eperson: EPerson, index: number) => {
|
||||||
},
|
if (eperson.id === epersonToAdd.id) {
|
||||||
addMemberToGroup(parentGroup, eperson: EPerson): Observable<RestResponse> {
|
this.epersonNonMembers.splice(index, 1);
|
||||||
this.epersonMembers = [...this.epersonMembers, eperson];
|
}
|
||||||
|
});
|
||||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||||
},
|
},
|
||||||
clearGroupsRequests() {
|
clearGroupsRequests() {
|
||||||
@@ -108,14 +106,14 @@ describe('MembersListComponent', () => {
|
|||||||
return '/access-control/groups/' + group.id;
|
return '/access-control/groups/' + group.id;
|
||||||
},
|
},
|
||||||
deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable<RestResponse> {
|
deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable<RestResponse> {
|
||||||
this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => {
|
// Remove eperson from list of members
|
||||||
if (eperson.id !== epersonToDelete.id) {
|
this.epersonMembers.forEach( (eperson: EPerson, index: number) => {
|
||||||
return eperson;
|
if (eperson.id === epersonToDelete.id) {
|
||||||
|
this.epersonMembers.splice(index, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (this.epersonMembers === undefined) {
|
// Add eperson to list of non-members
|
||||||
this.epersonMembers = [];
|
this.epersonNonMembers = [...this.epersonNonMembers, epersonToDelete];
|
||||||
}
|
|
||||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -168,13 +166,37 @@ describe('MembersListComponent', () => {
|
|||||||
expect(comp).toBeDefined();
|
expect(comp).toBeDefined();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should show list of eperson members of current active group', () => {
|
describe('current members list', () => {
|
||||||
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child'));
|
it('should show list of eperson members of current active group', () => {
|
||||||
expect(epersonIdsFound.length).toEqual(1);
|
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child'));
|
||||||
epersonMembers.map((eperson: EPerson) => {
|
expect(epersonIdsFound.length).toEqual(1);
|
||||||
expect(epersonIdsFound.find((foundEl) => {
|
epersonMembers.map((eperson: EPerson) => {
|
||||||
return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
|
expect(epersonIdsFound.find((foundEl) => {
|
||||||
})).toBeTruthy();
|
return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
|
||||||
|
})).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a delete button next to each member', () => {
|
||||||
|
const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr'));
|
||||||
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
|
expect(addButton).toBeNull();
|
||||||
|
expect(deleteButton).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if first delete button is pressed', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#ePeopleMembersOfGroup tbody .fa-trash-alt'));
|
||||||
|
deleteButton.nativeElement.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('then no ePerson remains as a member of the active group.', () => {
|
||||||
|
const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr'));
|
||||||
|
expect(epersonsFound.length).toEqual(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,76 +204,40 @@ describe('MembersListComponent', () => {
|
|||||||
describe('when searching without query', () => {
|
describe('when searching without query', () => {
|
||||||
let epersonsFound: DebugElement[];
|
let epersonsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
spyOn(component, 'isMemberOfGroup').and.callFake((ePerson: EPerson) => {
|
|
||||||
return observableOf(activeGroup.epersons.includes(ePerson));
|
|
||||||
});
|
|
||||||
component.search({ scope: 'metadata', query: '' });
|
component.search({ scope: 'metadata', query: '' });
|
||||||
tick();
|
tick();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
// Stop using the fake spy function (because otherwise the clicking on the buttons will not change anything
|
|
||||||
// because they don't change the value of activeGroup.epersons)
|
|
||||||
jasmine.getEnv().allowRespy(true);
|
|
||||||
spyOn(component, 'isMemberOfGroup').and.callThrough();
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should display all epersons', () => {
|
it('should display only non-members of the group', () => {
|
||||||
expect(epersonsFound.length).toEqual(2);
|
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr td:first-child'));
|
||||||
|
expect(epersonIdsFound.length).toEqual(1);
|
||||||
|
epersonNonMembers.map((eperson: EPerson) => {
|
||||||
|
expect(epersonIdsFound.find((foundEl) => {
|
||||||
|
return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
|
||||||
|
})).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if eperson is already a eperson', () => {
|
it('should display an add button next to non-members, not a delete button', () => {
|
||||||
it('should have delete button, else it should have add button', () => {
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
const memberIds: string[] = activeGroup.epersons.map((ePerson: EPerson) => ePerson.id);
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
const epersonId: DebugElement = foundEPersonRowElement.query(By.css('td:first-child'));
|
expect(addButton).not.toBeNull();
|
||||||
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
expect(deleteButton).toBeNull();
|
||||||
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
|
||||||
if (memberIds.includes(epersonId.nativeElement.textContent)) {
|
|
||||||
expect(addButton).toBeNull();
|
|
||||||
expect(deleteButton).not.toBeNull();
|
|
||||||
} else {
|
|
||||||
expect(deleteButton).toBeNull();
|
|
||||||
expect(addButton).not.toBeNull();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if first add button is pressed', () => {
|
describe('if first add button is pressed', () => {
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(() => {
|
||||||
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
||||||
addButton.nativeElement.click();
|
addButton.nativeElement.click();
|
||||||
tick();
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
|
||||||
it('then all the ePersons are member of the active group', () => {
|
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
|
||||||
expect(epersonsFound.length).toEqual(2);
|
|
||||||
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
|
||||||
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
|
||||||
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
|
||||||
expect(addButton).toBeNull();
|
|
||||||
expect(deleteButton).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
it('then all (two) ePersons are member of the active group. No non-members left', () => {
|
||||||
|
|
||||||
describe('if first delete button is pressed', () => {
|
|
||||||
beforeEach(fakeAsync(() => {
|
|
||||||
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
|
|
||||||
deleteButton.nativeElement.click();
|
|
||||||
tick();
|
|
||||||
fixture.detectChanges();
|
|
||||||
}));
|
|
||||||
it('then no ePerson is member of the active group', () => {
|
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
expect(epersonsFound.length).toEqual(2);
|
expect(epersonsFound.length).toEqual(0);
|
||||||
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
|
||||||
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
|
||||||
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
|
||||||
expect(deleteButton).toBeNull();
|
|
||||||
expect(addButton).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,28 +4,23 @@ import { Router, RouterLink } from '@angular/router';
|
|||||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
|
||||||
Subscription,
|
Subscription,
|
||||||
BehaviorSubject,
|
BehaviorSubject
|
||||||
combineLatest as observableCombineLatest,
|
|
||||||
ObservedValueOf,
|
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||||
import { Group } from '../../../../core/eperson/models/group.model';
|
import { Group } from '../../../../core/eperson/models/group.model';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteData,
|
|
||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
getAllCompletedRemoteData,
|
getAllCompletedRemoteData,
|
||||||
getRemoteDataPayload
|
getRemoteDataPayload
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
|
|
||||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { ContextHelpDirective } from '../../../../shared/context-help.directive';
|
import { ContextHelpDirective } from '../../../../shared/context-help.directive';
|
||||||
@@ -37,8 +32,8 @@ import { AsyncPipe, NgClass, NgForOf, NgIf } from '@angular/common';
|
|||||||
*/
|
*/
|
||||||
enum SubKey {
|
enum SubKey {
|
||||||
ActiveGroup,
|
ActiveGroup,
|
||||||
MembersDTO,
|
Members,
|
||||||
SearchResultsDTO,
|
SearchResults,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,11 +106,11 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* EPeople being displayed in search result, initially all members, after search result of search
|
* EPeople being displayed in search result, initially all members, after search result of search
|
||||||
*/
|
*/
|
||||||
ePeopleSearchDtos: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>(undefined);
|
ePeopleSearch: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject<PaginatedList<EPerson>>(undefined);
|
||||||
/**
|
/**
|
||||||
* List of EPeople members of currently active group being edited
|
* List of EPeople members of currently active group being edited
|
||||||
*/
|
*/
|
||||||
ePeopleMembersOfGroupDtos: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>(undefined);
|
ePeopleMembersOfGroup: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject<PaginatedList<EPerson>>(undefined);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination config used to display the list of EPeople that are result of EPeople search
|
* Pagination config used to display the list of EPeople that are result of EPeople search
|
||||||
@@ -144,7 +139,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Current search in edit group - epeople search form
|
// Current search in edit group - epeople search form
|
||||||
currentSearchQuery: string;
|
currentSearchQuery: string;
|
||||||
currentSearchScope: string;
|
|
||||||
|
|
||||||
// Whether or not user has done a EPeople search yet
|
// Whether or not user has done a EPeople search yet
|
||||||
searchDone: boolean;
|
searchDone: boolean;
|
||||||
@@ -163,18 +157,17 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
) {
|
) {
|
||||||
this.currentSearchQuery = '';
|
this.currentSearchQuery = '';
|
||||||
this.currentSearchScope = 'metadata';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.searchForm = this.formBuilder.group(({
|
this.searchForm = this.formBuilder.group(({
|
||||||
scope: 'metadata',
|
|
||||||
query: '',
|
query: '',
|
||||||
}));
|
}));
|
||||||
this.subs.set(SubKey.ActiveGroup, this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
|
this.subs.set(SubKey.ActiveGroup, this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
this.groupBeingEdited = activeGroup;
|
this.groupBeingEdited = activeGroup;
|
||||||
this.retrieveMembers(this.config.currentPage);
|
this.retrieveMembers(this.config.currentPage);
|
||||||
|
this.search({query: ''});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -186,8 +179,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
retrieveMembers(page: number): void {
|
retrieveMembers(page: number): void {
|
||||||
this.unsubFrom(SubKey.MembersDTO);
|
this.unsubFrom(SubKey.Members);
|
||||||
this.subs.set(SubKey.MembersDTO,
|
this.subs.set(SubKey.Members,
|
||||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||||
switchMap((currentPagination) => {
|
switchMap((currentPagination) => {
|
||||||
return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, {
|
return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, {
|
||||||
@@ -204,49 +197,12 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
return rd;
|
return rd;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
getRemoteDataPayload())
|
||||||
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
.subscribe((paginatedListOfEPersons: PaginatedList<EPerson>) => {
|
||||||
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
this.ePeopleMembersOfGroup.next(paginatedListOfEPersons);
|
||||||
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
|
||||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
|
||||||
epersonDtoModel.eperson = member;
|
|
||||||
epersonDtoModel.memberOfGroup = isMember;
|
|
||||||
return epersonDtoModel;
|
|
||||||
});
|
|
||||||
return dto$;
|
|
||||||
})]);
|
|
||||||
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
|
||||||
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
|
||||||
}));
|
|
||||||
}))
|
|
||||||
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
|
|
||||||
this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the given ePerson is a member of the group currently being edited
|
|
||||||
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
|
||||||
*/
|
|
||||||
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
|
||||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
|
||||||
mergeMap((group: Group) => {
|
|
||||||
if (group != null) {
|
|
||||||
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
|
|
||||||
currentPage: 1,
|
|
||||||
elementsPerPage: 9999
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
map((listEPeopleInGroup: PaginatedList<EPerson>) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)),
|
|
||||||
map((epeople: EPerson[]) => epeople.length > 0));
|
|
||||||
} else {
|
|
||||||
return observableOf(false);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from a subscription if it's still subscribed, and remove it from the map of
|
* Unsubscribe from a subscription if it's still subscribed, and remove it from the map of
|
||||||
* active subscriptions
|
* active subscriptions
|
||||||
@@ -263,14 +219,18 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a given EPerson from the members list of the group currently being edited
|
* Deletes a given EPerson from the members list of the group currently being edited
|
||||||
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
* @param eperson EPerson we want to delete as member from group that is currently being edited
|
||||||
*/
|
*/
|
||||||
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
|
deleteMemberFromGroup(eperson: EPerson) {
|
||||||
ePerson.memberOfGroup = false;
|
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, eperson);
|
||||||
this.showNotifications('deleteMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup);
|
this.showNotifications('deleteMember', response, this.dsoNameService.getName(eperson), activeGroup);
|
||||||
|
// Reload search results (if there is an active query).
|
||||||
|
// This will potentially add this deleted subgroup into the list of search results.
|
||||||
|
if (this.currentSearchQuery != null) {
|
||||||
|
this.search({query: this.currentSearchQuery});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||||
}
|
}
|
||||||
@@ -279,14 +239,18 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a given EPerson to the members list of the group currently being edited
|
* Adds a given EPerson to the members list of the group currently being edited
|
||||||
* @param ePerson EPerson we want to add as member to group that is currently being edited
|
* @param eperson EPerson we want to add as member to group that is currently being edited
|
||||||
*/
|
*/
|
||||||
addMemberToGroup(ePerson: EpersonDtoModel) {
|
addMemberToGroup(eperson: EPerson) {
|
||||||
ePerson.memberOfGroup = true;
|
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson);
|
const response = this.groupDataService.addMemberToGroup(activeGroup, eperson);
|
||||||
this.showNotifications('addMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup);
|
this.showNotifications('addMember', response, this.dsoNameService.getName(eperson), activeGroup);
|
||||||
|
// Reload search results (if there is an active query).
|
||||||
|
// This will potentially add this deleted subgroup into the list of search results.
|
||||||
|
if (this.currentSearchQuery != null) {
|
||||||
|
this.search({query: this.currentSearchQuery});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||||
}
|
}
|
||||||
@@ -294,37 +258,25 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search in the EPeople by name, email or metadata
|
* Search all EPeople who are NOT a member of the current group by name, email or metadata
|
||||||
* @param data Contains scope and query param
|
* @param data Contains query param
|
||||||
*/
|
*/
|
||||||
search(data: any) {
|
search(data: any) {
|
||||||
this.unsubFrom(SubKey.SearchResultsDTO);
|
this.unsubFrom(SubKey.SearchResults);
|
||||||
this.subs.set(SubKey.SearchResultsDTO,
|
this.subs.set(SubKey.SearchResults,
|
||||||
this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
|
this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
|
||||||
switchMap((paginationOptions) => {
|
switchMap((paginationOptions) => {
|
||||||
|
|
||||||
const query: string = data.query;
|
const query: string = data.query;
|
||||||
const scope: string = data.scope;
|
|
||||||
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
|
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
|
||||||
this.router.navigate([], {
|
|
||||||
queryParamsHandling: 'merge'
|
|
||||||
});
|
|
||||||
this.currentSearchQuery = query;
|
this.currentSearchQuery = query;
|
||||||
this.paginationService.resetPage(this.configSearch.id);
|
this.paginationService.resetPage(this.configSearch.id);
|
||||||
}
|
}
|
||||||
if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) {
|
|
||||||
this.router.navigate([], {
|
|
||||||
queryParamsHandling: 'merge'
|
|
||||||
});
|
|
||||||
this.currentSearchScope = scope;
|
|
||||||
this.paginationService.resetPage(this.configSearch.id);
|
|
||||||
}
|
|
||||||
this.searchDone = true;
|
this.searchDone = true;
|
||||||
|
|
||||||
return this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
return this.ePersonDataService.searchNonMembers(this.currentSearchQuery, this.groupBeingEdited.id, {
|
||||||
currentPage: paginationOptions.currentPage,
|
currentPage: paginationOptions.currentPage,
|
||||||
elementsPerPage: paginationOptions.pageSize
|
elementsPerPage: paginationOptions.pageSize
|
||||||
});
|
}, false, true);
|
||||||
}),
|
}),
|
||||||
getAllCompletedRemoteData(),
|
getAllCompletedRemoteData(),
|
||||||
map((rd: RemoteData<any>) => {
|
map((rd: RemoteData<any>) => {
|
||||||
@@ -334,23 +286,9 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
return rd;
|
return rd;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
getRemoteDataPayload())
|
||||||
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
.subscribe((paginatedListOfEPersons: PaginatedList<EPerson>) => {
|
||||||
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
this.ePeopleSearch.next(paginatedListOfEPersons);
|
||||||
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
|
||||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
|
||||||
epersonDtoModel.eperson = member;
|
|
||||||
epersonDtoModel.memberOfGroup = isMember;
|
|
||||||
return epersonDtoModel;
|
|
||||||
});
|
|
||||||
return dto$;
|
|
||||||
})]);
|
|
||||||
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
|
||||||
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
|
||||||
}));
|
|
||||||
}))
|
|
||||||
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
|
|
||||||
this.ePeopleSearchDtos.next(paginatedListOfDTOs);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,55 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||||
|
|
||||||
|
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
|
||||||
|
|
||||||
|
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
|
||||||
|
[paginationOptions]="config"
|
||||||
|
[pageInfoState]="(subGroups$ | async)?.payload"
|
||||||
|
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
|
||||||
|
[hideGear]="true"
|
||||||
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||||
|
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
|
||||||
|
<td class="align-middle">{{group.id}}</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<a (click)="groupDataService.startEditingNewGroup(group)"
|
||||||
|
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
|
||||||
|
{{ dsoNameService.getName(group) }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload)}}</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<div class="btn-group edit-field">
|
||||||
|
<button (click)="deleteSubgroupFromGroup(group)"
|
||||||
|
class="btn btn-outline-danger btn-sm deleteButton"
|
||||||
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
|
||||||
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</ds-pagination>
|
||||||
|
|
||||||
|
<div *ngIf="(subGroups$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
|
||||||
|
role="alert">
|
||||||
|
{{messagePrefix + '.no-subgroups-yet' | translate}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4 id="search" class="border-bottom pb-2">
|
<h4 id="search" class="border-bottom pb-2">
|
||||||
<span *dsContextHelp="{
|
<span *dsContextHelp="{
|
||||||
content: 'admin.access-control.groups.form.tooltip.editGroup.addSubgroups',
|
content: 'admin.access-control.groups.form.tooltip.editGroup.addSubgroups',
|
||||||
@@ -62,17 +111,7 @@
|
|||||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload) }}</td>
|
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload) }}</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button *ngIf="(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
<button (click)="addSubgroupToGroup(group)"
|
||||||
(click)="deleteSubgroupFromGroup(group)"
|
|
||||||
class="btn btn-outline-danger btn-sm deleteButton"
|
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
|
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</span>
|
|
||||||
|
|
||||||
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
|
||||||
(click)="addSubgroupToGroup(group)"
|
|
||||||
class="btn btn-outline-primary btn-sm addButton"
|
class="btn btn-outline-primary btn-sm addButton"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(group) } }}">
|
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(group) } }}">
|
||||||
<i class="fas fa-plus fa-fw"></i>
|
<i class="fas fa-plus fa-fw"></i>
|
||||||
@@ -90,53 +129,4 @@
|
|||||||
{{messagePrefix + '.no-items' | translate}}
|
{{messagePrefix + '.no-items' | translate}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
|
|
||||||
|
|
||||||
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
|
|
||||||
[paginationOptions]="config"
|
|
||||||
[pageInfoState]="(subGroups$ | async)?.payload"
|
|
||||||
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
|
|
||||||
[hideGear]="true"
|
|
||||||
[hidePagerWhenSinglePage]="true">
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
|
||||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
|
||||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
|
||||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
|
|
||||||
<td class="align-middle">{{group.id}}</td>
|
|
||||||
<td class="align-middle">
|
|
||||||
<a (click)="groupDataService.startEditingNewGroup(group)"
|
|
||||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
|
|
||||||
{{ dsoNameService.getName(group) }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload)}}</td>
|
|
||||||
<td class="align-middle">
|
|
||||||
<div class="btn-group edit-field">
|
|
||||||
<button (click)="deleteSubgroupFromGroup(group)"
|
|
||||||
class="btn btn-outline-danger btn-sm deleteButton"
|
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
|
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</ds-pagination>
|
|
||||||
|
|
||||||
<div *ngIf="(subGroups$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
|
|
||||||
role="alert">
|
|
||||||
{{messagePrefix + '.no-subgroups-yet' | translate}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
|
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, flush, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
import { Observable, of as observableOf, BehaviorSubject } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { RestResponse } from '../../../../core/cache/response.models';
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
@@ -18,15 +18,13 @@ import { NotificationsService } from '../../../../shared/notifications/notificat
|
|||||||
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
|
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
|
||||||
import { SubgroupsListComponent } from './subgroups-list.component';
|
import { SubgroupsListComponent } from './subgroups-list.component';
|
||||||
import {
|
import {
|
||||||
createSuccessfulRemoteDataObject$,
|
createSuccessfulRemoteDataObject$
|
||||||
createSuccessfulRemoteDataObject
|
|
||||||
} from '../../../../shared/remote-data.utils';
|
} from '../../../../shared/remote-data.utils';
|
||||||
import { RouterMock } from '../../../../shared/mocks/router.mock';
|
import { RouterMock } from '../../../../shared/mocks/router.mock';
|
||||||
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||||
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
|
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
|
||||||
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
|
||||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||||
import { map } from 'rxjs/operators';
|
|
||||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||||
@@ -34,6 +32,7 @@ import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mo
|
|||||||
import { ContextHelpDirective } from '../../../../shared/context-help.directive';
|
import { ContextHelpDirective } from '../../../../shared/context-help.directive';
|
||||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||||
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
|
||||||
|
import { EPersonMock2 } from 'src/app/shared/testing/eperson.mock';
|
||||||
|
|
||||||
describe('SubgroupsListComponent', () => {
|
describe('SubgroupsListComponent', () => {
|
||||||
let component: SubgroupsListComponent;
|
let component: SubgroupsListComponent;
|
||||||
@@ -42,57 +41,72 @@ describe('SubgroupsListComponent', () => {
|
|||||||
let builderService: FormBuilderService;
|
let builderService: FormBuilderService;
|
||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let activeGroup;
|
let activeGroup: Group;
|
||||||
let subgroups: Group[];
|
let subgroups: Group[];
|
||||||
let allGroups: Group[];
|
let groupNonMembers: Group[];
|
||||||
let routerStub;
|
let routerStub;
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
// Define a new mock activegroup for all tests below
|
||||||
|
let mockActiveGroup: Group = Object.assign(new Group(), {
|
||||||
|
handle: null,
|
||||||
|
subgroups: [GroupMock2],
|
||||||
|
epersons: [EPersonMock2],
|
||||||
|
selfRegistered: false,
|
||||||
|
permanent: false,
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/server/api/eperson/groups/activegroupid',
|
||||||
|
},
|
||||||
|
subgroups: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/subgroups' },
|
||||||
|
object: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/object' },
|
||||||
|
epersons: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/epersons' }
|
||||||
|
},
|
||||||
|
_name: 'activegroupname',
|
||||||
|
id: 'activegroupid',
|
||||||
|
uuid: 'activegroupid',
|
||||||
|
type: 'group',
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
activeGroup = GroupMock;
|
activeGroup = mockActiveGroup;
|
||||||
subgroups = [GroupMock2];
|
subgroups = [GroupMock2];
|
||||||
allGroups = [GroupMock, GroupMock2];
|
groupNonMembers = [GroupMock];
|
||||||
ePersonDataServiceStub = {};
|
ePersonDataServiceStub = {};
|
||||||
groupsDataServiceStub = {
|
groupsDataServiceStub = {
|
||||||
activeGroup: activeGroup,
|
activeGroup: activeGroup,
|
||||||
subgroups$: new BehaviorSubject(subgroups),
|
subgroups: subgroups,
|
||||||
|
groupNonMembers: groupNonMembers,
|
||||||
getActiveGroup(): Observable<Group> {
|
getActiveGroup(): Observable<Group> {
|
||||||
return observableOf(this.activeGroup);
|
return observableOf(this.activeGroup);
|
||||||
},
|
},
|
||||||
getSubgroups(): Group {
|
getSubgroups(): Group {
|
||||||
return this.activeGroup;
|
return this.subgroups;
|
||||||
},
|
},
|
||||||
findListByHref(
|
// This method is used to get all the current subgroups
|
||||||
_href: string
|
findListByHref(_href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
): Observable<RemoteData<PaginatedList<Group>>> {
|
return createSuccessfulRemoteDataObject$(buildPaginatedList<Group>(new PageInfo(), groupsDataServiceStub.getSubgroups()));
|
||||||
return this.subgroups$.pipe(
|
|
||||||
map((currentGroups: Group[]) => {
|
|
||||||
return createSuccessfulRemoteDataObject(
|
|
||||||
buildPaginatedList<Group>(new PageInfo(), currentGroups)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
getGroupEditPageRouterLink(group: Group): string {
|
getGroupEditPageRouterLink(group: Group): string {
|
||||||
return '/access-control/groups/' + group.id;
|
return '/access-control/groups/' + group.id;
|
||||||
},
|
},
|
||||||
searchGroups(
|
// This method is used to get all groups which are NOT currently a subgroup member
|
||||||
query: string
|
searchNonMemberGroups(query: string, group: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
): Observable<RemoteData<PaginatedList<Group>>> {
|
|
||||||
if (query === '') {
|
if (query === '') {
|
||||||
return createSuccessfulRemoteDataObject$(
|
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupNonMembers));
|
||||||
buildPaginatedList(new PageInfo(), allGroups)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return createSuccessfulRemoteDataObject$(
|
return createSuccessfulRemoteDataObject$(
|
||||||
buildPaginatedList(new PageInfo(), [])
|
buildPaginatedList(new PageInfo(), [])
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
addSubGroupToGroup(
|
addSubGroupToGroup(parentGroup, subgroupToAdd: Group): Observable<RestResponse> {
|
||||||
parentGroup,
|
// Add group to list of subgroups
|
||||||
subgroup: Group
|
this.subgroups = [...this.subgroups, subgroupToAdd];
|
||||||
): Observable<RestResponse> {
|
// Remove group from list of non-members
|
||||||
this.subgroups$.next([...this.subgroups$.getValue(), subgroup]);
|
this.groupNonMembers.forEach( (group: Group, index: number) => {
|
||||||
|
if (group.id === subgroupToAdd.id) {
|
||||||
|
this.groupNonMembers.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||||
},
|
},
|
||||||
clearGroupsRequests() {
|
clearGroupsRequests() {
|
||||||
@@ -101,17 +115,15 @@ describe('SubgroupsListComponent', () => {
|
|||||||
clearGroupLinkRequests() {
|
clearGroupLinkRequests() {
|
||||||
// empty
|
// empty
|
||||||
},
|
},
|
||||||
deleteSubGroupFromGroup(
|
deleteSubGroupFromGroup(parentGroup, subgroupToDelete: Group): Observable<RestResponse> {
|
||||||
parentGroup,
|
// Remove group from list of subgroups
|
||||||
subgroup: Group
|
this.subgroups.forEach( (group: Group, index: number) => {
|
||||||
): Observable<RestResponse> {
|
if (group.id === subgroupToDelete.id) {
|
||||||
this.subgroups$.next(
|
this.subgroups.splice(index, 1);
|
||||||
this.subgroups$.getValue().filter((group: Group) => {
|
}
|
||||||
if (group.id !== subgroup.id) {
|
});
|
||||||
return group;
|
// Add group to list of non-members
|
||||||
}
|
this.groupNonMembers = [...this.groupNonMembers, subgroupToDelete];
|
||||||
})
|
|
||||||
);
|
|
||||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -174,30 +186,38 @@ describe('SubgroupsListComponent', () => {
|
|||||||
expect(comp).toBeDefined();
|
expect(comp).toBeDefined();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should show list of subgroups of current active group', () => {
|
describe('current subgroup list', () => {
|
||||||
const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child'));
|
it('should show list of subgroups of current active group', () => {
|
||||||
expect(groupIdsFound.length).toEqual(1);
|
const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child'));
|
||||||
activeGroup.subgroups.map((group: Group) => {
|
expect(groupIdsFound.length).toEqual(1);
|
||||||
expect(groupIdsFound.find((foundEl) => {
|
subgroups.map((group: Group) => {
|
||||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
expect(groupIdsFound.find((foundEl) => {
|
||||||
})).toBeTruthy();
|
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||||
});
|
})).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
describe('if first group delete button is pressed', () => {
|
|
||||||
let groupsFound: DebugElement[];
|
it('should show a delete button next to each subgroup', () => {
|
||||||
beforeEach(fakeAsync(() => {
|
const subgroupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
|
||||||
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
subgroupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||||
addButton.triggerEventHandler('click', {
|
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
preventDefault: () => {/**/
|
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
}
|
expect(addButton).toBeNull();
|
||||||
|
expect(deleteButton).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if first group delete button is pressed', () => {
|
||||||
|
let groupsFound: DebugElement[];
|
||||||
|
beforeEach(() => {
|
||||||
|
const deleteButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
||||||
|
deleteButton.nativeElement.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('then no subgroup remains as a member of the active group', () => {
|
||||||
|
groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
|
||||||
|
expect(groupsFound.length).toEqual(0);
|
||||||
});
|
});
|
||||||
tick();
|
|
||||||
fixture.detectChanges();
|
|
||||||
}));
|
|
||||||
it('one less subgroup in list from 1 to 0 (of 2 total groups)', () => {
|
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
|
|
||||||
expect(groupsFound.length).toEqual(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,54 +226,38 @@ describe('SubgroupsListComponent', () => {
|
|||||||
let groupsFound: DebugElement[];
|
let groupsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
component.search({ query: '' });
|
component.search({ query: '' });
|
||||||
|
fixture.detectChanges();
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should display all groups', () => {
|
it('should display only non-member groups (i.e. groups that are not a subgroup)', () => {
|
||||||
fixture.detectChanges();
|
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
|
||||||
expect(groupsFound.length).toEqual(2);
|
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
|
||||||
const groupIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
|
const groupIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
|
||||||
allGroups.map((group: Group) => {
|
expect(groupIdsFound.length).toEqual(1);
|
||||||
|
groupNonMembers.map((group: Group) => {
|
||||||
expect(groupIdsFound.find((foundEl: DebugElement) => {
|
expect(groupIdsFound.find((foundEl: DebugElement) => {
|
||||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||||
})).toBeTruthy();
|
})).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if group is already a subgroup', () => {
|
it('should display an add button next to non-member groups, not a delete button', () => {
|
||||||
it('should have delete button, else it should have add button', () => {
|
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||||
|
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
|
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
|
expect(addButton).not.toBeNull();
|
||||||
|
expect(deleteButton).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if first add button is pressed', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const addButton: DebugElement = fixture.debugElement.query(By.css('#groupsSearch tbody .fa-plus'));
|
||||||
|
addButton.nativeElement.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('then all (two) Groups are subgroups of the active group. No non-members left', () => {
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
|
expect(groupsFound.length).toEqual(0);
|
||||||
if (getSubgroups !== undefined && getSubgroups.length > 0) {
|
|
||||||
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
|
||||||
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
|
|
||||||
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
|
||||||
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
|
||||||
expect(addButton).toBeNull();
|
|
||||||
if (activeGroup.id === groupId.nativeElement.textContent) {
|
|
||||||
expect(deleteButton).toBeNull();
|
|
||||||
} else {
|
|
||||||
expect(deleteButton).not.toBeNull();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const subgroupIds: string[] = activeGroup.subgroups.map((group: Group) => group.id);
|
|
||||||
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
|
||||||
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
|
|
||||||
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
|
||||||
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
|
||||||
if (subgroupIds.includes(groupId.nativeElement.textContent)) {
|
|
||||||
expect(addButton).toBeNull();
|
|
||||||
expect(deleteButton).not.toBeNull();
|
|
||||||
} else {
|
|
||||||
expect(deleteButton).toBeNull();
|
|
||||||
expect(addButton).not.toBeNull();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -2,16 +2,15 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
|
import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
|
||||||
import { Router, RouterLink } from '@angular/router';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||||
import { map, mergeMap, switchMap, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||||
import { Group } from '../../../../core/eperson/models/group.model';
|
import { Group } from '../../../../core/eperson/models/group.model';
|
||||||
import {
|
import {
|
||||||
getFirstCompletedRemoteData,
|
getAllCompletedRemoteData,
|
||||||
getFirstSucceededRemoteData,
|
getFirstCompletedRemoteData
|
||||||
getRemoteDataPayload
|
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
@@ -117,6 +116,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
this.groupBeingEdited = activeGroup;
|
this.groupBeingEdited = activeGroup;
|
||||||
this.retrieveSubGroups();
|
this.retrieveSubGroups();
|
||||||
|
this.search({query: ''});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -145,47 +145,6 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the given group is a subgroup of the group currently being edited
|
|
||||||
* @param possibleSubgroup Group that is a possible subgroup (being tested) of the group currently being edited
|
|
||||||
*/
|
|
||||||
isSubgroupOfGroup(possibleSubgroup: Group): Observable<boolean> {
|
|
||||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
|
||||||
mergeMap((activeGroup: Group) => {
|
|
||||||
if (activeGroup != null) {
|
|
||||||
if (activeGroup.uuid === possibleSubgroup.uuid) {
|
|
||||||
return observableOf(false);
|
|
||||||
} else {
|
|
||||||
return this.groupDataService.findListByHref(activeGroup._links.subgroups.href, {
|
|
||||||
currentPage: 1,
|
|
||||||
elementsPerPage: 9999
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
map((listTotalGroups: PaginatedList<Group>) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === possibleSubgroup.id)),
|
|
||||||
map((groups: Group[]) => groups.length > 0));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return observableOf(false);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the given group is the current group being edited
|
|
||||||
* @param group Group that is possibly the current group being edited
|
|
||||||
*/
|
|
||||||
isActiveGroup(group: Group): Observable<boolean> {
|
|
||||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
|
||||||
mergeMap((activeGroup: Group) => {
|
|
||||||
if (activeGroup != null && activeGroup.uuid === group.uuid) {
|
|
||||||
return observableOf(true);
|
|
||||||
}
|
|
||||||
return observableOf(false);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes given subgroup from the group currently being edited
|
* Deletes given subgroup from the group currently being edited
|
||||||
* @param subgroup Group we want to delete from the subgroups of the group currently being edited
|
* @param subgroup Group we want to delete from the subgroups of the group currently being edited
|
||||||
@@ -195,6 +154,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup);
|
const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup);
|
||||||
this.showNotifications('deleteSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
|
this.showNotifications('deleteSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
|
||||||
|
// Reload search results (if there is an active query).
|
||||||
|
// This will potentially add this deleted subgroup into the list of search results.
|
||||||
|
if (this.currentSearchQuery != null) {
|
||||||
|
this.search({query: this.currentSearchQuery});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||||
}
|
}
|
||||||
@@ -211,6 +175,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
if (activeGroup.uuid !== subgroup.uuid) {
|
if (activeGroup.uuid !== subgroup.uuid) {
|
||||||
const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup);
|
const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup);
|
||||||
this.showNotifications('addSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
|
this.showNotifications('addSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
|
||||||
|
// Reload search results (if there is an active query).
|
||||||
|
// This will potentially remove this added subgroup from search results.
|
||||||
|
if (this.currentSearchQuery != null) {
|
||||||
|
this.search({query: this.currentSearchQuery});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup'));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup'));
|
||||||
}
|
}
|
||||||
@@ -221,28 +190,38 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search in the groups (searches by group name and by uuid exact match)
|
* Search all non-member groups (searches by group name and by uuid exact match). Used to search for
|
||||||
|
* groups that could be added to current group as a subgroup.
|
||||||
* @param data Contains query param
|
* @param data Contains query param
|
||||||
*/
|
*/
|
||||||
search(data: any) {
|
search(data: any) {
|
||||||
const query: string = data.query;
|
|
||||||
if (query != null && this.currentSearchQuery !== query) {
|
|
||||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
|
|
||||||
this.currentSearchQuery = query;
|
|
||||||
this.configSearch.currentPage = 1;
|
|
||||||
}
|
|
||||||
this.searchDone = true;
|
|
||||||
|
|
||||||
this.unsubFrom(SubKey.SearchResults);
|
this.unsubFrom(SubKey.SearchResults);
|
||||||
this.subs.set(SubKey.SearchResults, this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
|
this.subs.set(SubKey.SearchResults,
|
||||||
switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, {
|
this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
|
||||||
currentPage: config.currentPage,
|
switchMap((paginationOptions) => {
|
||||||
elementsPerPage: config.pageSize
|
const query: string = data.query;
|
||||||
}, true, true, followLink('object')
|
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
|
||||||
))
|
this.currentSearchQuery = query;
|
||||||
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
this.paginationService.resetPage(this.configSearch.id);
|
||||||
this.searchResults$.next(rd);
|
}
|
||||||
}));
|
this.searchDone = true;
|
||||||
|
|
||||||
|
return this.groupDataService.searchNonMemberGroups(this.currentSearchQuery, this.groupBeingEdited.id, {
|
||||||
|
currentPage: paginationOptions.currentPage,
|
||||||
|
elementsPerPage: paginationOptions.pageSize
|
||||||
|
}, false, true, followLink('object'));
|
||||||
|
}),
|
||||||
|
getAllCompletedRemoteData(),
|
||||||
|
map((rd: RemoteData<any>) => {
|
||||||
|
if (rd.hasFailed) {
|
||||||
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
|
||||||
|
} else {
|
||||||
|
return rd;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
||||||
|
this.searchResults$.next(rd);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<h2 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h2>
|
<h2 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h2>
|
||||||
<div>
|
<div>
|
||||||
<button class="mr-auto btn btn-success"
|
<button class="mr-auto btn btn-success"
|
||||||
[routerLink]="['newGroup']">
|
[routerLink]="'create'">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -234,18 +234,28 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the members (epersons embedded value of a group)
|
* Get the members (epersons embedded value of a group)
|
||||||
|
* NOTE: At this time we only grab the *first* member in order to receive the `totalElements` value
|
||||||
|
* needed for our HTML template.
|
||||||
* @param group
|
* @param group
|
||||||
*/
|
*/
|
||||||
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return this.ePersonDataService.findListByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
|
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: 1,
|
||||||
|
}).pipe(getFirstSucceededRemoteData());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the subgroups (groups embedded value of a group)
|
* Get the subgroups (groups embedded value of a group)
|
||||||
|
* NOTE: At this time we only grab the *first* subgroup in order to receive the `totalElements` value
|
||||||
|
* needed for our HTML template.
|
||||||
* @param group
|
* @param group
|
||||||
*/
|
*/
|
||||||
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
return this.groupService.findListByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
|
return this.groupService.findListByHref(group._links.subgroups.href, {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: 1,
|
||||||
|
}).pipe(getFirstSucceededRemoteData());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -3,7 +3,8 @@ import {
|
|||||||
DynamicFormControlModel,
|
DynamicFormControlModel,
|
||||||
DynamicFormGroupModel,
|
DynamicFormGroupModel,
|
||||||
DynamicFormLayout,
|
DynamicFormLayout,
|
||||||
DynamicInputModel
|
DynamicInputModel,
|
||||||
|
DynamicTextAreaModel
|
||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
import { UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
@@ -60,7 +61,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* A dynamic input model for the scopeNote field
|
* A dynamic input model for the scopeNote field
|
||||||
*/
|
*/
|
||||||
scopeNote: DynamicInputModel;
|
scopeNote: DynamicTextAreaModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all dynamic input models
|
* A list of all dynamic input models
|
||||||
@@ -141,11 +142,12 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
maxLength: 'error.validation.metadata.qualifier.max-length',
|
maxLength: 'error.validation.metadata.qualifier.max-length',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.scopeNote = new DynamicInputModel({
|
this.scopeNote = new DynamicTextAreaModel({
|
||||||
id: 'scopeNote',
|
id: 'scopeNote',
|
||||||
label: scopenote,
|
label: scopenote,
|
||||||
name: 'scopeNote',
|
name: 'scopeNote',
|
||||||
required: false,
|
required: false,
|
||||||
|
rows: 5,
|
||||||
});
|
});
|
||||||
this.formModel = [
|
this.formModel = [
|
||||||
new DynamicFormGroupModel(
|
new DynamicFormGroupModel(
|
||||||
|
@@ -41,7 +41,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier" class="mb-0">.</label>{{field.qualifier}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@@ -14,12 +14,12 @@ import { NgClass } from '@angular/common';
|
|||||||
* Represents a non-expandable section in the admin sidebar
|
* Represents a non-expandable section in the admin sidebar
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
/* eslint-disable @angular-eslint/component-selector */
|
selector: 'ds-admin-sidebar-section',
|
||||||
selector: 'li[ds-admin-sidebar-section]',
|
templateUrl: './admin-sidebar-section.component.html',
|
||||||
templateUrl: './admin-sidebar-section.component.html',
|
styleUrls: ['./admin-sidebar-section.component.scss'],
|
||||||
styleUrls: ['./admin-sidebar-section.component.scss'],
|
standalone: true,
|
||||||
standalone: true,
|
imports: [NgClass, RouterLink, TranslateModule]
|
||||||
imports: [NgClass, RouterLink, TranslateModule]
|
|
||||||
})
|
})
|
||||||
@rendersSectionForMenu(MenuID.ADMIN, false)
|
@rendersSectionForMenu(MenuID.ADMIN, false)
|
||||||
export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit {
|
export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit {
|
||||||
|
@@ -26,10 +26,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<ng-container *ngFor="let section of (sections | async)">
|
<li *ngFor="let section of (sections | async)">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
</ng-container>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-nav">
|
<div class="navbar-nav">
|
||||||
|
@@ -17,13 +17,12 @@ import { NgClass, NgComponentOutlet, NgIf, NgFor, AsyncPipe } from '@angular/com
|
|||||||
* Represents a expandable section in the sidebar
|
* Represents a expandable section in the sidebar
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
/* eslint-disable @angular-eslint/component-selector */
|
selector: 'ds-expandable-admin-sidebar-section',
|
||||||
selector: 'li[ds-expandable-admin-sidebar-section]',
|
templateUrl: './expandable-admin-sidebar-section.component.html',
|
||||||
templateUrl: './expandable-admin-sidebar-section.component.html',
|
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||||
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
animations: [rotate, slide, bgColor],
|
||||||
animations: [rotate, slide, bgColor],
|
standalone: true,
|
||||||
standalone: true,
|
imports: [NgClass, NgComponentOutlet, NgIf, NgFor, AsyncPipe, TranslateModule]
|
||||||
imports: [NgClass, NgComponentOutlet, NgIf, NgFor, AsyncPipe, TranslateModule]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@rendersSectionForMenu(MenuID.ADMIN, true)
|
@rendersSectionForMenu(MenuID.ADMIN, true)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<ng-container *ngVar="(breadcrumbs$ | async) as breadcrumbs">
|
<ng-container *ngVar="(breadcrumbs$ | async) as breadcrumbs">
|
||||||
<nav *ngIf="(showBreadcrumbs$ | async)" aria-label="breadcrumb" class="nav-breadcrumb">
|
<nav *ngIf="(showBreadcrumbs$ | async)" aria-label="breadcrumb" class="nav-breadcrumb">
|
||||||
<ol class="container breadcrumb">
|
<ol class="container breadcrumb my-0">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'home.breadcrumbs', url: '/'}"></ng-container>
|
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'home.breadcrumbs', url: '/'}"></ng-container>
|
||||||
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
||||||
|
@@ -4,9 +4,8 @@
|
|||||||
|
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin-top: calc(-1 * var(--ds-content-spacing));
|
padding-bottom: calc(var(--ds-content-spacing) / 2);
|
||||||
padding-bottom: var(--ds-content-spacing / 3);
|
padding-top: calc(var(--ds-content-spacing) / 2);
|
||||||
padding-top: var(--ds-content-spacing / 3);
|
|
||||||
background-color: var(--ds-breadcrumb-bg);
|
background-color: var(--ds-breadcrumb-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -34,6 +34,7 @@ import {
|
|||||||
import { BrowseByComponent } from '../../shared/browse-by/browse-by.component';
|
import { BrowseByComponent } from '../../shared/browse-by/browse-by.component';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||||
|
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-date-page',
|
selector: 'ds-browse-by-date-page',
|
||||||
@@ -52,7 +53,8 @@ import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.comp
|
|||||||
ThemedComcolPageBrowseByComponent,
|
ThemedComcolPageBrowseByComponent,
|
||||||
BrowseByComponent,
|
BrowseByComponent,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
ThemedLoadingComponent
|
ThemedLoadingComponent,
|
||||||
|
ThemedBrowseByComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
@@ -119,11 +121,11 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
const lastItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC);
|
const lastItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC);
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => {
|
observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => {
|
||||||
let lowerLimit = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
|
let lowerLimit: number = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
|
||||||
let upperLimit = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
|
let upperLimit: number = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
|
||||||
const options = [];
|
const options: number[] = [];
|
||||||
const oneYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
const oneYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
||||||
const fiveYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
const fiveYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
||||||
if (lowerLimit <= fiveYearBreak) {
|
if (lowerLimit <= fiveYearBreak) {
|
||||||
lowerLimit -= 10;
|
lowerLimit -= 10;
|
||||||
} else if (lowerLimit <= oneYearBreak) {
|
} else if (lowerLimit <= oneYearBreak) {
|
||||||
@@ -131,7 +133,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
} else {
|
} else {
|
||||||
lowerLimit -= 1;
|
lowerLimit -= 1;
|
||||||
}
|
}
|
||||||
let i = upperLimit;
|
let i: number = upperLimit;
|
||||||
while (i > lowerLimit) {
|
while (i > lowerLimit) {
|
||||||
options.push(i);
|
options.push(i);
|
||||||
if (i <= fiveYearBreak) {
|
if (i <= fiveYearBreak) {
|
||||||
|
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
<div class="browse-by-metadata w-100">
|
<div class="browse-by-metadata w-100">
|
||||||
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
<ds-themed-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
||||||
title="{{'browse.title' | translate:
|
title="{{'browse.title' | translate:
|
||||||
{
|
{
|
||||||
collection: dsoNameService.getName((parent$ | async)?.payload),
|
collection: dsoNameService.getName((parent$ | async)?.payload),
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
[startsWithOptions]="startsWithOptions"
|
[startsWithOptions]="startsWithOptions"
|
||||||
(prev)="goPrev()"
|
(prev)="goPrev()"
|
||||||
(next)="goNext()">
|
(next)="goNext()">
|
||||||
</ds-browse-by>
|
</ds-themed-browse-by>
|
||||||
<ds-themed-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-themed-loading>
|
<ds-themed-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-themed-loading>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -37,6 +37,7 @@ import {
|
|||||||
import { BrowseByComponent } from '../../shared/browse-by/browse-by.component';
|
import { BrowseByComponent } from '../../shared/browse-by/browse-by.component';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||||
|
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
|
||||||
|
|
||||||
export const BBM_PAGINATION_ID = 'bbm';
|
export const BBM_PAGINATION_ID = 'bbm';
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ export const BBM_PAGINATION_ID = 'bbm';
|
|||||||
ThemedComcolPageHandleComponent,
|
ThemedComcolPageHandleComponent,
|
||||||
ComcolPageContentComponent,
|
ComcolPageContentComponent,
|
||||||
DsoEditMenuComponent,
|
DsoEditMenuComponent,
|
||||||
|
ThemedBrowseByComponent,
|
||||||
ThemedComcolPageBrowseByComponent,
|
ThemedComcolPageBrowseByComponent,
|
||||||
BrowseByComponent,
|
BrowseByComponent,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
@@ -191,7 +193,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
|||||||
this.value = '';
|
this.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof params.startsWith === 'string'){
|
if (params.startsWith === undefined || params.startsWith === '') {
|
||||||
|
this.startsWith = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params.startsWith === 'string'){
|
||||||
this.startsWith = params.startsWith.trim();
|
this.startsWith = params.startsWith.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -29,6 +29,7 @@ import {
|
|||||||
import { BrowseByComponent } from '../../shared/browse-by/browse-by.component';
|
import { BrowseByComponent } from '../../shared/browse-by/browse-by.component';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||||
|
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-title-page',
|
selector: 'ds-browse-by-title-page',
|
||||||
@@ -47,7 +48,8 @@ import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.comp
|
|||||||
ThemedComcolPageBrowseByComponent,
|
ThemedComcolPageBrowseByComponent,
|
||||||
BrowseByComponent,
|
BrowseByComponent,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
ThemedLoadingComponent
|
ThemedLoadingComponent,
|
||||||
|
ThemedBrowseByComponent
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
|
@@ -14,6 +14,7 @@ import { ThemedBrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/t
|
|||||||
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
||||||
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
||||||
import { FormModule } from '../shared/form/form.module';
|
import { FormModule } from '../shared/form/form.module';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
@@ -37,6 +38,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
FormModule,
|
FormModule,
|
||||||
BrowseBySwitcherComponent,
|
BrowseBySwitcherComponent,
|
||||||
ThemedBrowseBySwitcherComponent,
|
ThemedBrowseBySwitcherComponent,
|
||||||
|
SharedModule,
|
||||||
...ENTRY_COMPONENTS
|
...ENTRY_COMPONENTS
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
@@ -34,9 +34,6 @@
|
|||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</header>
|
</header>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
<div class="pl-2 space-children-mr">
|
|
||||||
<ds-dso-page-subscription-button [dso]="collection"></ds-dso-page-subscription-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
<!-- Browse-By Links -->
|
<!-- Browse-By Links -->
|
||||||
|
@@ -44,9 +44,6 @@ import {
|
|||||||
ThemedComcolPageHandleComponent
|
ThemedComcolPageHandleComponent
|
||||||
} from '../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
|
} from '../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
|
||||||
import { DsoEditMenuComponent } from '../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
import { DsoEditMenuComponent } from '../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
||||||
import {
|
|
||||||
DsoPageSubscriptionButtonComponent
|
|
||||||
} from '../shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component';
|
|
||||||
import {
|
import {
|
||||||
ThemedComcolPageBrowseByComponent
|
ThemedComcolPageBrowseByComponent
|
||||||
} from '../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
|
} from '../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
|
||||||
@@ -74,7 +71,6 @@ import { ObjectCollectionComponent } from '../shared/object-collection/object-co
|
|||||||
ComcolPageLogoComponent,
|
ComcolPageLogoComponent,
|
||||||
ThemedComcolPageHandleComponent,
|
ThemedComcolPageHandleComponent,
|
||||||
DsoEditMenuComponent,
|
DsoEditMenuComponent,
|
||||||
DsoPageSubscriptionButtonComponent,
|
|
||||||
ThemedComcolPageBrowseByComponent,
|
ThemedComcolPageBrowseByComponent,
|
||||||
ObjectCollectionComponent
|
ObjectCollectionComponent
|
||||||
],
|
],
|
||||||
|
@@ -24,6 +24,7 @@ import { FlatNode } from './flat-node.model';
|
|||||||
import { ShowMoreFlatNode } from './show-more-flat-node.model';
|
import { ShowMoreFlatNode } from './show-more-flat-node.model';
|
||||||
import { FindListOptions } from '../core/data/find-list-options.model';
|
import { FindListOptions } from '../core/data/find-list-options.model';
|
||||||
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
// Helper method to combine and flatten an array of observables of flatNode arrays
|
// Helper method to combine and flatten an array of observables of flatNode arrays
|
||||||
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
|
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
|
||||||
@@ -186,7 +187,7 @@ export class CommunityListService {
|
|||||||
return this.transformCommunity(community, level, parent, expandedNodes);
|
return this.transformCommunity(community, level, parent, expandedNodes);
|
||||||
});
|
});
|
||||||
if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) {
|
if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) {
|
||||||
obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])];
|
obsList = [...obsList, observableOf([showMoreFlatNode(`community-${uuidv4()}`, level, parent)])];
|
||||||
}
|
}
|
||||||
|
|
||||||
return combineAndFlatten(obsList);
|
return combineAndFlatten(obsList);
|
||||||
@@ -257,7 +258,7 @@ export class CommunityListService {
|
|||||||
let nodes = rd.payload.page
|
let nodes = rd.payload.page
|
||||||
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
|
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
|
||||||
if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
|
if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
|
||||||
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
|
nodes = [...nodes, showMoreFlatNode(`collection-${uuidv4()}`, level + 1, communityFlatNode)];
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<span class="fa fa-chevron-right invisible" aria-hidden="true"></span>
|
<span class="fa fa-chevron-right invisible" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="align-middle pt-2">
|
<div class="align-middle pt-2">
|
||||||
<button *ngIf="node!==loadingNode" (click)="getNextPage(node)"
|
<button *ngIf="!(dataSource.loading$ | async)" (click)="getNextPage(node)"
|
||||||
class="btn btn-outline-primary btn-sm" role="button">
|
class="btn btn-outline-primary btn-sm" role="button">
|
||||||
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
|
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -20,6 +20,7 @@ import { RouterLinkWithHref } from '@angular/router';
|
|||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||||
import { TruncatableComponent } from '../../shared/truncatable/truncatable.component';
|
import { TruncatableComponent } from '../../shared/truncatable/truncatable.component';
|
||||||
import { TruncatablePartComponent } from '../../shared/truncatable/truncatable-part/truncatable-part.component';
|
import { TruncatablePartComponent } from '../../shared/truncatable/truncatable-part/truncatable-part.component';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
describe('CommunityListComponent', () => {
|
describe('CommunityListComponent', () => {
|
||||||
let component: CommunityListComponent;
|
let component: CommunityListComponent;
|
||||||
@@ -141,7 +142,7 @@ describe('CommunityListComponent', () => {
|
|||||||
}
|
}
|
||||||
if (expandedNodes === null || isEmpty(expandedNodes)) {
|
if (expandedNodes === null || isEmpty(expandedNodes)) {
|
||||||
if (showMoreTopComNode) {
|
if (showMoreTopComNode) {
|
||||||
return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode('community', 0, null)]);
|
return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode(`community-${uuidv4()}`, 0, null)]);
|
||||||
} else {
|
} else {
|
||||||
return observableOf(mockTopFlatnodesUnexpanded.slice(0, endPageIndex));
|
return observableOf(mockTopFlatnodesUnexpanded.slice(0, endPageIndex));
|
||||||
}
|
}
|
||||||
@@ -168,21 +169,21 @@ describe('CommunityListComponent', () => {
|
|||||||
const endSubComIndex = this.pageSize * expandedParent.currentCommunityPage;
|
const endSubComIndex = this.pageSize * expandedParent.currentCommunityPage;
|
||||||
flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)];
|
flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)];
|
||||||
if (subComFlatnodes.length > endSubComIndex) {
|
if (subComFlatnodes.length > endSubComIndex) {
|
||||||
flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)];
|
flatnodes = [...flatnodes, showMoreFlatNode(`community-${uuidv4()}`, topNode.level + 1, expandedParent)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isNotEmpty(collFlatnodes)) {
|
if (isNotEmpty(collFlatnodes)) {
|
||||||
const endColIndex = this.pageSize * expandedParent.currentCollectionPage;
|
const endColIndex = this.pageSize * expandedParent.currentCollectionPage;
|
||||||
flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)];
|
flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)];
|
||||||
if (collFlatnodes.length > endColIndex) {
|
if (collFlatnodes.length > endColIndex) {
|
||||||
flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)];
|
flatnodes = [...flatnodes, showMoreFlatNode(`collection-${uuidv4()}`, topNode.level + 1, expandedParent)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (showMoreTopComNode) {
|
if (showMoreTopComNode) {
|
||||||
flatnodes = [...flatnodes, showMoreFlatNode('community', 0, null)];
|
flatnodes = [...flatnodes, showMoreFlatNode(`community-${uuidv4()}`, 0, null)];
|
||||||
}
|
}
|
||||||
return observableOf(flatnodes);
|
return observableOf(flatnodes);
|
||||||
}
|
}
|
||||||
|
@@ -92,7 +92,7 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
toggleExpanded(node: FlatNode) {
|
toggleExpanded(node: FlatNode) {
|
||||||
this.loadingNode = node;
|
this.loadingNode = node;
|
||||||
if (node.isExpanded) {
|
if (node.isExpanded) {
|
||||||
this.expandedNodes = this.expandedNodes.filter((node2) => node2.name !== node.name);
|
this.expandedNodes = this.expandedNodes.filter((node2) => node2.id !== node.id);
|
||||||
node.isExpanded = false;
|
node.isExpanded = false;
|
||||||
} else {
|
} else {
|
||||||
this.expandedNodes.push(node);
|
this.expandedNodes.push(node);
|
||||||
@@ -119,19 +119,18 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
getNextPage(node: FlatNode): void {
|
getNextPage(node: FlatNode): void {
|
||||||
this.loadingNode = node;
|
this.loadingNode = node;
|
||||||
if (node.parent != null) {
|
if (node.parent != null) {
|
||||||
if (node.id === 'collection') {
|
if (node.id.startsWith('collection')) {
|
||||||
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
||||||
parentNodeInExpandedNodes.currentCollectionPage++;
|
parentNodeInExpandedNodes.currentCollectionPage++;
|
||||||
}
|
}
|
||||||
if (node.id === 'community') {
|
if (node.id.startsWith('community')) {
|
||||||
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
||||||
parentNodeInExpandedNodes.currentCommunityPage++;
|
parentNodeInExpandedNodes.currentCommunityPage++;
|
||||||
}
|
}
|
||||||
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
|
||||||
} else {
|
} else {
|
||||||
this.paginationConfig.currentPage++;
|
this.paginationConfig.currentPage++;
|
||||||
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
|
||||||
}
|
}
|
||||||
|
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -21,9 +21,6 @@
|
|||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</header>
|
</header>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
<div class="pl-2 space-children-mr">
|
|
||||||
<ds-dso-page-subscription-button [dso]="communityPayload"></ds-dso-page-subscription-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
|
|
||||||
|
@@ -35,9 +35,6 @@ import {
|
|||||||
ThemedComcolPageBrowseByComponent
|
ThemedComcolPageBrowseByComponent
|
||||||
} from '../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
|
} from '../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
|
||||||
import { DsoEditMenuComponent } from '../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
import { DsoEditMenuComponent } from '../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
||||||
import {
|
|
||||||
DsoPageSubscriptionButtonComponent
|
|
||||||
} from '../shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component';
|
|
||||||
import {
|
import {
|
||||||
ThemedComcolPageHandleComponent
|
ThemedComcolPageHandleComponent
|
||||||
} from '../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
|
} from '../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
|
||||||
@@ -62,7 +59,6 @@ import { VarDirective } from '../shared/utils/var.directive';
|
|||||||
ThemedCollectionPageSubCollectionListComponent,
|
ThemedCollectionPageSubCollectionListComponent,
|
||||||
ThemedComcolPageBrowseByComponent,
|
ThemedComcolPageBrowseByComponent,
|
||||||
DsoEditMenuComponent,
|
DsoEditMenuComponent,
|
||||||
DsoPageSubscriptionButtonComponent,
|
|
||||||
ThemedComcolPageHandleComponent,
|
ThemedComcolPageHandleComponent,
|
||||||
ComcolPageLogoComponent,
|
ComcolPageLogoComponent,
|
||||||
ComcolPageHeaderComponent,
|
ComcolPageHeaderComponent,
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Collection } from '../../core/shared/collection.model';
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
@@ -66,6 +66,8 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro
|
|||||||
*/
|
*/
|
||||||
subCollectionsRDObs: BehaviorSubject<RemoteData<PaginatedList<Collection>>> = new BehaviorSubject<RemoteData<PaginatedList<Collection>>>({} as any);
|
subCollectionsRDObs: BehaviorSubject<RemoteData<PaginatedList<Collection>>> = new BehaviorSubject<RemoteData<PaginatedList<Collection>>>({} as any);
|
||||||
|
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected cds: CollectionDataService,
|
protected cds: CollectionDataService,
|
||||||
protected paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
@@ -93,7 +95,7 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro
|
|||||||
const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config);
|
const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config);
|
||||||
const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig);
|
const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig);
|
||||||
|
|
||||||
observableCombineLatest([pagination$, sort$]).pipe(
|
this.subscriptions.push(observableCombineLatest([pagination$, sort$]).pipe(
|
||||||
switchMap(([currentPagination, currentSort]) => {
|
switchMap(([currentPagination, currentSort]) => {
|
||||||
return this.cds.findByParent(this.community.id, {
|
return this.cds.findByParent(this.community.id, {
|
||||||
currentPage: currentPagination.currentPage,
|
currentPage: currentPagination.currentPage,
|
||||||
@@ -103,11 +105,12 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro
|
|||||||
})
|
})
|
||||||
).subscribe((results) => {
|
).subscribe((results) => {
|
||||||
this.subCollectionsRDObs.next(results);
|
this.subCollectionsRDObs.next(results);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.config.id);
|
this.paginationService.clearPagination(this.config?.id);
|
||||||
|
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
@@ -68,6 +68,8 @@ export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy
|
|||||||
*/
|
*/
|
||||||
subCommunitiesRDObs: BehaviorSubject<RemoteData<PaginatedList<Community>>> = new BehaviorSubject<RemoteData<PaginatedList<Community>>>({} as any);
|
subCommunitiesRDObs: BehaviorSubject<RemoteData<PaginatedList<Community>>> = new BehaviorSubject<RemoteData<PaginatedList<Community>>>({} as any);
|
||||||
|
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected cds: CommunityDataService,
|
protected cds: CommunityDataService,
|
||||||
protected paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
@@ -95,7 +97,7 @@ export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy
|
|||||||
const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config);
|
const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config);
|
||||||
const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig);
|
const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig);
|
||||||
|
|
||||||
observableCombineLatest([pagination$, sort$]).pipe(
|
this.subscriptions.push(observableCombineLatest([pagination$, sort$]).pipe(
|
||||||
switchMap(([currentPagination, currentSort]) => {
|
switchMap(([currentPagination, currentSort]) => {
|
||||||
return this.cds.findByParent(this.community.id, {
|
return this.cds.findByParent(this.community.id, {
|
||||||
currentPage: currentPagination.currentPage,
|
currentPage: currentPagination.currentPage,
|
||||||
@@ -105,11 +107,12 @@ export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy
|
|||||||
})
|
})
|
||||||
).subscribe((results) => {
|
).subscribe((results) => {
|
||||||
this.subCommunitiesRDObs.next(results);
|
this.subCommunitiesRDObs.next(results);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.config.id);
|
this.paginationService.clearPagination(this.config?.id);
|
||||||
|
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
|
|||||||
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
||||||
import { RouteEffects } from './services/route.effects';
|
import { RouteEffects } from './services/route.effects';
|
||||||
import { RouterEffects } from './router/router.effects';
|
import { RouterEffects } from './router/router.effects';
|
||||||
|
import { MenuEffects } from '../shared/menu/menu.effects';
|
||||||
|
|
||||||
export const coreEffects = [
|
export const coreEffects = [
|
||||||
RequestEffects,
|
RequestEffects,
|
||||||
@@ -18,4 +19,5 @@ export const coreEffects = [
|
|||||||
ObjectUpdatesEffects,
|
ObjectUpdatesEffects,
|
||||||
RouteEffects,
|
RouteEffects,
|
||||||
RouterEffects,
|
RouterEffects,
|
||||||
|
MenuEffects,
|
||||||
];
|
];
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
@@ -11,6 +11,8 @@ import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils
|
|||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { EMBED_SEPARATOR } from './base/base-data.service';
|
import { EMBED_SEPARATOR } from './base/base-data.service';
|
||||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
|
import { environment } from '../../../environments/environment.test';
|
||||||
|
import { AppConfig } from '../../../config/app-config.interface';
|
||||||
|
|
||||||
describe('DsoRedirectService', () => {
|
describe('DsoRedirectService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -56,6 +58,7 @@ describe('DsoRedirectService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
service = new DsoRedirectService(
|
service = new DsoRedirectService(
|
||||||
|
environment as AppConfig,
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache,
|
||||||
@@ -107,7 +110,7 @@ describe('DsoRedirectService', () => {
|
|||||||
redir.subscribe();
|
redir.subscribe();
|
||||||
scheduler.schedule(() => redir);
|
scheduler.schedule(() => redir);
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
expect(redirectService.redirect).toHaveBeenCalledWith('/items/' + remoteData.payload.uuid, 301);
|
expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/items/${remoteData.payload.uuid}`, 301);
|
||||||
});
|
});
|
||||||
it('should navigate to entities route with the corresponding entity type', () => {
|
it('should navigate to entities route with the corresponding entity type', () => {
|
||||||
remoteData.payload.type = 'item';
|
remoteData.payload.type = 'item';
|
||||||
@@ -124,7 +127,7 @@ describe('DsoRedirectService', () => {
|
|||||||
redir.subscribe();
|
redir.subscribe();
|
||||||
scheduler.schedule(() => redir);
|
scheduler.schedule(() => redir);
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
expect(redirectService.redirect).toHaveBeenCalledWith('/entities/publication/' + remoteData.payload.uuid, 301);
|
expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/entities/publication/${remoteData.payload.uuid}`, 301);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to collections route', () => {
|
it('should navigate to collections route', () => {
|
||||||
@@ -133,7 +136,7 @@ describe('DsoRedirectService', () => {
|
|||||||
redir.subscribe();
|
redir.subscribe();
|
||||||
scheduler.schedule(() => redir);
|
scheduler.schedule(() => redir);
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
expect(redirectService.redirect).toHaveBeenCalledWith('/collections/' + remoteData.payload.uuid, 301);
|
expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/collections/${remoteData.payload.uuid}`, 301);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to communities route', () => {
|
it('should navigate to communities route', () => {
|
||||||
@@ -142,7 +145,7 @@ describe('DsoRedirectService', () => {
|
|||||||
redir.subscribe();
|
redir.subscribe();
|
||||||
scheduler.schedule(() => redir);
|
scheduler.schedule(() => redir);
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
expect(redirectService.redirect).toHaveBeenCalledWith('/communities/' + remoteData.payload.uuid, 301);
|
expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/communities/${remoteData.payload.uuid}`, 301);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
* http://www.dspace.org/license/
|
* http://www.dspace.org/license/
|
||||||
*/
|
*/
|
||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable, Inject } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
@@ -21,6 +21,7 @@ import { DSpaceObject } from '../shared/dspace-object.model';
|
|||||||
import { IdentifiableDataService } from './base/identifiable-data.service';
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
import { getDSORoute } from '../../app-routing-paths';
|
import { getDSORoute } from '../../app-routing-paths';
|
||||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
|
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||||
|
|
||||||
const ID_ENDPOINT = 'pid';
|
const ID_ENDPOINT = 'pid';
|
||||||
const UUID_ENDPOINT = 'dso';
|
const UUID_ENDPOINT = 'dso';
|
||||||
@@ -70,6 +71,7 @@ export class DsoRedirectService {
|
|||||||
private dataService: DsoByIdOrUUIDDataService;
|
private dataService: DsoByIdOrUUIDDataService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
@@ -98,7 +100,7 @@ export class DsoRedirectService {
|
|||||||
let newRoute = getDSORoute(dso);
|
let newRoute = getDSORoute(dso);
|
||||||
if (hasValue(newRoute)) {
|
if (hasValue(newRoute)) {
|
||||||
// Use a "301 Moved Permanently" redirect for SEO purposes
|
// Use a "301 Moved Permanently" redirect for SEO purposes
|
||||||
this.hardRedirectService.redirect(newRoute, 301);
|
this.hardRedirectService.redirect(this.appConfig.ui.nameSpace.replace(/\/$/, '') + newRoute, 301);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -71,7 +71,7 @@ export class AuthorizationDataService extends BaseDataService<Authorization> imp
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(() => observableOf(false)),
|
catchError(() => observableOf([])),
|
||||||
oneAuthorizationMatchesFeature(featureId)
|
oneAuthorizationMatchesFeature(featureId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -68,13 +68,13 @@ export const oneAuthorizationMatchesFeature = (featureID: FeatureID) =>
|
|||||||
source.pipe(
|
source.pipe(
|
||||||
switchMap((authorizations: Authorization[]) => {
|
switchMap((authorizations: Authorization[]) => {
|
||||||
if (isNotEmpty(authorizations)) {
|
if (isNotEmpty(authorizations)) {
|
||||||
return observableCombineLatest(
|
return observableCombineLatest([
|
||||||
...authorizations
|
...authorizations
|
||||||
.filter((authorization: Authorization) => hasValue(authorization.feature))
|
.filter((authorization: Authorization) => hasValue(authorization.feature))
|
||||||
.map((authorization: Authorization) => authorization.feature.pipe(
|
.map((authorization: Authorization) => authorization.feature.pipe(
|
||||||
getFirstSucceededRemoteDataPayload()
|
getFirstSucceededRemoteDataPayload()
|
||||||
))
|
))
|
||||||
);
|
]);
|
||||||
} else {
|
} else {
|
||||||
return observableOf([]);
|
return observableOf([]);
|
||||||
}
|
}
|
||||||
|
@@ -639,6 +639,45 @@ describe('RequestService', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setStaleByHref', () => {
|
||||||
|
const uuid = 'c574a42c-4818-47ac-bbe1-6c3cd622c81f';
|
||||||
|
const href = 'https://rest.api/some/object';
|
||||||
|
const freshRE: any = {
|
||||||
|
request: { uuid, href },
|
||||||
|
state: RequestEntryState.Success
|
||||||
|
};
|
||||||
|
const staleRE: any = {
|
||||||
|
request: { uuid, href },
|
||||||
|
state: RequestEntryState.SuccessStale
|
||||||
|
};
|
||||||
|
|
||||||
|
it(`should call getByHref to retrieve the RequestEntry matching the href`, () => {
|
||||||
|
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
|
||||||
|
service.setStaleByHref(href);
|
||||||
|
expect(service.getByHref).toHaveBeenCalledWith(href);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should dispatch a RequestStaleAction for the RequestEntry returned by getByHref`, (done: DoneFn) => {
|
||||||
|
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
|
||||||
|
spyOn(store, 'dispatch');
|
||||||
|
service.setStaleByHref(href).subscribe(() => {
|
||||||
|
const requestStaleAction = new RequestStaleAction(uuid);
|
||||||
|
requestStaleAction.lastUpdated = jasmine.any(Number) as any;
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(requestStaleAction);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should emit true when the request in the store is stale`, () => {
|
||||||
|
spyOn(service, 'getByHref').and.returnValue(cold('a-b', {
|
||||||
|
a: freshRE,
|
||||||
|
b: staleRE
|
||||||
|
}));
|
||||||
|
const result$ = service.setStaleByHref(href);
|
||||||
|
expect(result$).toBeObservable(cold('--(c|)', { c: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('setStaleByHrefSubstring', () => {
|
describe('setStaleByHrefSubstring', () => {
|
||||||
let dispatchSpy: jasmine.Spy;
|
let dispatchSpy: jasmine.Spy;
|
||||||
let getByUUIDSpy: jasmine.Spy;
|
let getByUUIDSpy: jasmine.Spy;
|
||||||
|
@@ -16,7 +16,7 @@ import {
|
|||||||
RequestExecuteAction,
|
RequestExecuteAction,
|
||||||
RequestStaleAction
|
RequestStaleAction
|
||||||
} from './request.actions';
|
} from './request.actions';
|
||||||
import { GetRequest} from './request.models';
|
import { GetRequest } from './request.models';
|
||||||
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
import { coreSelector } from '../core.selectors';
|
import { coreSelector } from '../core.selectors';
|
||||||
@@ -351,7 +351,29 @@ export class RequestService {
|
|||||||
map((request: RequestEntry) => isStale(request.state)),
|
map((request: RequestEntry) => isStale(request.state)),
|
||||||
filter((stale: boolean) => stale),
|
filter((stale: boolean) => stale),
|
||||||
take(1),
|
take(1),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a request as stale
|
||||||
|
* @param href the href of the request
|
||||||
|
* @return an Observable that will emit true once the Request becomes stale
|
||||||
|
*/
|
||||||
|
setStaleByHref(href: string): Observable<boolean> {
|
||||||
|
const requestEntry$ = this.getByHref(href);
|
||||||
|
|
||||||
|
requestEntry$.pipe(
|
||||||
|
map((re: RequestEntry) => re.request.uuid),
|
||||||
|
take(1),
|
||||||
|
).subscribe((uuid: string) => {
|
||||||
|
this.store.dispatch(new RequestStaleAction(uuid));
|
||||||
|
});
|
||||||
|
|
||||||
|
return requestEntry$.pipe(
|
||||||
|
map((request: RequestEntry) => isStale(request.state)),
|
||||||
|
filter((stale: boolean) => stale),
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -364,10 +386,10 @@ export class RequestService {
|
|||||||
// if it's not a GET request
|
// if it's not a GET request
|
||||||
if (request.method !== RestRequestMethod.GET) {
|
if (request.method !== RestRequestMethod.GET) {
|
||||||
return true;
|
return true;
|
||||||
// if it is a GET request, check it isn't pending
|
// if it is a GET request, check it isn't pending
|
||||||
} else if (this.isPending(request)) {
|
} else if (this.isPending(request)) {
|
||||||
return false;
|
return false;
|
||||||
// if it is pending, check if we're allowed to use a cached version
|
// if it is pending, check if we're allowed to use a cached version
|
||||||
} else if (!useCachedVersionIfAvailable) {
|
} else if (!useCachedVersionIfAvailable) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
import { RootDataService } from './root-data.service';
|
import { RootDataService } from './root-data.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import {
|
||||||
import { Observable, of } from 'rxjs';
|
createSuccessfulRemoteDataObject$,
|
||||||
|
createFailedRemoteDataObject$
|
||||||
|
} from '../../shared/remote-data.utils';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { Root } from './root.model';
|
import { Root } from './root.model';
|
||||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
|
||||||
import { cold } from 'jasmine-marbles';
|
import { cold } from 'jasmine-marbles';
|
||||||
|
|
||||||
describe('RootDataService', () => {
|
describe('RootDataService', () => {
|
||||||
let service: RootDataService;
|
let service: RootDataService;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let restService;
|
let requestService;
|
||||||
let rootEndpoint;
|
let rootEndpoint;
|
||||||
let findByHrefSpy;
|
let findByHrefSpy;
|
||||||
|
|
||||||
@@ -19,10 +21,10 @@ describe('RootDataService', () => {
|
|||||||
halService = jasmine.createSpyObj('halService', {
|
halService = jasmine.createSpyObj('halService', {
|
||||||
getRootHref: rootEndpoint,
|
getRootHref: rootEndpoint,
|
||||||
});
|
});
|
||||||
restService = jasmine.createSpyObj('halService', {
|
requestService = jasmine.createSpyObj('requestService', [
|
||||||
get: jasmine.createSpy('get'),
|
'setStaleByHref',
|
||||||
});
|
]);
|
||||||
service = new RootDataService(null, null, null, halService, restService);
|
service = new RootDataService(requestService, null, null, halService);
|
||||||
|
|
||||||
findByHrefSpy = spyOn(service as any, 'findByHref');
|
findByHrefSpy = spyOn(service as any, 'findByHref');
|
||||||
findByHrefSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
|
findByHrefSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||||
@@ -47,12 +49,8 @@ describe('RootDataService', () => {
|
|||||||
let result$: Observable<boolean>;
|
let result$: Observable<boolean>;
|
||||||
|
|
||||||
it('should return observable of true when root endpoint is available', () => {
|
it('should return observable of true when root endpoint is available', () => {
|
||||||
const mockResponse = {
|
spyOn(service, 'findRoot').and.returnValue(createSuccessfulRemoteDataObject$<Root>({} as any));
|
||||||
statusCode: 200,
|
|
||||||
statusText: 'OK'
|
|
||||||
} as RawRestResponse;
|
|
||||||
|
|
||||||
restService.get.and.returnValue(of(mockResponse));
|
|
||||||
result$ = service.checkServerAvailability();
|
result$ = service.checkServerAvailability();
|
||||||
|
|
||||||
expect(result$).toBeObservable(cold('(a|)', {
|
expect(result$).toBeObservable(cold('(a|)', {
|
||||||
@@ -61,12 +59,8 @@ describe('RootDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return observable of false when root endpoint is not available', () => {
|
it('should return observable of false when root endpoint is not available', () => {
|
||||||
const mockResponse = {
|
spyOn(service, 'findRoot').and.returnValue(createFailedRemoteDataObject$<Root>('500'));
|
||||||
statusCode: 500,
|
|
||||||
statusText: 'Internal Server Error'
|
|
||||||
} as RawRestResponse;
|
|
||||||
|
|
||||||
restService.get.and.returnValue(of(mockResponse));
|
|
||||||
result$ = service.checkServerAvailability();
|
result$ = service.checkServerAvailability();
|
||||||
|
|
||||||
expect(result$).toBeObservable(cold('(a|)', {
|
expect(result$).toBeObservable(cold('(a|)', {
|
||||||
@@ -75,4 +69,12 @@ describe('RootDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(`invalidateRootCache`, () => {
|
||||||
|
it(`should set the cached root request to stale`, () => {
|
||||||
|
service.invalidateRootCache();
|
||||||
|
expect(halService.getRootHref).toHaveBeenCalled();
|
||||||
|
expect(requestService.setStaleByHref).toHaveBeenCalledWith(rootEndpoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -6,11 +6,10 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
|
||||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
|
||||||
import { catchError, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { BaseDataService } from './base/base-data.service';
|
import { BaseDataService } from './base/base-data.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve the {@link Root} object from the REST API.
|
* A service to retrieve the {@link Root} object from the REST API.
|
||||||
@@ -22,7 +21,6 @@ export class RootDataService extends BaseDataService<Root> {
|
|||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected restService: DspaceRestService,
|
|
||||||
) {
|
) {
|
||||||
super('', requestService, rdbService, objectCache, halService, 6 * 60 * 60 * 1000);
|
super('', requestService, rdbService, objectCache, halService, 6 * 60 * 60 * 1000);
|
||||||
}
|
}
|
||||||
@@ -31,12 +29,13 @@ export class RootDataService extends BaseDataService<Root> {
|
|||||||
* Check if root endpoint is available
|
* Check if root endpoint is available
|
||||||
*/
|
*/
|
||||||
checkServerAvailability(): Observable<boolean> {
|
checkServerAvailability(): Observable<boolean> {
|
||||||
return this.restService.get(this.halService.getRootHref()).pipe(
|
return this.findRoot().pipe(
|
||||||
catchError((err ) => {
|
catchError((err ) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return observableOf(false);
|
return observableOf(false);
|
||||||
}),
|
}),
|
||||||
map((res: RawRestResponse) => res.statusCode === 200)
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rootRd: RemoteData<Root>) => rootRd.statusCode === 200)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +56,6 @@ export class RootDataService extends BaseDataService<Root> {
|
|||||||
* Set to sale the root endpoint cache hit
|
* Set to sale the root endpoint cache hit
|
||||||
*/
|
*/
|
||||||
invalidateRootCache() {
|
invalidateRootCache() {
|
||||||
this.requestService.setStaleByHrefSubstring(this.halService.getRootHref());
|
this.requestService.setStaleByHref(this.halService.getRootHref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,7 @@ import {
|
|||||||
EPeopleRegistryCancelEPersonAction,
|
EPeopleRegistryCancelEPersonAction,
|
||||||
EPeopleRegistryEditEPersonAction
|
EPeopleRegistryEditEPersonAction
|
||||||
} from '../../access-control/epeople-registry/epeople-registry.actions';
|
} from '../../access-control/epeople-registry/epeople-registry.actions';
|
||||||
|
import { GroupMock } from '../../shared/testing/group-mock';
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { ChangeAnalyzer } from '../data/change-analyzer';
|
import { ChangeAnalyzer } from '../data/change-analyzer';
|
||||||
import { PatchRequest, PostRequest } from '../data/request.models';
|
import { PatchRequest, PostRequest } from '../data/request.models';
|
||||||
@@ -139,6 +140,30 @@ describe('EPersonDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('searchNonMembers', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'searchBy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('search with empty query and a group ID', () => {
|
||||||
|
service.searchNonMembers('', GroupMock.id);
|
||||||
|
const options = Object.assign(new FindListOptions(), {
|
||||||
|
searchParams: [Object.assign(new RequestParam('query', '')),
|
||||||
|
Object.assign(new RequestParam('group', GroupMock.id))]
|
||||||
|
});
|
||||||
|
expect(service.searchBy).toHaveBeenCalledWith('isNotMemberOf', options, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('search with query and a group ID', () => {
|
||||||
|
service.searchNonMembers('test', GroupMock.id);
|
||||||
|
const options = Object.assign(new FindListOptions(), {
|
||||||
|
searchParams: [Object.assign(new RequestParam('query', 'test')),
|
||||||
|
Object.assign(new RequestParam('group', GroupMock.id))]
|
||||||
|
});
|
||||||
|
expect(service.searchBy).toHaveBeenCalledWith('isNotMemberOf', options, true, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('updateEPerson', () => {
|
describe('updateEPerson', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMock));
|
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMock));
|
||||||
|
@@ -32,6 +32,7 @@ import { SearchData, SearchDataImpl } from '../data/base/search-data';
|
|||||||
import { PatchData, PatchDataImpl } from '../data/base/patch-data';
|
import { PatchData, PatchDataImpl } from '../data/base/patch-data';
|
||||||
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { getEPersonEditRoute, getEPersonsRoute } from '../../access-control/access-control-routing-paths';
|
||||||
|
|
||||||
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
|
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
|
||||||
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
|
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
|
||||||
@@ -173,6 +174,34 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
|
|||||||
return this.searchBy(searchMethod, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.searchBy(searchMethod, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for all EPerons which are *not* a member of a given group, via a passed in query
|
||||||
|
* (searches all EPerson metadata and by exact UUID).
|
||||||
|
* Endpoint used: /eperson/epesons/search/isNotMemberOf?query=<:string>&group=<:uuid>
|
||||||
|
* @param query search query param
|
||||||
|
* @param group UUID of group to exclude results from. Members of this group will never be returned.
|
||||||
|
* @param options
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
public searchNonMembers(query: string, group: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
|
const searchParams = [new RequestParam('query', query), new RequestParam('group', group)];
|
||||||
|
let findListOptions = new FindListOptions();
|
||||||
|
if (options) {
|
||||||
|
findListOptions = Object.assign(new FindListOptions(), options);
|
||||||
|
}
|
||||||
|
if (findListOptions.searchParams) {
|
||||||
|
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
|
||||||
|
} else {
|
||||||
|
findListOptions.searchParams = searchParams;
|
||||||
|
}
|
||||||
|
return this.searchBy('isNotMemberOf', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new patch to the object cache
|
* Add a new patch to the object cache
|
||||||
* The patch is derived from the differences between the given object and its version in the object cache
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
@@ -278,15 +307,14 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
|
|||||||
this.editEPerson(ePerson);
|
this.editEPerson(ePerson);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return '/access-control/epeople';
|
return getEPersonEditRoute(ePerson.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get EPeople admin page
|
* Get EPeople admin page
|
||||||
* @param ePerson New EPerson to edit
|
|
||||||
*/
|
*/
|
||||||
public getEPeoplePageRouterLink(): string {
|
public getEPeoplePageRouterLink(): string {
|
||||||
return '/access-control/epeople';
|
return getEPersonsRoute();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -43,11 +43,11 @@ describe('GroupDataService', () => {
|
|||||||
let rdbService;
|
let rdbService;
|
||||||
let objectCache;
|
let objectCache;
|
||||||
function init() {
|
function init() {
|
||||||
restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson';
|
restEndpointURL = 'https://rest.api/server/api/eperson';
|
||||||
groupsEndpoint = `${restEndpointURL}/groups`;
|
groupsEndpoint = `${restEndpointURL}/groups`;
|
||||||
groups = [GroupMock, GroupMock2];
|
groups = [GroupMock, GroupMock2];
|
||||||
groups$ = createSuccessfulRemoteDataObject$(createPaginatedList(groups));
|
groups$ = createSuccessfulRemoteDataObject$(createPaginatedList(groups));
|
||||||
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ });
|
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://rest.api/server/api/eperson/groups': groups$ });
|
||||||
halService = new HALEndpointServiceStub(restEndpointURL);
|
halService = new HALEndpointServiceStub(restEndpointURL);
|
||||||
objectCache = getMockObjectCacheService();
|
objectCache = getMockObjectCacheService();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -110,6 +110,30 @@ describe('GroupDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('searchNonMemberGroups', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'searchBy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('search with empty query and a group ID', () => {
|
||||||
|
service.searchNonMemberGroups('', GroupMock.id);
|
||||||
|
const options = Object.assign(new FindListOptions(), {
|
||||||
|
searchParams: [Object.assign(new RequestParam('query', '')),
|
||||||
|
Object.assign(new RequestParam('group', GroupMock.id))]
|
||||||
|
});
|
||||||
|
expect(service.searchBy).toHaveBeenCalledWith('isNotMemberOf', options, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('search with query and a group ID', () => {
|
||||||
|
service.searchNonMemberGroups('test', GroupMock.id);
|
||||||
|
const options = Object.assign(new FindListOptions(), {
|
||||||
|
searchParams: [Object.assign(new RequestParam('query', 'test')),
|
||||||
|
Object.assign(new RequestParam('group', GroupMock.id))]
|
||||||
|
});
|
||||||
|
expect(service.searchBy).toHaveBeenCalledWith('isNotMemberOf', options, true, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('addSubGroupToGroup', () => {
|
describe('addSubGroupToGroup', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
objectCache.getByHref.and.returnValue(observableOf({
|
objectCache.getByHref.and.returnValue(observableOf({
|
||||||
|
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { createSelector, select, Store } from '@ngrx/store';
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
import { Observable, zip as observableZip } from 'rxjs';
|
import { Observable, zip as observableZip } from 'rxjs';
|
||||||
import { filter, map, take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
GroupRegistryCancelGroupAction,
|
GroupRegistryCancelGroupAction,
|
||||||
GroupRegistryEditGroupAction
|
GroupRegistryEditGroupAction
|
||||||
@@ -38,6 +38,7 @@ import { PatchData, PatchDataImpl } from '../data/base/patch-data';
|
|||||||
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { getGroupEditRoute } from '../../access-control/access-control-routing-paths';
|
||||||
|
|
||||||
const groupRegistryStateSelector = (state: AppState) => state.groupRegistry;
|
const groupRegistryStateSelector = (state: AppState) => state.groupRegistry;
|
||||||
const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup);
|
const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup);
|
||||||
@@ -101,23 +102,31 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the current user is member of to the indicated group
|
* Searches for all groups which are *not* a member of a given group, via a passed in query
|
||||||
*
|
* (searches in group name and by exact UUID).
|
||||||
* @param groupName
|
* Endpoint used: /eperson/groups/search/isNotMemberOf?query=<:string>&group=<:uuid>
|
||||||
* the group name
|
* @param query search query param
|
||||||
* @return boolean
|
* @param group UUID of group to exclude results from. Members of this group will never be returned.
|
||||||
* true if user is member of the indicated group, false otherwise
|
* @param options
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
isMemberOf(groupName: string): Observable<boolean> {
|
public searchNonMemberGroups(query: string, group: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Group>[]): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
const searchHref = 'isMemberOf';
|
const searchParams = [new RequestParam('query', query), new RequestParam('group', group)];
|
||||||
const options = new FindListOptions();
|
let findListOptions = new FindListOptions();
|
||||||
options.searchParams = [new RequestParam('groupName', groupName)];
|
if (options) {
|
||||||
|
findListOptions = Object.assign(new FindListOptions(), options);
|
||||||
return this.searchBy(searchHref, options).pipe(
|
}
|
||||||
filter((groups: RemoteData<PaginatedList<Group>>) => !groups.isResponsePending),
|
if (findListOptions.searchParams) {
|
||||||
take(1),
|
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
|
||||||
map((groups: RemoteData<PaginatedList<Group>>) => groups.payload.totalElements > 0)
|
} else {
|
||||||
);
|
findListOptions.searchParams = searchParams;
|
||||||
|
}
|
||||||
|
return this.searchBy('isNotMemberOf', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -261,15 +270,15 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
|||||||
* @param group Group we want edit page for
|
* @param group Group we want edit page for
|
||||||
*/
|
*/
|
||||||
public getGroupEditPageRouterLink(group: Group): string {
|
public getGroupEditPageRouterLink(group: Group): string {
|
||||||
return this.getGroupEditPageRouterLinkWithID(group.id);
|
return getGroupEditRoute(group.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Edit page of group
|
* Get Edit page of group
|
||||||
* @param groupID Group ID we want edit page for
|
* @param groupID Group ID we want edit page for
|
||||||
*/
|
*/
|
||||||
public getGroupEditPageRouterLinkWithID(groupId: string): string {
|
public getGroupEditPageRouterLinkWithID(groupID: string): string {
|
||||||
return '/access-control/groups/' + groupId;
|
return getGroupEditRoute(groupID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -13,9 +13,4 @@ export class EpersonDtoModel {
|
|||||||
* Whether or not the linked EPerson is able to be deleted
|
* Whether or not the linked EPerson is able to be deleted
|
||||||
*/
|
*/
|
||||||
public ableToDelete: boolean;
|
public ableToDelete: boolean;
|
||||||
/**
|
|
||||||
* Whether or not this EPerson is member of group on page it is being used on
|
|
||||||
*/
|
|
||||||
public memberOfGroup: boolean;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,68 +1,86 @@
|
|||||||
import { ServerCheckGuard } from './server-check.guard';
|
import { ServerCheckGuard } from './server-check.guard';
|
||||||
import { Router } from '@angular/router';
|
import { Router, NavigationStart, UrlTree, NavigationEnd, RouterEvent } from '@angular/router';
|
||||||
|
|
||||||
import { of } from 'rxjs';
|
import { of, ReplaySubject } from 'rxjs';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
|
||||||
import { RootDataService } from '../data/root-data.service';
|
import { RootDataService } from '../data/root-data.service';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import SpyObj = jasmine.SpyObj;
|
import SpyObj = jasmine.SpyObj;
|
||||||
|
|
||||||
describe('ServerCheckGuard', () => {
|
describe('ServerCheckGuard', () => {
|
||||||
let guard: ServerCheckGuard;
|
let guard: ServerCheckGuard;
|
||||||
let router: SpyObj<Router>;
|
let router: Router;
|
||||||
|
const eventSubject = new ReplaySubject<RouterEvent>(1);
|
||||||
let rootDataServiceStub: SpyObj<RootDataService>;
|
let rootDataServiceStub: SpyObj<RootDataService>;
|
||||||
|
let testScheduler: TestScheduler;
|
||||||
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
|
let redirectUrlTree: UrlTree;
|
||||||
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
|
|
||||||
invalidateRootCache: jasmine.createSpy('invalidateRootCache')
|
|
||||||
});
|
|
||||||
router = jasmine.createSpyObj('Router', {
|
|
||||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
|
||||||
|
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
|
||||||
|
invalidateRootCache: jasmine.createSpy('invalidateRootCache'),
|
||||||
|
findRoot: jasmine.createSpy('findRoot')
|
||||||
|
});
|
||||||
|
redirectUrlTree = new UrlTree();
|
||||||
|
router = {
|
||||||
|
events: eventSubject.asObservable(),
|
||||||
|
navigateByUrl: jasmine.createSpy('navigateByUrl'),
|
||||||
|
parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree)
|
||||||
|
} as any;
|
||||||
guard = new ServerCheckGuard(router, rootDataServiceStub);
|
guard = new ServerCheckGuard(router, rootDataServiceStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
router.navigateByUrl.calls.reset();
|
|
||||||
rootDataServiceStub.invalidateRootCache.calls.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
expect(guard).toBeTruthy();
|
expect(guard).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when root endpoint has succeeded', () => {
|
describe('when root endpoint request has succeeded', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rootDataServiceStub.checkServerAvailability.and.returnValue(of(true));
|
rootDataServiceStub.checkServerAvailability.and.returnValue(of(true));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not redirect to error page', () => {
|
it('should return true', () => {
|
||||||
guard.canActivateChild({} as any, {} as any).pipe(
|
testScheduler.run(({ expectObservable }) => {
|
||||||
take(1)
|
const result$ = guard.canActivateChild({} as any, {} as any);
|
||||||
).subscribe((canActivate: boolean) => {
|
expectObservable(result$).toBe('(a|)', { a: true });
|
||||||
expect(canActivate).toEqual(true);
|
|
||||||
expect(rootDataServiceStub.invalidateRootCache).not.toHaveBeenCalled();
|
|
||||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when root endpoint has not succeeded', () => {
|
describe('when root endpoint request has not succeeded', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rootDataServiceStub.checkServerAvailability.and.returnValue(of(false));
|
rootDataServiceStub.checkServerAvailability.and.returnValue(of(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to error page', () => {
|
it('should return a UrlTree with the route to the 500 error page', () => {
|
||||||
guard.canActivateChild({} as any, {} as any).pipe(
|
testScheduler.run(({ expectObservable }) => {
|
||||||
take(1)
|
const result$ = guard.canActivateChild({} as any, {} as any);
|
||||||
).subscribe((canActivate: boolean) => {
|
expectObservable(result$).toBe('(b|)', { b: redirectUrlTree });
|
||||||
expect(canActivate).toEqual(false);
|
|
||||||
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalled();
|
|
||||||
expect(router.navigateByUrl).toHaveBeenCalledWith(getPageInternalServerErrorRoute());
|
|
||||||
});
|
});
|
||||||
|
expect(router.parseUrl).toHaveBeenCalledWith('/500');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`listenForRouteChanges`, () => {
|
||||||
|
it(`should retrieve the root endpoint, without using the cache, when the method is first called`, () => {
|
||||||
|
testScheduler.run(() => {
|
||||||
|
guard.listenForRouteChanges();
|
||||||
|
expect(rootDataServiceStub.findRoot).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should invalidate the root cache on every NavigationStart event`, () => {
|
||||||
|
testScheduler.run(() => {
|
||||||
|
guard.listenForRouteChanges();
|
||||||
|
eventSubject.next(new NavigationStart(1,''));
|
||||||
|
eventSubject.next(new NavigationEnd(1,'', ''));
|
||||||
|
eventSubject.next(new NavigationStart(2,''));
|
||||||
|
eventSubject.next(new NavigationEnd(2,'', ''));
|
||||||
|
eventSubject.next(new NavigationStart(3,''));
|
||||||
|
});
|
||||||
|
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,15 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateChild,
|
||||||
|
Router,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
UrlTree,
|
||||||
|
NavigationStart
|
||||||
|
} from '@angular/router';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { take, tap } from 'rxjs/operators';
|
import { take, map, filter } from 'rxjs/operators';
|
||||||
|
|
||||||
import { RootDataService } from '../data/root-data.service';
|
import { RootDataService } from '../data/root-data.service';
|
||||||
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
||||||
@@ -23,17 +30,38 @@ export class ServerCheckGuard implements CanActivateChild {
|
|||||||
*/
|
*/
|
||||||
canActivateChild(
|
canActivateChild(
|
||||||
route: ActivatedRouteSnapshot,
|
route: ActivatedRouteSnapshot,
|
||||||
state: RouterStateSnapshot): Observable<boolean> {
|
state: RouterStateSnapshot
|
||||||
|
): Observable<boolean | UrlTree> {
|
||||||
|
|
||||||
return this.rootDataService.checkServerAvailability().pipe(
|
return this.rootDataService.checkServerAvailability().pipe(
|
||||||
take(1),
|
take(1),
|
||||||
tap((isAvailable: boolean) => {
|
map((isAvailable: boolean) => {
|
||||||
if (!isAvailable) {
|
if (!isAvailable) {
|
||||||
this.rootDataService.invalidateRootCache();
|
return this.router.parseUrl(getPageInternalServerErrorRoute());
|
||||||
this.router.navigateByUrl(getPageInternalServerErrorRoute());
|
} else {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen to all router events. Every time a new navigation starts, invalidate the cache
|
||||||
|
* for the root endpoint. That way we retrieve it once per routing operation to ensure the
|
||||||
|
* backend is not down. But if the guard is called multiple times during the same routing
|
||||||
|
* operation, the cached version is used.
|
||||||
|
*/
|
||||||
|
listenForRouteChanges(): void {
|
||||||
|
// we'll always be too late for the first NavigationStart event with the router subscribe below,
|
||||||
|
// so this statement is for the very first route operation. A `find` without using the cache,
|
||||||
|
// rather than an invalidateRootCache, because invalidating as the app is bootstrapping can
|
||||||
|
// break other features
|
||||||
|
this.rootDataService.findRoot(false);
|
||||||
|
|
||||||
|
this.router.events.pipe(
|
||||||
|
filter(event => event instanceof NavigationStart),
|
||||||
|
).subscribe(() => {
|
||||||
|
this.rootDataService.invalidateRootCache();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync, fakeAsync, flush } from '@angular/core/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { CurationFormComponent } from './curation-form.component';
|
import { CurationFormComponent } from './curation-form.component';
|
||||||
@@ -16,6 +16,7 @@ import { ConfigurationDataService } from '../core/data/configuration-data.servic
|
|||||||
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||||
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths';
|
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths';
|
||||||
import { HandleService } from '../shared/handle.service';
|
import { HandleService } from '../shared/handle.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
describe('CurationFormComponent', () => {
|
describe('CurationFormComponent', () => {
|
||||||
let comp: CurationFormComponent;
|
let comp: CurationFormComponent;
|
||||||
@@ -54,7 +55,7 @@ describe('CurationFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleService = {
|
handleService = {
|
||||||
normalizeHandle: (a) => a
|
normalizeHandle: (a: string) => observableOf(a),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
notificationsService = new NotificationsServiceStub();
|
notificationsService = new NotificationsServiceStub();
|
||||||
@@ -150,12 +151,13 @@ describe('CurationFormComponent', () => {
|
|||||||
], []);
|
], []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should show an error notification and return when an invalid dsoHandle is provided`, () => {
|
it(`should show an error notification and return when an invalid dsoHandle is provided`, fakeAsync(() => {
|
||||||
comp.dsoHandle = 'test-handle';
|
comp.dsoHandle = 'test-handle';
|
||||||
spyOn(handleService, 'normalizeHandle').and.returnValue(null);
|
spyOn(handleService, 'normalizeHandle').and.returnValue(observableOf(null));
|
||||||
comp.submit();
|
comp.submit();
|
||||||
|
flush();
|
||||||
|
|
||||||
expect(notificationsService.error).toHaveBeenCalled();
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
expect(scriptDataService.invoke).not.toHaveBeenCalled();
|
expect(scriptDataService.invoke).not.toHaveBeenCalled();
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
@@ -1,23 +1,23 @@
|
|||||||
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ScriptDataService } from '../core/data/processes/script-data.service';
|
import { ScriptDataService } from '../core/data/processes/script-data.service';
|
||||||
import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
|
||||||
import { find, map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
import { TranslateService, TranslateModule } from '@ngx-translate/core';
|
import { TranslateService, TranslateModule } from '@ngx-translate/core';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { ProcessDataService } from '../core/data/processes/process-data.service';
|
|
||||||
import { Process } from '../process-page/processes/process.model';
|
import { Process } from '../process-page/processes/process.model';
|
||||||
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||||
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths';
|
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths';
|
||||||
import { HandleService } from '../shared/handle.service';
|
import { HandleService } from '../shared/handle.service';
|
||||||
import { NgFor, NgIf } from '@angular/common';
|
import { NgFor, NgIf } from '@angular/common';
|
||||||
|
|
||||||
export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask';
|
export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component responsible for rendering the Curation Task form
|
* Component responsible for rendering the Curation Task form
|
||||||
*/
|
*/
|
||||||
@@ -27,7 +27,7 @@ export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [FormsModule, ReactiveFormsModule, NgFor, NgIf, TranslateModule]
|
imports: [FormsModule, ReactiveFormsModule, NgFor, NgIf, TranslateModule]
|
||||||
})
|
})
|
||||||
export class CurationFormComponent implements OnInit {
|
export class CurationFormComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
config: Observable<RemoteData<ConfigurationProperty>>;
|
config: Observable<RemoteData<ConfigurationProperty>>;
|
||||||
tasks: string[];
|
tasks: string[];
|
||||||
@@ -36,10 +36,11 @@ export class CurationFormComponent implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
dsoHandle: string;
|
dsoHandle: string;
|
||||||
|
|
||||||
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private scriptDataService: ScriptDataService,
|
private scriptDataService: ScriptDataService,
|
||||||
private configurationDataService: ConfigurationDataService,
|
private configurationDataService: ConfigurationDataService,
|
||||||
private processDataService: ProcessDataService,
|
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private handleService: HandleService,
|
private handleService: HandleService,
|
||||||
@@ -48,6 +49,10 @@ export class CurationFormComponent implements OnInit {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach((sub: Subscription) => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.form = new UntypedFormGroup({
|
this.form = new UntypedFormGroup({
|
||||||
task: new UntypedFormControl(''),
|
task: new UntypedFormControl(''),
|
||||||
@@ -55,16 +60,15 @@ export class CurationFormComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.config = this.configurationDataService.findByPropertyName(CURATION_CFG);
|
this.config = this.configurationDataService.findByPropertyName(CURATION_CFG);
|
||||||
this.config.pipe(
|
this.subs.push(this.config.pipe(
|
||||||
find((rd: RemoteData<ConfigurationProperty>) => rd.hasSucceeded),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
map((rd: RemoteData<ConfigurationProperty>) => rd.payload)
|
).subscribe((configProperties: ConfigurationProperty) => {
|
||||||
).subscribe((configProperties) => {
|
|
||||||
this.tasks = configProperties.values
|
this.tasks = configProperties.values
|
||||||
.filter((value) => isNotEmpty(value) && value.includes('='))
|
.filter((value) => isNotEmpty(value) && value.includes('='))
|
||||||
.map((value) => value.split('=')[1].trim());
|
.map((value) => value.split('=')[1].trim());
|
||||||
this.form.get('task').patchValue(this.tasks[0]);
|
this.form.get('task').patchValue(this.tasks[0]);
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,33 +84,41 @@ export class CurationFormComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
submit() {
|
submit() {
|
||||||
const taskName = this.form.get('task').value;
|
const taskName = this.form.get('task').value;
|
||||||
let handle;
|
let handle$: Observable<string | null>;
|
||||||
if (this.hasHandleValue()) {
|
if (this.hasHandleValue()) {
|
||||||
handle = this.handleService.normalizeHandle(this.dsoHandle);
|
handle$ = this.handleService.normalizeHandle(this.dsoHandle).pipe(
|
||||||
if (isEmpty(handle)) {
|
map((handle: string | null) => {
|
||||||
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
|
if (isEmpty(handle)) {
|
||||||
this.translateService.get('curation.form.submit.error.invalid-handle'));
|
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
|
||||||
return;
|
this.translateService.get('curation.form.submit.error.invalid-handle'));
|
||||||
}
|
}
|
||||||
|
return handle;
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
handle = this.handleService.normalizeHandle(this.form.get('handle').value);
|
handle$ = this.handleService.normalizeHandle(this.form.get('handle').value).pipe(
|
||||||
if (isEmpty(handle)) {
|
map((handle: string | null) => isEmpty(handle) ? 'all' : handle),
|
||||||
handle = 'all';
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scriptDataService.invoke('curate', [
|
this.subs.push(handle$.subscribe((handle: string) => {
|
||||||
{ name: '-t', value: taskName },
|
if (hasValue(handle)) {
|
||||||
{ name: '-i', value: handle },
|
this.subs.push(this.scriptDataService.invoke('curate', [
|
||||||
], []).pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<Process>) => {
|
{ name: '-t', value: taskName },
|
||||||
if (rd.hasSucceeded) {
|
{ name: '-i', value: handle },
|
||||||
this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'),
|
], []).pipe(
|
||||||
this.translateService.get('curation.form.submit.success.content'));
|
getFirstCompletedRemoteData(),
|
||||||
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
|
).subscribe((rd: RemoteData<Process>) => {
|
||||||
} else {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
|
this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'),
|
||||||
this.translateService.get('curation.form.submit.error.content'));
|
this.translateService.get('curation.form.submit.success.content'));
|
||||||
|
void this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
|
||||||
|
this.translateService.get('curation.form.submit.error.content'));
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<ds-register-email-form
|
<ds-themed-register-email-form
|
||||||
[MESSAGE_PREFIX]="'forgot-email.form'" [typeRequest]="typeRequest">
|
[MESSAGE_PREFIX]="'forgot-email.form'" [typeRequest]="typeRequest">
|
||||||
</ds-register-email-form>
|
</ds-themed-register-email-form>
|
||||||
|
@@ -3,13 +3,14 @@ import {
|
|||||||
RegisterEmailFormComponent,
|
RegisterEmailFormComponent,
|
||||||
TYPE_REQUEST_FORGOT
|
TYPE_REQUEST_FORGOT
|
||||||
} from '../../register-email-form/register-email-form.component';
|
} from '../../register-email-form/register-email-form.component';
|
||||||
|
import { ThemedRegisterEmailFormComponent } from 'src/app/register-email-form/themed-registry-email-form.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-forgot-email',
|
selector: 'ds-forgot-email',
|
||||||
styleUrls: ['./forgot-email.component.scss'],
|
styleUrls: ['./forgot-email.component.scss'],
|
||||||
templateUrl: './forgot-email.component.html',
|
templateUrl: './forgot-email.component.html',
|
||||||
imports: [
|
imports: [
|
||||||
RegisterEmailFormComponent
|
RegisterEmailFormComponent, ThemedRegisterEmailFormComponent
|
||||||
],
|
],
|
||||||
standalone: true
|
standalone: true
|
||||||
})
|
})
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}">
|
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}" id="header-navbar-wrapper">
|
||||||
<ds-themed-header></ds-themed-header>
|
<ds-themed-header></ds-themed-header>
|
||||||
<ds-themed-navbar></ds-themed-navbar>
|
<ds-themed-navbar></ds-themed-navbar>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
:host {
|
:host {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: var(--ds-nav-z-index);
|
div#header-navbar-wrapper {
|
||||||
|
border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||||
import { ContextHelpService } from '../../shared/context-help.service';
|
import { ContextHelpService } from '../../shared/context-help.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { NgIf, AsyncPipe } from '@angular/common';
|
import { NgIf, AsyncPipe } from '@angular/common';
|
||||||
@@ -19,12 +19,23 @@ import { NgIf, AsyncPipe } from '@angular/common';
|
|||||||
export class ContextHelpToggleComponent implements OnInit {
|
export class ContextHelpToggleComponent implements OnInit {
|
||||||
buttonVisible$: Observable<boolean>;
|
buttonVisible$: Observable<boolean>;
|
||||||
|
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private contextHelpService: ContextHelpService,
|
protected elRef: ElementRef,
|
||||||
) { }
|
protected contextHelpService: ContextHelpService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0));
|
this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0));
|
||||||
|
this.subscriptions.push(this.buttonVisible$.subscribe((showContextHelpToggle: boolean) => {
|
||||||
|
if (showContextHelpToggle) {
|
||||||
|
this.elRef.nativeElement.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
this.elRef.nativeElement.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick() {
|
onClick() {
|
||||||
|
@@ -7,12 +7,12 @@
|
|||||||
|
|
||||||
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
|
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
|
||||||
<ds-themed-search-navbar></ds-themed-search-navbar>
|
<ds-themed-search-navbar></ds-themed-search-navbar>
|
||||||
<ds-lang-switch></ds-lang-switch>
|
<ds-themed-lang-switch></ds-themed-lang-switch>
|
||||||
<ds-context-help-toggle></ds-context-help-toggle>
|
<ds-context-help-toggle></ds-context-help-toggle>
|
||||||
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
|
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
|
||||||
<ds-impersonate-navbar></ds-impersonate-navbar>
|
<ds-impersonate-navbar></ds-impersonate-navbar>
|
||||||
<div class="pl-2">
|
<div *ngIf="isXsOrSm$ | async" class="pl-2">
|
||||||
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
<button class="navbar-toggler px-0" type="button" (click)="toggleNavbar()"
|
||||||
aria-controls="collapsingNav"
|
aria-controls="collapsingNav"
|
||||||
aria-expanded="false" [attr.aria-label]="'nav.toggle' | translate">
|
aria-expanded="false" [attr.aria-label]="'nav.toggle' | translate">
|
||||||
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
|
header {
|
||||||
|
background-color: var(--ds-header-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-brand img {
|
.navbar-brand img {
|
||||||
max-height: var(--ds-header-logo-height);
|
max-height: var(--ds-header-logo-height);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -20,3 +24,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(var(--bs-spacer) / 3);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
@@ -18,6 +18,8 @@ import { LangSwitchComponent } from '../shared/lang-switch/lang-switch.component
|
|||||||
import { ContextHelpToggleComponent } from './context-help-toggle/context-help-toggle.component';
|
import { ContextHelpToggleComponent } from './context-help-toggle/context-help-toggle.component';
|
||||||
import { ThemedAuthNavMenuComponent } from '../shared/auth-nav-menu/themed-auth-nav-menu.component';
|
import { ThemedAuthNavMenuComponent } from '../shared/auth-nav-menu/themed-auth-nav-menu.component';
|
||||||
import { ImpersonateNavbarComponent } from '../shared/impersonate-navbar/impersonate-navbar.component';
|
import { ImpersonateNavbarComponent } from '../shared/impersonate-navbar/impersonate-navbar.component';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
|
||||||
|
|
||||||
let comp: HeaderComponent;
|
let comp: HeaderComponent;
|
||||||
let fixture: ComponentFixture<HeaderComponent>;
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
@@ -44,7 +46,8 @@ describe('HeaderComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub()},
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub()},
|
||||||
{ provide: LocaleService, useValue: mockLocaleService }
|
{ provide: LocaleService, useValue: mockLocaleService },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
@@ -61,7 +64,7 @@ describe('HeaderComponent', () => {
|
|||||||
fixture = TestBed.createComponent(HeaderComponent);
|
fixture = TestBed.createComponent(HeaderComponent);
|
||||||
|
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the toggle button is clicked', () => {
|
describe('when the toggle button is clicked', () => {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { MenuID } from '../shared/menu/menu-id.model';
|
import { MenuID } from '../shared/menu/menu-id.model';
|
||||||
@@ -10,6 +10,8 @@ import { LangSwitchComponent } from '../shared/lang-switch/lang-switch.component
|
|||||||
import { ThemedSearchNavbarComponent } from '../search-navbar/themed-search-navbar.component';
|
import { ThemedSearchNavbarComponent } from '../search-navbar/themed-search-navbar.component';
|
||||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
import { ThemedLangSwitchComponent } from '../shared/lang-switch/themed-lang-switch.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the header with the logo and simple navigation
|
* Represents the header with the logo and simple navigation
|
||||||
@@ -19,22 +21,27 @@ import { RouterLink } from '@angular/router';
|
|||||||
styleUrls: ['header.component.scss'],
|
styleUrls: ['header.component.scss'],
|
||||||
templateUrl: 'header.component.html',
|
templateUrl: 'header.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterLink, NgbDropdownModule, ThemedSearchNavbarComponent, LangSwitchComponent, ContextHelpToggleComponent, ThemedAuthNavMenuComponent, ImpersonateNavbarComponent, TranslateModule]
|
imports: [RouterLink, ThemedLangSwitchComponent, NgbDropdownModule, ThemedSearchNavbarComponent, LangSwitchComponent, ContextHelpToggleComponent, ThemedAuthNavMenuComponent, ImpersonateNavbarComponent, TranslateModule]
|
||||||
})
|
})
|
||||||
export class HeaderComponent {
|
export class HeaderComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* Whether user is authenticated.
|
* Whether user is authenticated.
|
||||||
* @type {Observable<string>}
|
* @type {Observable<string>}
|
||||||
*/
|
*/
|
||||||
public isAuthenticated: Observable<boolean>;
|
public isAuthenticated: Observable<boolean>;
|
||||||
public showAuth = false;
|
public isXsOrSm$: Observable<boolean>;
|
||||||
menuID = MenuID.PUBLIC;
|
menuID = MenuID.PUBLIC;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private menuService: MenuService
|
protected menuService: MenuService,
|
||||||
|
protected windowService: HostWindowService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
|
}
|
||||||
|
|
||||||
public toggleNavbar(): void {
|
public toggleNavbar(): void {
|
||||||
this.menuService.toggleMenu(this.menuID);
|
this.menuService.toggleMenu(this.menuID);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="jumbotron jumbotron-fluid">
|
<div class="jumbotron jumbotron-fluid mt-ncs">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex flex-wrap">
|
<div class="d-flex flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: calc(-1 * var(--ds-content-spacing));
|
|
||||||
margin-bottom: calc(-1 * var(--ds-content-spacing));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-3 {
|
.display-3 {
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<div class="mt-4" [ngClass]="placeholderFontClass" *ngIf="itemRD?.hasSucceeded && itemRD?.payload?.page.length > 0" @fadeIn>
|
<div class="mt-4" [ngClass]="placeholderFontClass" *ngIf="itemRD?.hasSucceeded && itemRD?.payload?.page.length > 0" @fadeIn>
|
||||||
<div class="d-flex flex-row border-bottom mb-4 pb-4 ng-tns-c416-2"></div>
|
<div class="d-flex flex-row border-bottom mb-4 pb-4"></div>
|
||||||
<h2> {{'home.recent-submissions.head' | translate}}</h2>
|
<h2> {{'home.recent-submissions.head' | translate}}</h2>
|
||||||
<div class="my-4" *ngFor="let item of itemRD?.payload?.page">
|
<div class="my-4" *ngFor="let item of itemRD?.payload?.page">
|
||||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4">
|
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4">
|
||||||
</ds-listable-object-component-loader>
|
</ds-listable-object-component-loader>
|
||||||
</div>
|
</div>
|
||||||
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4 float-left ng-tns-c290-40"> {{'vocabulary-treeview.load-more' | translate }} ...</button>
|
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4"> {{'vocabulary-treeview.load-more' | translate }} ...</button>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}">
|
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}">
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div class="row row-offcanvas row-offcanvas-right">
|
<div class="row row-offcanvas row-offcanvas-right">
|
||||||
<div class="col-xs-12 col-sm-12 col-md-9 main-content">
|
<div class="col-xs-12 col-sm-12 col-md-9">
|
||||||
<form class="primary" [formGroup]="feedbackForm" (ngSubmit)="createFeedback()">
|
<form class="primary" [formGroup]="feedbackForm" (ngSubmit)="createFeedback()">
|
||||||
<h2>{{ 'info.feedback.head' | translate }}</h2>
|
<h2>{{ 'info.feedback.head' | translate }}</h2>
|
||||||
<p>{{ 'info.feedback.info' | translate }}</p>
|
<p>{{ 'info.feedback.info' | translate }}</p>
|
||||||
|
@@ -38,6 +38,8 @@ import { ItemPageBitstreamsGuard } from './item-page-bitstreams.guard';
|
|||||||
import { ItemPageRelationshipsGuard } from './item-page-relationships.guard';
|
import { ItemPageRelationshipsGuard } from './item-page-relationships.guard';
|
||||||
import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard';
|
import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard';
|
||||||
import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard';
|
import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard';
|
||||||
|
import { ItemPageCurateGuard } from './item-page-curate.guard';
|
||||||
|
import { ItemPageAccessControlGuard } from './item-page-access-control.guard';
|
||||||
import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component';
|
import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component';
|
||||||
import { ItemPageRegisterDoiGuard } from './item-page-register-doi.guard';
|
import { ItemPageRegisterDoiGuard } from './item-page-register-doi.guard';
|
||||||
import { ItemCurateComponent } from './item-curate/item-curate.component';
|
import { ItemCurateComponent } from './item-curate/item-curate.component';
|
||||||
@@ -87,7 +89,8 @@ import { ItemAccessControlComponent } from './item-access-control/item-access-co
|
|||||||
{
|
{
|
||||||
path: 'curate',
|
path: 'curate',
|
||||||
component: ItemCurateComponent,
|
component: ItemCurateComponent,
|
||||||
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
|
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true },
|
||||||
|
canActivate: [ItemPageCurateGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'relationships',
|
path: 'relationships',
|
||||||
@@ -116,7 +119,8 @@ import { ItemAccessControlComponent } from './item-access-control/item-access-co
|
|||||||
{
|
{
|
||||||
path: 'access-control',
|
path: 'access-control',
|
||||||
component: ItemAccessControlComponent,
|
component: ItemAccessControlComponent,
|
||||||
data: { title: 'item.edit.tabs.access-control.title', showBreadcrumbs: true }
|
data: { title: 'item.edit.tabs.access-control.title', showBreadcrumbs: true },
|
||||||
|
canActivate: [ItemPageAccessControlGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mapper',
|
path: 'mapper',
|
||||||
@@ -202,11 +206,13 @@ import { ItemAccessControlComponent } from './item-access-control/item-access-co
|
|||||||
ItemPageWithdrawGuard,
|
ItemPageWithdrawGuard,
|
||||||
ItemPageAdministratorGuard,
|
ItemPageAdministratorGuard,
|
||||||
ItemPageMetadataGuard,
|
ItemPageMetadataGuard,
|
||||||
|
ItemPageCurateGuard,
|
||||||
ItemPageStatusGuard,
|
ItemPageStatusGuard,
|
||||||
ItemPageBitstreamsGuard,
|
ItemPageBitstreamsGuard,
|
||||||
ItemPageRelationshipsGuard,
|
ItemPageRelationshipsGuard,
|
||||||
ItemPageVersionHistoryGuard,
|
ItemPageVersionHistoryGuard,
|
||||||
ItemPageCollectionMapperGuard,
|
ItemPageCollectionMapperGuard,
|
||||||
|
ItemPageAccessControlGuard,
|
||||||
ItemPageRegisterDoiGuard,
|
ItemPageRegisterDoiGuard,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -25,7 +25,7 @@
|
|||||||
<div class="{{columnSizes.columns[3].buildClasses()}} row-element d-flex align-items-center">
|
<div class="{{columnSizes.columns[3].buildClasses()}} row-element d-flex align-items-center">
|
||||||
<div class="text-center w-100">
|
<div class="text-center w-100">
|
||||||
<div class="btn-group relationship-action-buttons">
|
<div class="btn-group relationship-action-buttons">
|
||||||
<a *ngIf="bitstreamDownloadUrl != null" [href]="bitstreamDownloadUrl"
|
<a *ngIf="bitstreamDownloadUrl != null" [routerLink]="bitstreamDownloadUrl"
|
||||||
class="btn btn-outline-primary btn-sm"
|
class="btn btn-outline-primary btn-sm"
|
||||||
title="{{'item.edit.bitstreams.edit.buttons.download' | translate}}"
|
title="{{'item.edit.bitstreams.edit.buttons.download' | translate}}"
|
||||||
[attr.data-test]="'download-button' | dsBrowserOnly">
|
[attr.data-test]="'download-button' | dsBrowserOnly">
|
||||||
|
@@ -15,6 +15,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { BrowserOnlyMockPipe } from '../../../../shared/testing/browser-only-mock.pipe';
|
import { BrowserOnlyMockPipe } from '../../../../shared/testing/browser-only-mock.pipe';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
let comp: ItemEditBitstreamComponent;
|
let comp: ItemEditBitstreamComponent;
|
||||||
let fixture: ComponentFixture<ItemEditBitstreamComponent>;
|
let fixture: ComponentFixture<ItemEditBitstreamComponent>;
|
||||||
@@ -74,7 +75,10 @@ describe('ItemEditBitstreamComponent', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), ItemEditBitstreamComponent,
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
ItemEditBitstreamComponent,
|
||||||
VarDirective],
|
VarDirective],
|
||||||
declarations: [BrowserOnlyMockPipe],
|
declarations: [BrowserOnlyMockPipe],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -21,7 +21,11 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<p>
|
<p>
|
||||||
<label for="inheritPoliciesCheckbox">
|
<label for="inheritPoliciesCheckbox">
|
||||||
<input type="checkbox" name="tc" [(ngModel)]="inheritPolicies" id="inheritPoliciesCheckbox">
|
<ng-template #tooltipContent>
|
||||||
|
{{ 'item.edit.move.inheritpolicies.tooltip' | translate }}
|
||||||
|
</ng-template>
|
||||||
|
<input type="checkbox" name="tc" [(ngModel)]="inheritPolicies" id="inheritPoliciesCheckbox" [ngbTooltip]="tooltipContent"
|
||||||
|
>
|
||||||
{{'item.edit.move.inheritpolicies.checkbox' |translate}}
|
{{'item.edit.move.inheritpolicies.checkbox' |translate}}
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
|
@@ -22,12 +22,14 @@ import { AsyncPipe, NgIf } from '@angular/common';
|
|||||||
import {
|
import {
|
||||||
AuthorizedCollectionSelectorComponent
|
AuthorizedCollectionSelectorComponent
|
||||||
} from '../../../shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component';
|
} from '../../../shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-move',
|
selector: 'ds-item-move',
|
||||||
templateUrl: './item-move.component.html',
|
templateUrl: './item-move.component.html',
|
||||||
imports: [
|
imports: [
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
|
NgbModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
|
@@ -28,4 +28,12 @@ export class ItemOperation {
|
|||||||
this.disabled = disabled;
|
this.disabled = disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether this operation is authorized
|
||||||
|
* @param authorized
|
||||||
|
*/
|
||||||
|
setAuthorized(authorized: boolean): void {
|
||||||
|
this.authorized = authorized;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
||||||
|
*/
|
||||||
|
export class ItemPageAccessControlGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check administrator authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.AdministratorOf);
|
||||||
|
}
|
||||||
|
}
|
31
src/app/item-page/edit-item-page/item-page-curate.guard.ts
Normal file
31
src/app/item-page/edit-item-page/item-page-curate.guard.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
||||||
|
*/
|
||||||
|
export class ItemPageCurateGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check administrator authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.AdministratorOf);
|
||||||
|
}
|
||||||
|
}
|
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">
|
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">
|
||||||
<ds-item-operation *ngIf="operation" [operation]="operation"></ds-item-operation>
|
<ds-item-operation [operation]="operation"></ds-item-operation>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,6 +16,7 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
|
|||||||
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
|
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
|
||||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||||
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
||||||
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
let mockIdentifierDataService: IdentifierDataService;
|
let mockIdentifierDataService: IdentifierDataService;
|
||||||
let mockConfigurationDataService: ConfigurationDataService;
|
let mockConfigurationDataService: ConfigurationDataService;
|
||||||
@@ -57,12 +58,18 @@ describe('ItemStatusComponent', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let orcidAuthService: any;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
isAuthorized: observableOf(true)
|
isAuthorized: observableOf(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
|
||||||
|
onlyAdminCanDisconnectProfileFromOrcid: observableOf ( true ),
|
||||||
|
isLinkedToOrcid: true
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, ItemStatusComponent],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, ItemStatusComponent],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -70,9 +77,10 @@ describe('ItemStatusComponent', () => {
|
|||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
{ provide: IdentifierDataService, useValue: mockIdentifierDataService },
|
{ provide: IdentifierDataService, useValue: mockIdentifierDataService },
|
||||||
{ provide: ConfigurationDataService, useValue: mockConfigurationDataService }
|
{ provide: ConfigurationDataService, useValue: mockConfigurationDataService },
|
||||||
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
{ provide: OrcidAuthService, useValue: orcidAuthService },
|
||||||
}).compileComponents();
|
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@@ -3,16 +3,14 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
|
|||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||||
import { ItemOperation } from '../item-operation/itemOperation.model';
|
import { ItemOperation } from '../item-operation/itemOperation.model';
|
||||||
import { distinctUntilChanged, map, mergeMap, switchMap, toArray } from 'rxjs/operators';
|
import { concatMap, distinctUntilChanged, first, map, mergeMap, switchMap, toArray } from 'rxjs/operators';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import {
|
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, } from '../../../core/shared/operators';
|
||||||
getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload,
|
|
||||||
} from '../../../core/shared/operators';
|
|
||||||
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
|
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
|
||||||
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
|
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
|
||||||
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
||||||
@@ -21,6 +19,7 @@ import { IdentifierData } from '../../../shared/object-list/identifier-data/iden
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { AsyncPipe, NgClass, NgForOf, NgIf } from '@angular/common';
|
import { AsyncPipe, NgClass, NgForOf, NgIf } from '@angular/common';
|
||||||
import { ItemOperationComponent } from '../item-operation/item-operation.component';
|
import { ItemOperationComponent } from '../item-operation/item-operation.component';
|
||||||
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-status',
|
selector: 'ds-item-status',
|
||||||
@@ -86,6 +85,7 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
private identifierDataService: IdentifierDataService,
|
private identifierDataService: IdentifierDataService,
|
||||||
private configurationService: ConfigurationDataService,
|
private configurationService: ConfigurationDataService,
|
||||||
|
private orcidAuthService: OrcidAuthService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,14 +95,16 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso));
|
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso));
|
||||||
this.itemRD$.pipe(
|
this.itemRD$.pipe(
|
||||||
|
first(),
|
||||||
map((data: RemoteData<Item>) => data.payload)
|
map((data: RemoteData<Item>) => data.payload)
|
||||||
).subscribe((item: Item) => {
|
).pipe(
|
||||||
this.statusData = Object.assign({
|
switchMap((item: Item) => {
|
||||||
id: item.id,
|
this.statusData = Object.assign({
|
||||||
handle: item.handle,
|
id: item.id,
|
||||||
lastModified: item.lastModified
|
handle: item.handle,
|
||||||
});
|
lastModified: item.lastModified
|
||||||
this.statusDataKeys = Object.keys(this.statusData);
|
});
|
||||||
|
this.statusDataKeys = Object.keys(this.statusData);
|
||||||
|
|
||||||
// Observable for item identifiers (retrieved from embedded link)
|
// Observable for item identifiers (retrieved from embedded link)
|
||||||
this.identifiers$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
|
this.identifiers$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
|
||||||
@@ -118,99 +120,108 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
// Observable for configuration determining whether the Register DOI feature is enabled
|
// Observable for configuration determining whether the Register DOI feature is enabled
|
||||||
let registerConfigEnabled$: Observable<boolean> = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
|
let registerConfigEnabled$: Observable<boolean> = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
map((rd: RemoteData<ConfigurationProperty>) => {
|
map((enabledRD: RemoteData<ConfigurationProperty>) => enabledRD.hasSucceeded && enabledRD.payload.values.length > 0)
|
||||||
// If the config property is exposed via rest and has a value set, return it
|
|
||||||
if (rd.hasSucceeded && hasValue(rd.payload) && isNotEmpty(rd.payload.values)) {
|
|
||||||
return rd.payload.values[0] === 'true';
|
|
||||||
}
|
|
||||||
// Otherwise, return false
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Construct a base list of operations.
|
* Construct a base list of operations.
|
||||||
The key is used to build messages
|
* The key is used to build messages
|
||||||
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
* i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
||||||
The value is supposed to be a href for the button
|
* The value is supposed to be a href for the button
|
||||||
*/
|
|
||||||
const operations: ItemOperation[] = [];
|
|
||||||
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true));
|
|
||||||
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true));
|
|
||||||
if (item.isWithdrawn) {
|
|
||||||
operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.ReinstateItem, true));
|
|
||||||
} else {
|
|
||||||
operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw', FeatureID.WithdrawItem, true));
|
|
||||||
}
|
|
||||||
if (item.isDiscoverable) {
|
|
||||||
operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private', FeatureID.CanMakePrivate, true));
|
|
||||||
} else {
|
|
||||||
operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public', FeatureID.CanMakePrivate, true));
|
|
||||||
}
|
|
||||||
operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete', FeatureID.CanDelete, true));
|
|
||||||
operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move', FeatureID.CanMove, true));
|
|
||||||
this.operations$.next(operations);
|
|
||||||
|
|
||||||
/*
|
|
||||||
When the identifier data stream changes, determine whether the register DOI button should be shown or not.
|
|
||||||
This is based on whether the DOI is in the right state (minted or pending, not already queued for registration
|
|
||||||
or registered) and whether the configuration property identifiers.item-status.register-doi is true
|
|
||||||
*/
|
*/
|
||||||
this.identifierDataService.getIdentifierDataFor(item).pipe(
|
const currentUrl = this.getCurrentUrl(item);
|
||||||
getFirstSucceededRemoteData(),
|
const inititalOperations: ItemOperation[] = [
|
||||||
getRemoteDataPayload(),
|
new ItemOperation('authorizations', `${currentUrl}/authorizations`, FeatureID.CanManagePolicies, true),
|
||||||
mergeMap((data: IdentifierData) => {
|
new ItemOperation('mappedCollections', `${currentUrl}/mapper`, FeatureID.CanManageMappings, true),
|
||||||
let identifiers = data.identifiers;
|
item.isWithdrawn
|
||||||
let no_doi = true;
|
? new ItemOperation('reinstate', `${currentUrl}/reinstate`, FeatureID.ReinstateItem, true)
|
||||||
let pending = false;
|
: new ItemOperation('withdraw', `${currentUrl}/withdraw`, FeatureID.WithdrawItem, true),
|
||||||
if (identifiers !== undefined && identifiers !== null) {
|
item.isDiscoverable
|
||||||
identifiers.forEach((identifier: Identifier) => {
|
? new ItemOperation('private', `${currentUrl}/private`, FeatureID.CanMakePrivate, true)
|
||||||
if (hasValue(identifier) && identifier.identifierType === 'doi') {
|
: new ItemOperation('public', `${currentUrl}/public`, FeatureID.CanMakePrivate, true),
|
||||||
// The item has some kind of DOI
|
new ItemOperation('move', `${currentUrl}/move`, FeatureID.CanMove, true),
|
||||||
no_doi = false;
|
new ItemOperation('delete', `${currentUrl}/delete`, FeatureID.CanDelete, true)
|
||||||
if (identifier.identifierStatus === 'PENDING' || identifier.identifierStatus === 'MINTED'
|
];
|
||||||
|| identifier.identifierStatus == null) {
|
|
||||||
// The item's DOI is pending, minted or null.
|
this.operations$.next(inititalOperations);
|
||||||
// It isn't registered, reserved, queued for registration or reservation or update, deleted
|
|
||||||
// or queued for deletion.
|
/**
|
||||||
pending = true;
|
* When the identifier data stream changes, determine whether the register DOI button should be shown or not.
|
||||||
}
|
* This is based on whether the DOI is in the right state (minted or pending, not already queued for registration
|
||||||
|
* or registered) and whether the configuration property identifiers.item-status.register-doi is true
|
||||||
|
*/
|
||||||
|
const ops$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
mergeMap((dataRD: RemoteData<IdentifierData>) => {
|
||||||
|
if (dataRD.hasSucceeded) {
|
||||||
|
let identifiers = dataRD.payload.identifiers;
|
||||||
|
let no_doi = true;
|
||||||
|
let pending = false;
|
||||||
|
if (identifiers !== undefined && identifiers !== null) {
|
||||||
|
identifiers.forEach((identifier: Identifier) => {
|
||||||
|
if (hasValue(identifier) && identifier.identifierType === 'doi') {
|
||||||
|
// The item has some kind of DOI
|
||||||
|
no_doi = false;
|
||||||
|
if (['PENDING', 'MINTED', null].includes(identifier.identifierStatus)) {
|
||||||
|
// The item's DOI is pending, minted or null.
|
||||||
|
// It isn't registered, reserved, queued for registration or reservation or update, deleted
|
||||||
|
// or queued for deletion.
|
||||||
|
pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
// If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true
|
||||||
}
|
return registerConfigEnabled$.pipe(
|
||||||
// If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true
|
map((enabled: boolean) => {
|
||||||
return registerConfigEnabled$.pipe(
|
return enabled && (pending || no_doi);
|
||||||
map((enabled: boolean) => {
|
}
|
||||||
return enabled && (pending || no_doi);
|
));
|
||||||
|
} else {
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe
|
||||||
|
switchMap((showDoi: boolean) => {
|
||||||
|
const ops = [...inititalOperations];
|
||||||
|
if (showDoi) {
|
||||||
|
const op = new ItemOperation('register-doi', `${currentUrl}/register-doi`, FeatureID.CanRegisterDOI, true);
|
||||||
|
ops.splice(ops.length - 1, 0, op); // Add item before last
|
||||||
|
}
|
||||||
|
return inititalOperations;
|
||||||
|
}),
|
||||||
|
concatMap((op: ItemOperation) => {
|
||||||
|
if (hasValue(op.featureID)) {
|
||||||
|
return this.authorizationService.isAuthorized(op.featureID, item.self).pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((authorized) => {
|
||||||
|
op.setDisabled(!authorized);
|
||||||
|
op.setAuthorized(authorized);
|
||||||
|
return op;
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
));
|
|
||||||
}),
|
|
||||||
// Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe
|
|
||||||
switchMap((showDoi: boolean) => {
|
|
||||||
let ops = [...operations];
|
|
||||||
if (showDoi) {
|
|
||||||
ops.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI, true));
|
|
||||||
}
|
|
||||||
return ops;
|
|
||||||
}),
|
|
||||||
// Merge map checks and transforms each operation in the array based on whether it is authorized or not (disabled)
|
|
||||||
mergeMap((op: ItemOperation) => {
|
|
||||||
if (hasValue(op.featureID)) {
|
|
||||||
return this.authorizationService.isAuthorized(op.featureID, item.self).pipe(
|
|
||||||
distinctUntilChanged(),
|
|
||||||
map((authorized) => new ItemOperation(op.operationKey, op.operationUrl, op.featureID, !authorized, authorized))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return [op];
|
return [op];
|
||||||
}
|
}),
|
||||||
}),
|
toArray()
|
||||||
// Wait for all operations to be emitted and return as an array
|
);
|
||||||
toArray(),
|
|
||||||
).subscribe((data) => {
|
let orcidOps$ = of([]);
|
||||||
// Update the operations$ subject that draws the administrative buttons on the status page
|
if (this.orcidAuthService.isLinkedToOrcid(item)) {
|
||||||
this.operations$.next(data);
|
orcidOps$ = this.orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid().pipe(
|
||||||
});
|
map((canDisconnect) => {
|
||||||
});
|
if (canDisconnect) {
|
||||||
|
return [new ItemOperation('unlinkOrcid', `${currentUrl}/unlink-orcid`)];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return combineLatest([ops$, orcidOps$]);
|
||||||
|
}),
|
||||||
|
map(([ops, orcidOps]: [ItemOperation[], ItemOperation[]]) => [...ops, ...orcidOps])
|
||||||
|
).subscribe((ops) => this.operations$.next(ops));
|
||||||
|
|
||||||
this.itemPageRoute$ = this.itemRD$.pipe(
|
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
@@ -219,6 +230,7 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current url without query params
|
* Get the current url without query params
|
||||||
* @returns {string} url
|
* @returns {string} url
|
||||||
|
@@ -77,7 +77,8 @@ export class MiradorViewerComponent implements OnInit {
|
|||||||
const manifestApiEndpoint = encodeURIComponent(environment.rest.baseUrl + '/iiif/'
|
const manifestApiEndpoint = encodeURIComponent(environment.rest.baseUrl + '/iiif/'
|
||||||
+ this.object.id + '/manifest');
|
+ this.object.id + '/manifest');
|
||||||
// The Express path to Mirador viewer.
|
// The Express path to Mirador viewer.
|
||||||
let viewerPath = '/iiif/mirador/index.html?manifest=' + manifestApiEndpoint;
|
let viewerPath = `${environment.ui.nameSpace}${environment.ui.nameSpace.length > 1 ? '/' : ''}`
|
||||||
|
+ `iiif/mirador/index.html?manifest=${manifestApiEndpoint}`;
|
||||||
if (this.searchable) {
|
if (this.searchable) {
|
||||||
// Tell the viewer add search to menu.
|
// Tell the viewer add search to menu.
|
||||||
viewerPath += '&searchable=' + this.searchable;
|
viewerPath += '&searchable=' + this.searchable;
|
||||||
|
@@ -3,8 +3,8 @@
|
|||||||
<div>
|
<div>
|
||||||
<img class="mb-4 login-logo" src="assets/images/dspace-logo.png" alt="{{'repository.image.logo' | translate}}">
|
<img class="mb-4 login-logo" src="assets/images/dspace-logo.png" alt="{{'repository.image.logo' | translate}}">
|
||||||
<h1 class="h3 mb-0 font-weight-normal">{{"login.form.header" | translate}}</h1>
|
<h1 class="h3 mb-0 font-weight-normal">{{"login.form.header" | translate}}</h1>
|
||||||
<ds-log-in
|
<ds-themed-log-in
|
||||||
[isStandalonePage]="true"></ds-log-in>
|
[isStandalonePage]="true"></ds-themed-log-in>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -17,6 +17,7 @@ import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
|
|||||||
import { isAuthenticated } from '../core/auth/selectors';
|
import { isAuthenticated } from '../core/auth/selectors';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { LogInComponent } from '../shared/log-in/log-in.component';
|
import { LogInComponent } from '../shared/log-in/log-in.component';
|
||||||
|
import { ThemedLogInComponent } from '../shared/log-in/themed-log-in.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents the login page
|
* This component represents the login page
|
||||||
@@ -26,7 +27,7 @@ import { LogInComponent } from '../shared/log-in/log-in.component';
|
|||||||
styleUrls: ['./login-page.component.scss'],
|
styleUrls: ['./login-page.component.scss'],
|
||||||
templateUrl: './login-page.component.html',
|
templateUrl: './login-page.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [LogInComponent, TranslateModule]
|
imports: [LogInComponent, ThemedLogInComponent, TranslateModule]
|
||||||
})
|
})
|
||||||
export class LoginPageComponent implements OnDestroy, OnInit {
|
export class LoginPageComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
|
@@ -14,9 +14,9 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul @slide *ngIf="(active | async)" (click)="deactivateSection($event)"
|
<ul @slide *ngIf="(active | async)" (click)="deactivateSection($event)"
|
||||||
class="m-0 shadow-none border-top-0 dropdown-menu show">
|
class="m-0 shadow-none border-top-0 dropdown-menu show">
|
||||||
<ng-container *ngFor="let subSection of (subSections$ | async)">
|
<li *ngFor="let subSection of (subSections$ | async)">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||||
</ng-container>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -6,14 +6,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
|
background-color: var(--ds-expandable-navbar-bg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
::ng-deep a.nav-link {
|
::ng-deep a.nav-link {
|
||||||
|
color: var(--ds-expandable-navbar-link-color) !important;
|
||||||
padding-right: var(--bs-spacer);
|
padding-right: var(--bs-spacer);
|
||||||
padding-left: var(--bs-spacer);
|
padding-left: var(--bs-spacer);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
color: var(--ds-expandable-navbar-link-color-hover) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,7 +4,6 @@ import { MenuService } from '../../shared/menu/menu.service';
|
|||||||
import { slide } from '../../shared/animations/slide';
|
import { slide } from '../../shared/animations/slide';
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import { HostWindowService } from '../../shared/host-window.service';
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator';
|
|
||||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||||
import { NgComponentOutlet, NgIf, NgFor, AsyncPipe } from '@angular/common';
|
import { NgComponentOutlet, NgIf, NgFor, AsyncPipe } from '@angular/common';
|
||||||
import { RouterLinkActive } from '@angular/router';
|
import { RouterLinkActive } from '@angular/router';
|
||||||
@@ -21,7 +20,6 @@ import { VarDirective } from '../../shared/utils/var.directive';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [VarDirective, RouterLinkActive, NgComponentOutlet, NgIf, NgFor, AsyncPipe]
|
imports: [VarDirective, RouterLinkActive, NgComponentOutlet, NgIf, NgFor, AsyncPipe]
|
||||||
})
|
})
|
||||||
@rendersSectionForMenu(MenuID.PUBLIC, true)
|
|
||||||
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit {
|
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* This section resides in the Public Navbar
|
* This section resides in the Public Navbar
|
||||||
|
@@ -8,11 +8,10 @@ import { MenuID } from '../../shared/menu/menu-id.model';
|
|||||||
* Themed wrapper for ExpandableNavbarSectionComponent
|
* Themed wrapper for ExpandableNavbarSectionComponent
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
/* eslint-disable @angular-eslint/component-selector */
|
selector: 'ds-themed-expandable-navbar-section',
|
||||||
selector: 'li[ds-themed-expandable-navbar-section]',
|
styleUrls: [],
|
||||||
styleUrls: [],
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
standalone: true
|
||||||
standalone: true
|
|
||||||
})
|
})
|
||||||
@rendersSectionForMenu(MenuID.PUBLIC, true)
|
@rendersSectionForMenu(MenuID.PUBLIC, true)
|
||||||
export class ThemedExpandableNavbarSectionComponent extends ThemedComponent<ExpandableNavbarSectionComponent> {
|
export class ThemedExpandableNavbarSectionComponent extends ThemedComponent<ExpandableNavbarSectionComponent> {
|
||||||
|
@@ -9,12 +9,11 @@ import { NgComponentOutlet, AsyncPipe } from '@angular/common';
|
|||||||
* Represents a non-expandable section in the navbar
|
* Represents a non-expandable section in the navbar
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
/* eslint-disable @angular-eslint/component-selector */
|
selector: 'ds-navbar-section',
|
||||||
selector: 'li[ds-navbar-section]',
|
templateUrl: './navbar-section.component.html',
|
||||||
templateUrl: './navbar-section.component.html',
|
styleUrls: ['./navbar-section.component.scss'],
|
||||||
styleUrls: ['./navbar-section.component.scss'],
|
standalone: true,
|
||||||
standalone: true,
|
imports: [NgComponentOutlet, AsyncPipe]
|
||||||
imports: [NgComponentOutlet, AsyncPipe]
|
|
||||||
})
|
})
|
||||||
@rendersSectionForMenu(MenuID.PUBLIC, false)
|
@rendersSectionForMenu(MenuID.PUBLIC, false)
|
||||||
export class NavbarSectionComponent extends MenuSectionComponent implements OnInit {
|
export class NavbarSectionComponent extends MenuSectionComponent implements OnInit {
|
||||||
|
@@ -6,11 +6,11 @@
|
|||||||
<div id="collapsingNav">
|
<div id="collapsingNav">
|
||||||
<ul class="navbar-nav navbar-navigation mr-auto shadow-none">
|
<ul class="navbar-nav navbar-navigation mr-auto shadow-none">
|
||||||
<li *ngIf="(isXsOrSm$ | async) && (isAuthenticated$ | async)">
|
<li *ngIf="(isXsOrSm$ | async) && (isAuthenticated$ | async)">
|
||||||
<ds-user-menu [inExpandableNavbar]="true"></ds-user-menu>
|
<ds-themed-user-menu [inExpandableNavbar]="true"></ds-themed-user-menu>
|
||||||
</li>
|
</li>
|
||||||
<ng-container *ngFor="let section of (sections | async)">
|
<li *ngFor="let section of (sections | async)">
|
||||||
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
||||||
</ng-container>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
nav.navbar {
|
nav.navbar {
|
||||||
border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid;
|
background-color: var(--ds-navbar-bg);
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,9 +11,11 @@ nav.navbar {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
z-index: var(--ds-nav-z-index);
|
||||||
&.open {
|
&.open {
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 100vh; //doesn't matter because wrapper is sticky
|
min-height: 100vh; //doesn't matter because wrapper is sticky
|
||||||
|
border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid; // open navbar covers header-navbar-wrapper border
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,8 +40,9 @@ nav.navbar {
|
|||||||
.navbar-nav {
|
.navbar-nav {
|
||||||
::ng-deep a.nav-link {
|
::ng-deep a.nav-link {
|
||||||
color: var(--ds-navbar-link-color);
|
color: var(--ds-navbar-link-color);
|
||||||
}
|
|
||||||
::ng-deep a.nav-link:hover {
|
&:hover, &:focus {
|
||||||
color: var(--ds-navbar-link-color-hover);
|
color: var(--ds-navbar-link-color-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { UserMenuComponent } from '../shared/auth-nav-menu/user-menu/user-menu.component';
|
import { UserMenuComponent } from '../shared/auth-nav-menu/user-menu/user-menu.component';
|
||||||
import { NgClass, NgIf, NgFor, NgComponentOutlet, AsyncPipe } from '@angular/common';
|
import { NgClass, NgIf, NgFor, NgComponentOutlet, AsyncPipe } from '@angular/common';
|
||||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ThemedUserMenuComponent } from '../shared/auth-nav-menu/user-menu/themed-user-menu.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing the public navbar
|
* Component representing the public navbar
|
||||||
@@ -26,7 +27,7 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
templateUrl: './navbar.component.html',
|
templateUrl: './navbar.component.html',
|
||||||
animations: [slideMobileNav],
|
animations: [slideMobileNav],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgbDropdownModule, NgClass, NgIf, UserMenuComponent, NgFor, NgComponentOutlet, AsyncPipe, TranslateModule]
|
imports: [NgbDropdownModule, NgClass, NgIf, UserMenuComponent, ThemedUserMenuComponent, NgFor, NgComponentOutlet, AsyncPipe, TranslateModule]
|
||||||
})
|
})
|
||||||
export class NavbarComponent extends MenuComponent {
|
export class NavbarComponent extends MenuComponent {
|
||||||
/**
|
/**
|
||||||
|
@@ -2,16 +2,22 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { RegisterEmailFormComponent } from './register-email-form.component';
|
import { RegisterEmailFormComponent } from './register-email-form.component';
|
||||||
|
import { ThemedRegisterEmailFormComponent } from './themed-registry-email-form.component';
|
||||||
|
|
||||||
|
const DECLARATIONS = [
|
||||||
|
RegisterEmailFormComponent,
|
||||||
|
ThemedRegisterEmailFormComponent,
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
RegisterEmailFormComponent
|
...DECLARATIONS,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [
|
exports: [
|
||||||
RegisterEmailFormComponent,
|
...DECLARATIONS,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -0,0 +1,37 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||||
|
import { RegisterEmailFormComponent } from './register-email-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for {@link RegisterEmailFormComponent}
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-register-email-form',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../shared/theme-support/themed.component.html',
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class ThemedRegisterEmailFormComponent extends ThemedComponent<RegisterEmailFormComponent> {
|
||||||
|
|
||||||
|
@Input() MESSAGE_PREFIX: string;
|
||||||
|
|
||||||
|
@Input() typeRequest: string;
|
||||||
|
|
||||||
|
protected inAndOutputNames: (keyof RegisterEmailFormComponent & keyof this)[] = [
|
||||||
|
'MESSAGE_PREFIX',
|
||||||
|
'typeRequest',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'RegisterEmailFormComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../themes/${themeName}/app/register-email-form/register-email-form.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./register-email-form.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user