mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-16 06:23:03 +00:00
Merge remote-tracking branch 'upstream/main' into w2p-98855_themeable-file-download-link_contribute-main
# Conflicts: # src/app/shared/shared.module.ts # src/app/shared/theme-support/themed.component.ts # src/themes/custom/eager-theme.module.ts
This commit is contained in:
@@ -10,6 +10,16 @@ import { MembersListComponent } from './group-registry/group-form/members-list/m
|
||||
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
|
||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||
import { FormModule } from '../shared/form/form.module';
|
||||
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
|
||||
/**
|
||||
* Condition for displaying error messages on email form field
|
||||
*/
|
||||
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
||||
(control: AbstractControl, model: any, hasFocus: boolean) => {
|
||||
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
|
||||
};
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -26,6 +36,12 @@ import { FormModule } from '../shared/form/form.module';
|
||||
GroupFormComponent,
|
||||
SubgroupsListComponent,
|
||||
MembersListComponent
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||
useValue: ValidateEmailErrorStateMatcher
|
||||
},
|
||||
]
|
||||
})
|
||||
/**
|
||||
|
@@ -9,7 +9,18 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.edit' | translate}}</h2>
|
||||
<h2 class="border-bottom pb-2">
|
||||
<span
|
||||
*dsContextHelp="{
|
||||
content: 'admin.access-control.groups.form.tooltip.editGroupPage',
|
||||
id: 'edit-group-page',
|
||||
iconPlacement: 'right',
|
||||
tooltipPlacement: ['right', 'bottom']
|
||||
}"
|
||||
>
|
||||
{{messagePrefix + '.head.edit' | translate}}
|
||||
</span>
|
||||
</h2>
|
||||
</ng-template>
|
||||
|
||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
||||
|
@@ -266,6 +266,43 @@ describe('GroupFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should edit with name and description operations', () => {
|
||||
const operations = [{
|
||||
op: 'add',
|
||||
path: '/metadata/dc.description',
|
||||
value: 'testDescription'
|
||||
}, {
|
||||
op: 'replace',
|
||||
path: '/name',
|
||||
value: 'newGroupName'
|
||||
}];
|
||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||
});
|
||||
|
||||
it('should edit with description operations', () => {
|
||||
component.groupName.value = null;
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
const operations = [{
|
||||
op: 'add',
|
||||
path: '/metadata/dc.description',
|
||||
value: 'testDescription'
|
||||
}];
|
||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||
});
|
||||
|
||||
it('should edit with name operations', () => {
|
||||
component.groupDescription.value = null;
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
const operations = [{
|
||||
op: 'replace',
|
||||
path: '/name',
|
||||
value: 'newGroupName'
|
||||
}];
|
||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||
});
|
||||
|
||||
it('should emit the existing group using the correct new values', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||
|
@@ -46,6 +46,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-group-form',
|
||||
@@ -194,6 +195,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
label: groupDescription,
|
||||
name: 'groupDescription',
|
||||
required: false,
|
||||
spellCheck: environment.form.spellCheck,
|
||||
});
|
||||
this.formModel = [
|
||||
this.groupName,
|
||||
@@ -344,8 +346,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (hasValue(this.groupDescription.value)) {
|
||||
operations = [...operations, {
|
||||
op: 'replace',
|
||||
path: '/metadata/dc.description/0/value',
|
||||
op: 'add',
|
||||
path: '/metadata/dc.description',
|
||||
value: this.groupDescription.value
|
||||
}];
|
||||
}
|
||||
|
@@ -1,9 +1,19 @@
|
||||
<ng-container>
|
||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||
|
||||
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
|
||||
|
||||
<h4 id="search" class="border-bottom pb-2">
|
||||
<span
|
||||
*dsContextHelp="{
|
||||
content: 'admin.access-control.groups.form.tooltip.editGroup.addEpeople',
|
||||
id: 'edit-group-add-epeople',
|
||||
iconPlacement: 'right',
|
||||
tooltipPlacement: ['top', 'right', 'bottom']
|
||||
}"
|
||||
>
|
||||
{{messagePrefix + '.search.head' | translate}}
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div>
|
||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||
|
@@ -1,7 +1,16 @@
|
||||
<ng-container>
|
||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||
|
||||
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
|
||||
<h4 id="search" class="border-bottom pb-2">
|
||||
<span *dsContextHelp="{
|
||||
content: 'admin.access-control.groups.form.tooltip.editGroup.addSubgroups',
|
||||
id: 'edit-group-add-subgroups',
|
||||
iconPlacement: 'right',
|
||||
tooltipPlacement: ['top', 'right', 'bottom']
|
||||
}"
|
||||
>
|
||||
{{messagePrefix + '.search.head' | translate}}
|
||||
</span>
|
||||
|
||||
</h4>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
|
@@ -1,12 +1,8 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
|
@@ -13,32 +13,34 @@
|
||||
[paginationOptions]="pageConfig"
|
||||
[pageInfoState]="(bitstreamFormats | async)?.payload"
|
||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="formats" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
[checked]="isSelected(bitstreamFormat) | async"
|
||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||
>
|
||||
</label>
|
||||
</td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
|
||||
</tr>
|
||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
[checked]="isSelected(bitstreamFormat) | async"
|
||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||
>
|
||||
</label>
|
||||
</td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@@ -129,16 +129,19 @@ describe('BitstreamFormatsComponent', () => {
|
||||
});
|
||||
|
||||
it('should contain the correct formats', () => {
|
||||
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
|
||||
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(3)')).nativeElement;
|
||||
expect(unknownName.textContent).toBe('Unknown');
|
||||
|
||||
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(2)')).nativeElement;
|
||||
const UUID: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
|
||||
expect(UUID.textContent).toBe('test-uuid-1');
|
||||
|
||||
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(3)')).nativeElement;
|
||||
expect(licenseName.textContent).toBe('License');
|
||||
|
||||
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(2)')).nativeElement;
|
||||
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(3)')).nativeElement;
|
||||
expect(ccLicenseName.textContent).toBe('CC License');
|
||||
|
||||
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(2)')).nativeElement;
|
||||
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(3)')).nativeElement;
|
||||
expect(adobeName.textContent).toBe('Adobe PDF');
|
||||
});
|
||||
});
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, Observable} from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||
import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@@ -29,21 +28,14 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||
|
||||
/**
|
||||
* The current pagination configuration for the page used by the FindAll method
|
||||
* Currently simply renders all bitstream formats
|
||||
*/
|
||||
config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||
elementsPerPage: 20
|
||||
});
|
||||
|
||||
/**
|
||||
* The current pagination configuration for the page
|
||||
* Currently simply renders all bitstream formats
|
||||
*/
|
||||
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'rbp',
|
||||
pageSize: 20
|
||||
pageSize: 20,
|
||||
pageSizeOptions: [20, 40, 60, 80, 100]
|
||||
});
|
||||
|
||||
constructor(private notificationsService: NotificationsService,
|
||||
@@ -51,7 +43,7 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
private translateService: TranslateService,
|
||||
private bitstreamFormatService: BitstreamFormatDataService,
|
||||
private paginationService: PaginationService,
|
||||
) {
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +141,7 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
|
||||
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
|
||||
switchMap((findListOptions: FindListOptions) => {
|
||||
return this.bitstreamFormatService.findAll(findListOptions);
|
||||
})
|
||||
|
@@ -15,6 +15,7 @@ import { Router } from '@angular/router';
|
||||
import { hasValue, isEmpty } from '../../../../shared/empty.util';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths';
|
||||
import { environment } from '../../../../../environments/environment';
|
||||
|
||||
/**
|
||||
* The component responsible for rendering the form to create/edit a bitstream format
|
||||
@@ -90,6 +91,7 @@ export class FormatFormComponent implements OnInit {
|
||||
name: 'description',
|
||||
label: 'admin.registries.bitstream-formats.edit.description.label',
|
||||
hint: 'admin.registries.bitstream-formats.edit.description.hint',
|
||||
spellCheck: environment.form.spellCheck,
|
||||
|
||||
}),
|
||||
new DynamicSelectModel({
|
||||
|
@@ -19,10 +19,7 @@ import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
|
||||
describe('MetadataRegistryComponent', () => {
|
||||
let comp: MetadataRegistryComponent;
|
||||
|
@@ -25,6 +25,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
|
||||
</tr>
|
||||
@@ -39,6 +40,7 @@
|
||||
(change)="selectMetadataField(field, $event)">
|
||||
</label>
|
||||
</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">.</label>{{field.qualifier}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||
</tr>
|
||||
|
@@ -23,11 +23,8 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
|
||||
describe('MetadataSchemaComponent', () => {
|
||||
let comp: MetadataSchemaComponent;
|
||||
@@ -169,10 +166,10 @@ describe('MetadataSchemaComponent', () => {
|
||||
});
|
||||
|
||||
it('should contain the correct fields', () => {
|
||||
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(2)')).nativeElement;
|
||||
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(3)')).nativeElement;
|
||||
expect(editorField.textContent).toBe('mock.contributor.editor');
|
||||
|
||||
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(2)')).nativeElement;
|
||||
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(3)')).nativeElement;
|
||||
expect(illustratorField.textContent).toBe('mock.contributor.illustrator');
|
||||
});
|
||||
|
||||
|
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
||||
import { Component } from '@angular/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
@@ -6,7 +6,7 @@ import { ScriptDataService } from '../../core/data/processes/script-data.service
|
||||
import { AdminSidebarComponent } from './admin-sidebar.component';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
|
||||
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service.stub';
|
||||
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
@@ -16,7 +16,6 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import createSpy = jasmine.createSpy;
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
@@ -5,7 +5,7 @@ import { AuthService } from '../../core/auth/auth.service';
|
||||
import { slideSidebar } from '../../shared/animations/slide';
|
||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
@@ -69,7 +69,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
||||
this.sidebarWidth = this.variableService.getVariable('--ds-sidebar-items-width');
|
||||
this.authService.isAuthenticated()
|
||||
.subscribe((loggedIn: boolean) => {
|
||||
if (loggedIn) {
|
||||
@@ -100,6 +100,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
});
|
||||
this.menuVisible = this.menuService.isMenuVisibleWithVisibleSections(this.menuID);
|
||||
}
|
||||
|
||||
@HostListener('focusin')
|
||||
|
@@ -3,7 +3,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { ExpandableAdminSidebarSectionComponent } from './expandable-admin-sidebar-section.component';
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Component } from '@angular/core';
|
||||
|
@@ -2,7 +2,7 @@ import { Component, Inject, Injector, OnInit } from '@angular/core';
|
||||
import { rotate } from '../../../shared/animations/rotate';
|
||||
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
|
||||
import { slide } from '../../../shared/animations/slide';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||
import { bgColor } from '../../../shared/animations/bgColor';
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||
@@ -65,7 +65,7 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.sidebarActiveBg = this.variableService.getVariable('adminSidebarActiveBg');
|
||||
this.sidebarActiveBg = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
||||
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||
this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)
|
||||
|
@@ -10,6 +10,7 @@ import { AdminSearchModule } from './admin-search-page/admin-search.module';
|
||||
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||
import { UploadModule } from '../shared/upload/upload.module';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -25,7 +26,8 @@ const ENTRY_COMPONENTS = [
|
||||
AccessControlModule,
|
||||
AdminSearchModule.withEntryComponents(),
|
||||
AdminWorkflowModuleModule.withEntryComponents(),
|
||||
SharedModule
|
||||
SharedModule,
|
||||
UploadModule,
|
||||
],
|
||||
declarations: [
|
||||
AdminCurationTasksComponent,
|
||||
|
@@ -18,7 +18,7 @@ import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.ser
|
||||
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableService } from './shared/sass-helper/css-variable.service';
|
||||
import { CSSVariableServiceStub } from './shared/testing/css-variable-service.stub';
|
||||
import { MenuServiceStub } from './shared/testing/menu-service.stub';
|
||||
import { HostWindowService } from './shared/host-window.service';
|
||||
|
@@ -25,13 +25,12 @@ import { HostWindowState } from './shared/search/host-window.reducer';
|
||||
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
||||
import { isAuthenticationBlocking } from './core/auth/selectors';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableService } from './shared/sass-helper/css-variable.service';
|
||||
import { environment } from '../environments/environment';
|
||||
import { models } from './core/core.module';
|
||||
import { ThemeService } from './shared/theme-support/theme.service';
|
||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||
import { distinctNext } from './core/shared/distinct-next';
|
||||
import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
@@ -110,18 +109,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
private storeCSSVariables() {
|
||||
this.cssService.addCSSVariable('xlMin', '1200px');
|
||||
this.cssService.addCSSVariable('mdMin', '768px');
|
||||
this.cssService.addCSSVariable('lgMin', '576px');
|
||||
this.cssService.addCSSVariable('smMin', '0');
|
||||
this.cssService.addCSSVariable('adminSidebarActiveBg', '#0f1b28');
|
||||
this.cssService.addCSSVariable('sidebarItemsWidth', '250px');
|
||||
this.cssService.addCSSVariable('collapsedSidebarWidth', '53.234px');
|
||||
this.cssService.addCSSVariable('totalSidebarWidth', '303.234px');
|
||||
// const vars = variables.locals || {};
|
||||
// Object.keys(vars).forEach((name: string) => {
|
||||
// this.cssService.addCSSVariable(name, vars[name]);
|
||||
// })
|
||||
this.cssService.clearCSSVariables();
|
||||
this.cssService.addCSSVariables(this.cssService.getCSSVariablesFromStylesheets(this.document));
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
|
@@ -1,14 +1,12 @@
|
||||
import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
|
||||
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DYNAMIC_MATCHER_PROVIDERS, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
@@ -28,7 +26,6 @@ import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
|
||||
import { LogInterceptor } from './core/log/log.interceptor';
|
||||
import { EagerThemesModule } from '../themes/eager-themes.module';
|
||||
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
||||
import { NgxMaskModule } from 'ngx-mask';
|
||||
import { StoreDevModules } from '../config/store/devtools';
|
||||
import { RootModule } from './root.module';
|
||||
|
||||
@@ -46,14 +43,6 @@ export function getMetaReducers(appConfig: AppConfig): MetaReducer<AppState>[] {
|
||||
return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition for displaying error messages on email form field
|
||||
*/
|
||||
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
||||
(control: AbstractControl, model: any, hasFocus: boolean) => {
|
||||
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
|
||||
};
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
@@ -64,7 +53,6 @@ const IMPORTS = [
|
||||
ScrollToModule.forRoot(),
|
||||
NgbModule,
|
||||
TranslateModule.forRoot(),
|
||||
NgxMaskModule.forRoot(),
|
||||
EffectsModule.forRoot(appEffects),
|
||||
StoreModule.forRoot(appReducers, storeModuleConfig),
|
||||
StoreRouterConnectingModule.forRoot(),
|
||||
@@ -113,11 +101,6 @@ const PROVIDERS = [
|
||||
useClass: LogInterceptor,
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||
useValue: ValidateEmailErrorStateMatcher
|
||||
},
|
||||
...DYNAMIC_MATCHER_PROVIDERS,
|
||||
];
|
||||
|
||||
const DECLARATIONS = [
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
import { routerReducer, RouterReducerState } from '@ngrx/router-store';
|
||||
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
||||
import {
|
||||
ePeopleRegistryReducer,
|
||||
@@ -35,31 +35,27 @@ import {
|
||||
ObjectSelectionListState,
|
||||
objectSelectionReducer
|
||||
} from './shared/object-select/object-select.reducer';
|
||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/css-variable.reducer';
|
||||
|
||||
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
||||
import {
|
||||
filterReducer,
|
||||
SearchFiltersState
|
||||
} from './shared/search/search-filters/search-filter/search-filter.reducer';
|
||||
import {
|
||||
sidebarFilterReducer,
|
||||
SidebarFiltersState
|
||||
} from './shared/sidebar/filter/sidebar-filter.reducer';
|
||||
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
|
||||
import { MenusState } from './shared/menu/menus-state.model';
|
||||
import { correlationIdReducer } from './correlation-id/correlation-id.reducer';
|
||||
import { contextHelpReducer, ContextHelpState } from './shared/context-help.reducer';
|
||||
|
||||
export interface AppState {
|
||||
router: fromRouter.RouterReducerState;
|
||||
router: RouterReducerState;
|
||||
hostWindow: HostWindowState;
|
||||
forms: FormState;
|
||||
metadataRegistry: MetadataRegistryState;
|
||||
notifications: NotificationsState;
|
||||
sidebar: SidebarState;
|
||||
sidebarFilter: SidebarFiltersState;
|
||||
searchFilter: SearchFiltersState;
|
||||
truncatable: TruncatablesState;
|
||||
cssVariables: CSSVariablesState;
|
||||
@@ -72,16 +68,16 @@ export interface AppState {
|
||||
epeopleRegistry: EPeopleRegistryState;
|
||||
groupRegistry: GroupRegistryState;
|
||||
correlationId: string;
|
||||
contextHelp: ContextHelpState;
|
||||
}
|
||||
|
||||
export const appReducers: ActionReducerMap<AppState> = {
|
||||
router: fromRouter.routerReducer,
|
||||
router: routerReducer,
|
||||
hostWindow: hostWindowReducer,
|
||||
forms: formReducer,
|
||||
metadataRegistry: metadataRegistryReducer,
|
||||
notifications: notificationsReducer,
|
||||
sidebar: sidebarReducer,
|
||||
sidebarFilter: sidebarFilterReducer,
|
||||
searchFilter: filterReducer,
|
||||
truncatable: truncatableReducer,
|
||||
cssVariables: cssVariablesReducer,
|
||||
@@ -93,7 +89,8 @@ export const appReducers: ActionReducerMap<AppState> = {
|
||||
communityList: CommunityListReducer,
|
||||
epeopleRegistry: ePeopleRegistryReducer,
|
||||
groupRegistry: groupRegistryReducer,
|
||||
correlationId: correlationIdReducer
|
||||
correlationId: correlationIdReducer,
|
||||
contextHelp: contextHelpReducer,
|
||||
};
|
||||
|
||||
export const routerStateSelector = (state: AppState) => state.router;
|
||||
|
@@ -6,7 +6,7 @@ import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { BitstreamDownloadPageComponent } from './bitstream-download-page.component';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { getForbiddenRoute } from '../../app-routing-paths';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
@@ -1,7 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { hasValue, isNotEmpty } from '../empty.util';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { getRemoteDataPayload} from '../../core/shared/operators';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
@@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
|
||||
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { BitstreamPageResolver } from './bitstream-page.resolver';
|
||||
import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component';
|
||||
import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component';
|
||||
import { ResourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver';
|
||||
import { ResourcePolicyCreateComponent } from '../shared/resource-policies/create/resource-policy-create.component';
|
||||
import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||
|
@@ -6,6 +6,7 @@ import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
|
||||
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
||||
import { FormModule } from '../shared/form/form.module';
|
||||
import { ResourcePoliciesModule } from '../shared/resource-policies/resource-policies.module';
|
||||
import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component';
|
||||
|
||||
/**
|
||||
* This module handles all components that are necessary for Bitstream related pages
|
||||
@@ -20,7 +21,8 @@ import { ResourcePoliciesModule } from '../shared/resource-policies/resource-pol
|
||||
],
|
||||
declarations: [
|
||||
BitstreamAuthorizationsComponent,
|
||||
EditBitstreamPageComponent
|
||||
EditBitstreamPageComponent,
|
||||
BitstreamDownloadPageComponent,
|
||||
]
|
||||
})
|
||||
export class BitstreamPageModule {
|
||||
|
@@ -26,7 +26,7 @@ import {
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import {
|
||||
getAllSucceededRemoteDataPayload,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
|
||||
import { of as observableOf, EMPTY } from 'rxjs';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
@@ -10,7 +10,7 @@
|
||||
</nav>
|
||||
|
||||
<ng-template #breadcrumb let-text="text" let-url="url">
|
||||
<li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate">{{text | translate}}</a></div></li>
|
||||
<li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate" [ngbTooltip]="text | translate" placement="bottom" >{{text | translate}}</a></div></li>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #activeBreadcrumb let-text="text">
|
||||
|
@@ -23,11 +23,14 @@ li.breadcrumb-item {
|
||||
}
|
||||
}
|
||||
|
||||
li.breadcrumb-item > a {
|
||||
color: var(--ds-breadcrumb-link-color) !important;
|
||||
li.breadcrumb-item {
|
||||
a {
|
||||
color: var(--ds-breadcrumb-link-color);
|
||||
}
|
||||
}
|
||||
|
||||
li.breadcrumb-item.active {
|
||||
color: var(--ds-breadcrumb-link-active-color) !important;
|
||||
color: var(--ds-breadcrumb-link-active-color);
|
||||
}
|
||||
|
||||
.breadcrumb-item+ .breadcrumb-item::before {
|
||||
|
@@ -65,6 +65,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails);
|
||||
this.updatePageWithItems(searchOptions, this.value, undefined);
|
||||
this.updateParent(params.scope);
|
||||
this.updateLogo();
|
||||
this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
|
||||
}));
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { first } from 'rxjs/operators';
|
||||
import { BrowseByGuard } from './browse-by-guard';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { BrowseDefinition } from '../core/shared/browse-definition.model';
|
||||
import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator';
|
||||
|
@@ -5,6 +5,11 @@
|
||||
<!-- Parent Name -->
|
||||
<ds-comcol-page-header [name]="parentContext.name">
|
||||
</ds-comcol-page-header>
|
||||
<!-- Collection logo -->
|
||||
<ds-comcol-page-logo *ngIf="logo$"
|
||||
[logo]="(logo$ | async)?.payload"
|
||||
[alternateText]="'Community or Collection Logo'">
|
||||
</ds-comcol-page-logo>
|
||||
<!-- Handle -->
|
||||
<ds-themed-comcol-page-handle
|
||||
[content]="parentContext.handle"
|
||||
|
@@ -144,6 +144,9 @@ describe('BrowseByMetadataPageComponent', () => {
|
||||
|
||||
route.params = observableOf(paramsWithValue);
|
||||
comp.ngOnInit();
|
||||
comp.updateParent('fake-scope');
|
||||
comp.updateLogo();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should fetch items', () => {
|
||||
@@ -151,6 +154,10 @@ describe('BrowseByMetadataPageComponent', () => {
|
||||
expect(result.payload.page).toEqual(mockItems);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch the logo', () => {
|
||||
expect(comp.logo$).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when calling browseParamsToOptions', () => {
|
||||
|
@@ -15,7 +15,11 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { filter, map, mergeMap } from 'rxjs/operators';
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||
|
||||
export const BBM_PAGINATION_ID = 'bbm';
|
||||
@@ -48,6 +52,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
parent$: Observable<RemoteData<DSpaceObject>>;
|
||||
|
||||
/**
|
||||
* The logo of the current Community or Collection
|
||||
*/
|
||||
logo$: Observable<RemoteData<Bitstream>>;
|
||||
|
||||
/**
|
||||
* The pagination config used to display the values
|
||||
*/
|
||||
@@ -151,6 +160,7 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||
this.updatePage(browseParamsToOptions(params, currentPage, currentSort, this.browseId, false));
|
||||
}
|
||||
this.updateParent(params.scope);
|
||||
this.updateLogo();
|
||||
}));
|
||||
this.updateStartsWithTextOptions();
|
||||
|
||||
@@ -196,12 +206,31 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
updateParent(scope: string) {
|
||||
if (hasValue(scope)) {
|
||||
this.parent$ = this.dsoService.findById(scope).pipe(
|
||||
const linksToFollow = () => {
|
||||
return [followLink('logo')];
|
||||
};
|
||||
this.parent$ = this.dsoService.findById(scope,
|
||||
true,
|
||||
true,
|
||||
...linksToFollow() as FollowLinkConfig<DSpaceObject>[]).pipe(
|
||||
getFirstSucceededRemoteData()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the parent Community or Collection logo
|
||||
*/
|
||||
updateLogo() {
|
||||
if (hasValue(this.parent$)) {
|
||||
this.logo$ = this.parent$.pipe(
|
||||
map((rd: RemoteData<Collection | Community>) => rd.payload),
|
||||
filter((collectionOrCommunity: Collection | Community) => hasValue(collectionOrCommunity.logo)),
|
||||
mergeMap((collectionOrCommunity: Collection | Community) => collectionOrCommunity.logo)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the previous page
|
||||
*/
|
||||
|
@@ -4,16 +4,21 @@ import { BrowseByModule } from './browse-by.module';
|
||||
import { ItemDataService } from '../core/data/item-data.service';
|
||||
import { BrowseService } from '../core/browse/browse.service';
|
||||
import { BrowseByGuard } from './browse-by-guard';
|
||||
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedBrowseByModule,
|
||||
BrowseByRoutingModule,
|
||||
BrowseByModule.withEntryComponents()
|
||||
BrowseByModule.withEntryComponents(),
|
||||
],
|
||||
providers: [
|
||||
ItemDataService,
|
||||
BrowseService,
|
||||
BrowseByGuard
|
||||
BrowseByGuard,
|
||||
],
|
||||
declarations: [
|
||||
|
||||
]
|
||||
})
|
||||
export class BrowseByPageModule {
|
||||
|
@@ -49,6 +49,7 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
|
||||
this.updateParent(params.scope);
|
||||
this.updateLogo();
|
||||
}));
|
||||
this.updateStartsWithTextOptions();
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-title-page.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component';
|
||||
import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
|
||||
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
|
||||
@@ -10,6 +9,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||
import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component';
|
||||
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
|
||||
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
|
||||
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -25,9 +25,9 @@ const ENTRY_COMPONENTS = [
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedBrowseByModule,
|
||||
CommonModule,
|
||||
ComcolModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
BrowseBySwitcherComponent,
|
||||
@@ -45,7 +45,7 @@ export class BrowseByModule {
|
||||
*/
|
||||
static withEntryComponents() {
|
||||
return {
|
||||
ngModule: SharedModule,
|
||||
ngModule: SharedBrowseByModule,
|
||||
providers: ENTRY_COMPONENTS.map((component) => ({provide: component}))
|
||||
};
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
|
||||
import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<string> = {
|
||||
id: 'entityType',
|
||||
@@ -26,21 +27,26 @@ export const collectionFormModels: DynamicFormControlModel[] = [
|
||||
new DynamicTextAreaModel({
|
||||
id: 'description',
|
||||
name: 'dc.description',
|
||||
spellCheck: environment.form.spellCheck,
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'abstract',
|
||||
name: 'dc.description.abstract',
|
||||
spellCheck: environment.form.spellCheck,
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'rights',
|
||||
name: 'dc.rights',
|
||||
spellCheck: environment.form.spellCheck,
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'tableofcontents',
|
||||
name: 'dc.description.tableofcontents',
|
||||
spellCheck: environment.form.spellCheck,
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'license',
|
||||
name: 'dc.rights.license',
|
||||
spellCheck: environment.form.spellCheck,
|
||||
})
|
||||
];
|
||||
|
@@ -16,7 +16,7 @@ import { Collection } from '../../core/shared/collection.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { ChangeDetectionStrategy, EventEmitter } from '@angular/core';
|
||||
import { EventEmitter } from '@angular/core';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
||||
import { By } from '@angular/platform-browser';
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
|
||||
import { GroupDataService } from '../../core/eperson/group-data.service';
|
||||
import { LinkHeadService } from '../../core/services/link-head.service';
|
||||
|
@@ -72,6 +72,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||
id: 'statistics_collection_:id',
|
||||
active: true,
|
||||
visible: true,
|
||||
index: 2,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics',
|
||||
|
@@ -16,6 +16,7 @@ import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { CollectionFormModule } from './collection-form/collection-form.module';
|
||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||
import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||
import { DsoSharedModule } from '../dso-shared/dso-shared.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -25,7 +26,8 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||
StatisticsModule.forRoot(),
|
||||
EditItemPageModule,
|
||||
CollectionFormModule,
|
||||
ComcolModule
|
||||
ComcolModule,
|
||||
DsoSharedModule,
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
@@ -38,7 +40,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||
],
|
||||
providers: [
|
||||
SearchService,
|
||||
]
|
||||
],
|
||||
})
|
||||
export class CollectionPageModule {
|
||||
|
||||
|
@@ -11,11 +11,11 @@
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-weight-bold">{{'collection.source.controls.harvest.last' | translate}}</span>
|
||||
<span>{{contentSource?.message ? contentSource?.message : 'collection.source.controls.harvest.no-information'|translate }}</span>
|
||||
<span>{{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-weight-bold">{{'collection.source.controls.harvest.message' | translate}}</span>
|
||||
<span>{{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}</span>
|
||||
<span>{{contentSource?.message ? contentSource?.message: 'collection.source.controls.harvest.no-information'|translate }}</span>
|
||||
</div>
|
||||
|
||||
<button *ngIf="!(testConfigRunning$ |async)" class="btn btn-secondary"
|
||||
|
@@ -8,8 +8,7 @@ import {
|
||||
DynamicInputModel,
|
||||
DynamicOptionControlModel,
|
||||
DynamicRadioGroupModel,
|
||||
DynamicSelectModel,
|
||||
DynamicTextAreaModel
|
||||
DynamicSelectModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@@ -23,7 +22,7 @@ import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { first, map, switchMap, take } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { MetadataConfig } from '../../../core/shared/metadata-config.model';
|
||||
|
@@ -25,7 +25,7 @@ import { ComcolModule } from '../../shared/comcol/comcol.module';
|
||||
CollectionFormModule,
|
||||
ResourcePoliciesModule,
|
||||
FormModule,
|
||||
ComcolModule
|
||||
ComcolModule,
|
||||
],
|
||||
declarations: [
|
||||
EditCollectionPageComponent,
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<ng-container *ngIf="itemRD?.hasSucceeded">
|
||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||
<ds-themed-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-themed-item-metadata>
|
||||
<ds-themed-dso-edit-metadata [updateDataService]="itemTemplateService" [dso]="itemRD?.payload"></ds-themed-dso-edit-metadata>
|
||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||
</ng-container>
|
||||
<ds-themed-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-themed-loading>
|
||||
|
@@ -6,6 +6,7 @@ import { CommunityListPageRoutingModule } from './community-list-page.routing.mo
|
||||
import { CommunityListComponent } from './community-list/community-list.component';
|
||||
import { ThemedCommunityListPageComponent } from './themed-community-list-page.component';
|
||||
import { ThemedCommunityListComponent } from './community-list/themed-community-list.component';
|
||||
import { CdkTreeModule } from '@angular/cdk/tree';
|
||||
|
||||
|
||||
const DECLARATIONS = [
|
||||
@@ -21,13 +22,15 @@ const DECLARATIONS = [
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CommunityListPageRoutingModule
|
||||
CommunityListPageRoutingModule,
|
||||
CdkTreeModule,
|
||||
],
|
||||
declarations: [
|
||||
...DECLARATIONS
|
||||
],
|
||||
exports: [
|
||||
...DECLARATIONS,
|
||||
CdkTreeModule,
|
||||
],
|
||||
})
|
||||
export class CommunityListPageModule {
|
||||
|
@@ -13,6 +13,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
/**
|
||||
* Form used for creating and editing communities
|
||||
@@ -52,18 +53,22 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
|
||||
new DynamicTextAreaModel({
|
||||
id: 'description',
|
||||
name: 'dc.description',
|
||||
spellCheck: environment.form.spellCheck,
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'abstract',
|
||||
name: 'dc.description.abstract',
|
||||
spellCheck: environment.form.spellCheck,
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'rights',
|
||||
name: 'dc.rights',
|
||||
spellCheck: environment.form.spellCheck,
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'tableofcontents',
|
||||
name: 'dc.description.tableofcontents',
|
||||
spellCheck: environment.form.spellCheck,
|
||||
}),
|
||||
];
|
||||
|
||||
|
@@ -55,6 +55,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||
id: 'statistics_community_:id',
|
||||
active: true,
|
||||
visible: true,
|
||||
index: 2,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics',
|
||||
|
@@ -36,7 +36,7 @@ const DECLARATIONS = [CommunityPageComponent,
|
||||
CommunityPageRoutingModule,
|
||||
StatisticsModule.forRoot(),
|
||||
CommunityFormModule,
|
||||
ComcolModule
|
||||
ComcolModule,
|
||||
],
|
||||
declarations: [
|
||||
...DECLARATIONS
|
||||
|
@@ -21,7 +21,7 @@ import { ComcolModule } from '../../shared/comcol/comcol.module';
|
||||
EditCommunityPageRoutingModule,
|
||||
CommunityFormModule,
|
||||
ComcolModule,
|
||||
ResourcePoliciesModule
|
||||
ResourcePoliciesModule,
|
||||
],
|
||||
declarations: [
|
||||
EditCommunityPageComponent,
|
||||
|
@@ -17,9 +17,6 @@ import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
||||
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
@@ -29,7 +26,6 @@ import { GroupDataService } from '../../core/eperson/group-data.service';
|
||||
import { LinkHeadService } from '../../core/services/link-head.service';
|
||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
|
||||
import { SearchServiceStub } from '../../shared/testing/search-service.stub';
|
||||
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
|
||||
|
@@ -17,9 +17,6 @@ import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
|
@@ -196,7 +196,24 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
authStatus.token = new AuthTokenInfo(accessToken);
|
||||
} else {
|
||||
authStatus.authenticated = false;
|
||||
authStatus.error = isNotEmpty(error) ? ((typeof error === 'string') ? JSON.parse(error) : error) : null;
|
||||
if (isNotEmpty(error)) {
|
||||
if (typeof error === 'string') {
|
||||
try {
|
||||
authStatus.error = JSON.parse(error);
|
||||
} catch (e) {
|
||||
console.error('Unknown auth error "', error, '" caused ', e);
|
||||
authStatus.error = {
|
||||
error: 'Unknown',
|
||||
message: 'Unknown auth error',
|
||||
status: 500,
|
||||
timestamp: Date.now(),
|
||||
path: ''
|
||||
};
|
||||
}
|
||||
} else {
|
||||
authStatus.error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
return authStatus;
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ import { createSelector } from '@ngrx/store';
|
||||
* notation packages up all of the exports into a single object.
|
||||
*/
|
||||
import { AuthState } from './auth.reducer';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
|
||||
|
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { hasValue, isEmpty } from '../../shared/empty.util';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Metadata } from '../shared/metadata.utils';
|
||||
|
||||
/**
|
||||
* Returns a name for a {@link DSpaceObject} based
|
||||
@@ -67,4 +68,45 @@ export class DSONameService {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Hit highlight
|
||||
*
|
||||
* @param object
|
||||
* @param dso
|
||||
*
|
||||
* @returns {string} html embedded hit highlight.
|
||||
*/
|
||||
getHitHighlights(object: any, dso: DSpaceObject): string {
|
||||
const types = dso.getRenderTypes();
|
||||
const entityType = types
|
||||
.filter((type) => typeof type === 'string')
|
||||
.find((type: string) => (['Person', 'OrgUnit']).includes(type)) as string;
|
||||
if (entityType === 'Person') {
|
||||
const familyName = this.firstMetadataValue(object, dso, 'person.familyName');
|
||||
const givenName = this.firstMetadataValue(object, dso, 'person.givenName');
|
||||
if (isEmpty(familyName) && isEmpty(givenName)) {
|
||||
return this.firstMetadataValue(object, dso, 'dc.title') || dso.name;
|
||||
} else if (isEmpty(familyName) || isEmpty(givenName)) {
|
||||
return familyName || givenName;
|
||||
}
|
||||
return `${familyName}, ${givenName}`;
|
||||
} else if (entityType === 'OrgUnit') {
|
||||
return this.firstMetadataValue(object, dso, 'organization.legalName');
|
||||
}
|
||||
return this.firstMetadataValue(object, dso, 'dc.title') || dso.name || this.translateService.instant('dso.name.untitled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights.
|
||||
*
|
||||
* @param object
|
||||
* @param dso
|
||||
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
|
||||
*
|
||||
* @returns {string} the first matching string value, or `undefined`.
|
||||
*/
|
||||
firstMetadataValue(object: any, dso: DSpaceObject, keyOrKeys: string | string[]): string {
|
||||
return Metadata.firstValue([object.hitHighlights, dso.metadata], keyOrKeys);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/no-namespace
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { Item } from '../shared/item.model';
|
||||
|
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/no-namespace
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
import { RemoveFromObjectCacheAction } from './object-cache.actions';
|
||||
import { serverSyncBufferReducer } from './server-sync-buffer.reducer';
|
||||
|
@@ -2,15 +2,12 @@ import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
||||
|
||||
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
|
||||
import { Action, StoreConfig, StoreModule } from '@ngrx/store';
|
||||
import { MyDSpaceGuard } from '../my-dspace-page/my-dspace.guard';
|
||||
|
||||
import { isNotEmpty } from '../shared/empty.util';
|
||||
import { FormBuilderService } from '../shared/form/builder/form-builder.service';
|
||||
import { FormService } from '../shared/form/form.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest/endpoint-mocking-rest.service';
|
||||
@@ -23,10 +20,7 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
|
||||
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||
import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service';
|
||||
import { AuthenticatedGuard } from './auth/authenticated.guard';
|
||||
import { AuthStatus } from './auth/models/auth-status.model';
|
||||
import { BrowseService } from './browse/browse.service';
|
||||
@@ -138,9 +132,6 @@ import {
|
||||
import { Registration } from './shared/registration.model';
|
||||
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||
import {
|
||||
DsDynamicTypeBindRelationService
|
||||
} from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
|
||||
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
||||
import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service';
|
||||
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
|
||||
@@ -150,7 +141,6 @@ import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-ent
|
||||
import { Vocabulary } from './submission/vocabularies/models/vocabulary.model';
|
||||
import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||
import { VocabularyService } from './submission/vocabularies/vocabulary.service';
|
||||
import { VocabularyTreeviewService } from '../shared/vocabulary-treeview/vocabulary-treeview.service';
|
||||
import { ConfigurationDataService } from './data/configuration-data.service';
|
||||
import { ConfigurationProperty } from './shared/configuration-property.model';
|
||||
import { ReloadGuard } from './reload/reload.guard';
|
||||
@@ -211,12 +201,6 @@ const PROVIDERS = [
|
||||
DSOResponseParsingService,
|
||||
{ provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap },
|
||||
{ provide: DspaceRestService, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] },
|
||||
DynamicFormLayoutService,
|
||||
DynamicFormService,
|
||||
DynamicFormValidationService,
|
||||
FormBuilderService,
|
||||
SectionFormOperationsService,
|
||||
FormService,
|
||||
EPersonDataService,
|
||||
LinkHeadService,
|
||||
HALEndpointService,
|
||||
@@ -245,19 +229,16 @@ const PROVIDERS = [
|
||||
SubmissionResponseParsingService,
|
||||
SubmissionJsonPatchOperationsService,
|
||||
JsonPatchOperationsBuilder,
|
||||
UploaderService,
|
||||
UUIDService,
|
||||
NotificationsService,
|
||||
WorkspaceitemDataService,
|
||||
WorkflowItemDataService,
|
||||
UploaderService,
|
||||
DSpaceObjectDataService,
|
||||
ConfigurationDataService,
|
||||
DSOChangeAnalyzer,
|
||||
DefaultChangeAnalyzer,
|
||||
ArrayMoveChangeAnalyzer,
|
||||
ObjectSelectService,
|
||||
CSSVariableService,
|
||||
MenuService,
|
||||
ObjectUpdatesService,
|
||||
SearchService,
|
||||
@@ -268,7 +249,6 @@ const PROVIDERS = [
|
||||
ClaimedTaskDataService,
|
||||
PoolTaskDataService,
|
||||
BitstreamDataService,
|
||||
DsDynamicTypeBindRelationService,
|
||||
EntityTypeDataService,
|
||||
ContentSourceResponseParsingService,
|
||||
ItemTemplateDataService,
|
||||
@@ -304,7 +284,6 @@ const PROVIDERS = [
|
||||
VocabularyService,
|
||||
VocabularyDataService,
|
||||
VocabularyEntryDetailsDataService,
|
||||
VocabularyTreeviewService,
|
||||
SequenceService,
|
||||
GroupDataService,
|
||||
FeedbackDataService,
|
||||
|
@@ -41,21 +41,28 @@ describe('ArrayMoveChangeAnalyzer', () => {
|
||||
], new MoveTest(0, 3));
|
||||
|
||||
testMove([
|
||||
{ op: 'move', from: '/2', path: '/3' },
|
||||
{ op: 'move', from: '/0', path: '/3' },
|
||||
{ op: 'move', from: '/2', path: '/1' }
|
||||
], new MoveTest(0, 3), new MoveTest(1, 2));
|
||||
|
||||
testMove([
|
||||
{ op: 'move', from: '/3', path: '/4' },
|
||||
{ op: 'move', from: '/0', path: '/1' },
|
||||
{ op: 'move', from: '/3', path: '/4' }
|
||||
], new MoveTest(0, 1), new MoveTest(3, 4));
|
||||
|
||||
testMove([], new MoveTest(0, 4), new MoveTest(4, 0));
|
||||
|
||||
testMove([
|
||||
{ op: 'move', from: '/2', path: '/3' },
|
||||
{ op: 'move', from: '/0', path: '/3' },
|
||||
{ op: 'move', from: '/2', path: '/1' }
|
||||
], new MoveTest(0, 4), new MoveTest(1, 3), new MoveTest(2, 4));
|
||||
|
||||
testMove([
|
||||
{ op: 'move', from: '/3', path: '/4' },
|
||||
{ op: 'move', from: '/2', path: '/4' },
|
||||
{ op: 'move', from: '/1', path: '/3' },
|
||||
{ op: 'move', from: '/0', path: '/3' },
|
||||
], new MoveTest(4, 1), new MoveTest(4, 2), new MoveTest(0, 3));
|
||||
});
|
||||
|
||||
describe('when some values are undefined (index 2 and 3)', () => {
|
||||
|
@@ -16,22 +16,31 @@ export class ArrayMoveChangeAnalyzer<T> {
|
||||
* @param array2 The custom array to compare with the original
|
||||
*/
|
||||
diff(array1: T[], array2: T[]): MoveOperation[] {
|
||||
const result = [];
|
||||
const moved = [...array1];
|
||||
array1.forEach((value: T, index: number) => {
|
||||
if (hasValue(value)) {
|
||||
const otherIndex = array2.indexOf(value);
|
||||
const movedIndex = moved.indexOf(value);
|
||||
if (index !== otherIndex && movedIndex !== otherIndex) {
|
||||
moveItemInArray(moved, movedIndex, otherIndex);
|
||||
result.push(Object.assign({
|
||||
op: 'move',
|
||||
from: '/' + movedIndex,
|
||||
path: '/' + otherIndex
|
||||
}) as MoveOperation);
|
||||
}
|
||||
return this.getMoves(array1, array2).map((move) => Object.assign({
|
||||
op: 'move',
|
||||
from: '/' + move[0],
|
||||
path: '/' + move[1],
|
||||
}) as MoveOperation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine a set of moves required to transform array1 into array2
|
||||
* The moves are returned as an array of pairs of numbers where the first number is the original index and the second
|
||||
* is the new index
|
||||
* It is assumed the operations are executed in the order they're returned (and not simultaneously)
|
||||
* @param array1
|
||||
* @param array2
|
||||
*/
|
||||
private getMoves(array1: any[], array2: any[]): number[][] {
|
||||
const moved = [...array2];
|
||||
|
||||
return array1.reduce((moves, item, index) => {
|
||||
if (hasValue(item) && item !== moved[index]) {
|
||||
const last = moved.lastIndexOf(item);
|
||||
moveItemInArray(moved, last, index);
|
||||
moves.unshift([index, last]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
return moves;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import { ResourceType } from '../../shared/resource-type';
|
||||
import { BaseDataService } from './base-data.service';
|
||||
import { HALDataService } from './hal-data-service.interface';
|
||||
import { dataService, getDataServiceFor } from './data-service.decorator';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class TestService extends BaseDataService<any> {
|
||||
}
|
||||
@@ -28,7 +29,7 @@ let testType;
|
||||
|
||||
describe('@dataService/getDataServiceFor', () => {
|
||||
beforeEach(() => {
|
||||
testType = new ResourceType('testType-' + new Date().getTime());
|
||||
testType = new ResourceType(`testType-${uuidv4()}`);
|
||||
});
|
||||
|
||||
it('should register a resourcetype for a dataservice', () => {
|
||||
|
@@ -3,7 +3,7 @@ import { ChangeAnalyzer } from './change-analyzer';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { MetadataMap } from '../shared/metadata.models';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
/**
|
||||
* A class to determine what differs between two
|
||||
|
@@ -29,5 +29,7 @@ export enum FeatureID {
|
||||
CanViewUsageStatistics = 'canViewUsageStatistics',
|
||||
CanSendFeedback = 'canSendFeedback',
|
||||
CanClaimItem = 'canClaimItem',
|
||||
CanSynchronizeWithORCID = 'canSynchronizeWithORCID'
|
||||
CanSynchronizeWithORCID = 'canSynchronizeWithORCID',
|
||||
CanSubmit = 'canSubmit',
|
||||
CanEditItem = 'canEditItem',
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/no-namespace
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
import {
|
||||
AddFieldUpdateAction,
|
||||
|
@@ -0,0 +1,33 @@
|
||||
import { MetadataPatchOperation } from './metadata-patch-operation.model';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
|
||||
/**
|
||||
* Wrapper object for a metadata patch move Operation
|
||||
*/
|
||||
export class MetadataPatchMoveOperation extends MetadataPatchOperation {
|
||||
static operationType = 'move';
|
||||
|
||||
/**
|
||||
* The original place of the metadata value to move
|
||||
*/
|
||||
from: number;
|
||||
|
||||
/**
|
||||
* The new place to move the metadata value to
|
||||
*/
|
||||
to: number;
|
||||
|
||||
constructor(field: string, from: number, to: number) {
|
||||
super(MetadataPatchMoveOperation.operationType, field);
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the MetadataPatchOperation into a fast-json-patch Operation by constructing its path and other properties
|
||||
* using the information provided.
|
||||
*/
|
||||
toOperation(): Operation {
|
||||
return { op: this.op as any, from: `/metadata/${this.field}/${this.from}`, path: `/metadata/${this.field}/${this.to}` };
|
||||
}
|
||||
}
|
@@ -10,13 +10,19 @@ import { DeleteRequest } from './request.models';
|
||||
import { RelationshipDataService } from './relationship-data.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { RequestEntry } from './request-entry.model';
|
||||
import { FindListOptions } from './find-list-options.model';
|
||||
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||
import { MetadataValue } from '../shared/metadata.models';
|
||||
import { MetadataRepresentationType } from '../shared/metadata-representation/metadata-representation.model';
|
||||
|
||||
describe('RelationshipDataService', () => {
|
||||
let service: RelationshipDataService;
|
||||
@@ -233,4 +239,152 @@ describe('RelationshipDataService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveMetadataRepresentation', () => {
|
||||
const parentItem: Item = Object.assign(new Item(), {
|
||||
id: 'parent-item',
|
||||
metadata: {
|
||||
'dc.contributor.author': [
|
||||
Object.assign(new MetadataValue(), {
|
||||
language: null,
|
||||
value: 'Related Author with authority',
|
||||
authority: 'virtual::related-author',
|
||||
place: 2
|
||||
}),
|
||||
Object.assign(new MetadataValue(), {
|
||||
language: null,
|
||||
value: 'Author without authority',
|
||||
place: 1
|
||||
}),
|
||||
],
|
||||
'dc.creator': [
|
||||
Object.assign(new MetadataValue(), {
|
||||
language: null,
|
||||
value: 'Related Creator with authority',
|
||||
authority: 'virtual::related-creator',
|
||||
place: 3,
|
||||
}),
|
||||
Object.assign(new MetadataValue(), {
|
||||
language: null,
|
||||
value: 'Related Creator with authority - unauthorized',
|
||||
authority: 'virtual::related-creator-unauthorized',
|
||||
place: 4,
|
||||
}),
|
||||
],
|
||||
'dc.title': [
|
||||
Object.assign(new MetadataValue(), {
|
||||
language: null,
|
||||
value: 'Parent Item'
|
||||
}),
|
||||
]
|
||||
}
|
||||
});
|
||||
const relatedAuthor: Item = Object.assign(new Item(), {
|
||||
id: 'related-author',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
Object.assign(new MetadataValue(), {
|
||||
language: null,
|
||||
value: 'Related Author'
|
||||
}),
|
||||
]
|
||||
}
|
||||
});
|
||||
const relatedCreator: Item = Object.assign(new Item(), {
|
||||
id: 'related-creator',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
Object.assign(new MetadataValue(), {
|
||||
language: null,
|
||||
value: 'Related Creator'
|
||||
}),
|
||||
],
|
||||
'dspace.entity.type': 'Person',
|
||||
}
|
||||
});
|
||||
const authorRelation: Relationship = Object.assign(new Relationship(), {
|
||||
leftItem: createSuccessfulRemoteDataObject$(parentItem),
|
||||
rightItem: createSuccessfulRemoteDataObject$(relatedAuthor)
|
||||
});
|
||||
const creatorRelation: Relationship = Object.assign(new Relationship(), {
|
||||
leftItem: createSuccessfulRemoteDataObject$(parentItem),
|
||||
rightItem: createSuccessfulRemoteDataObject$(relatedCreator),
|
||||
});
|
||||
const creatorRelationUnauthorized: Relationship = Object.assign(new Relationship(), {
|
||||
leftItem: createSuccessfulRemoteDataObject$(parentItem),
|
||||
rightItem: createFailedRemoteDataObject$('Unauthorized', 401),
|
||||
});
|
||||
|
||||
let metadatum: MetadataValue;
|
||||
|
||||
beforeEach(() => {
|
||||
service.findById = (id: string) => {
|
||||
if (id === 'related-author') {
|
||||
return createSuccessfulRemoteDataObject$(authorRelation);
|
||||
}
|
||||
if (id === 'related-creator') {
|
||||
return createSuccessfulRemoteDataObject$(creatorRelation);
|
||||
}
|
||||
if (id === 'related-creator-unauthorized') {
|
||||
return createSuccessfulRemoteDataObject$(creatorRelationUnauthorized);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe('when the metadata isn\'t virtual', () => {
|
||||
beforeEach(() => {
|
||||
metadatum = parentItem.metadata['dc.contributor.author'][1];
|
||||
});
|
||||
|
||||
it('should return a plain text MetadatumRepresentation', (done) => {
|
||||
service.resolveMetadataRepresentation(metadatum, parentItem, 'Person').subscribe((result) => {
|
||||
expect(result.representationType).toEqual(MetadataRepresentationType.PlainText);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the metadata is a virtual author', () => {
|
||||
beforeEach(() => {
|
||||
metadatum = parentItem.metadata['dc.contributor.author'][0];
|
||||
});
|
||||
|
||||
it('should return a ItemMetadataRepresentation with the correct value', (done) => {
|
||||
service.resolveMetadataRepresentation(metadatum, parentItem, 'Person').subscribe((result) => {
|
||||
expect(result.representationType).toEqual(MetadataRepresentationType.Item);
|
||||
expect(result.getValue()).toEqual(metadatum.value);
|
||||
expect((result as any).id).toEqual(relatedAuthor.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the metadata is a virtual creator', () => {
|
||||
beforeEach(() => {
|
||||
metadatum = parentItem.metadata['dc.creator'][0];
|
||||
});
|
||||
|
||||
it('should return a ItemMetadataRepresentation with the correct value', (done) => {
|
||||
service.resolveMetadataRepresentation(metadatum, parentItem, 'Person').subscribe((result) => {
|
||||
expect(result.representationType).toEqual(MetadataRepresentationType.Item);
|
||||
expect(result.getValue()).toEqual(metadatum.value);
|
||||
expect((result as any).id).toEqual(relatedCreator.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the metadata refers to a relationship leading to an error response', () => {
|
||||
beforeEach(() => {
|
||||
metadatum = parentItem.metadata['dc.creator'][1];
|
||||
});
|
||||
|
||||
it('should return an authority controlled MetadatumRepresentation', (done) => {
|
||||
service.resolveMetadataRepresentation(metadatum, parentItem, 'Person').subscribe((result) => {
|
||||
expect(result.representationType).toEqual(MetadataRepresentationType.AuthorityControlled);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||
import {
|
||||
compareArraysUsingIds, PAGINATED_RELATIONS_TO_ITEMS_OPERATOR,
|
||||
@@ -46,6 +46,11 @@ import { PutData, PutDataImpl } from './base/put-data';
|
||||
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||
import { dataService } from './base/data-service.decorator';
|
||||
import { itemLinksToFollow } from '../../shared/utils/relation-query.utils';
|
||||
import { MetadataValue } from '../shared/metadata.models';
|
||||
import { MetadataRepresentation } from '../shared/metadata-representation/metadata-representation.model';
|
||||
import { MetadatumRepresentation } from '../shared/metadata-representation/metadatum/metadatum-representation.model';
|
||||
import { ItemMetadataRepresentation } from '../shared/metadata-representation/item/item-metadata-representation.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
|
||||
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
|
||||
|
||||
@@ -550,4 +555,40 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
||||
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Relationship>[]): Observable<RemoteData<PaginatedList<Relationship>>> {
|
||||
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a {@link MetadataValue} into a {@link MetadataRepresentation} of the correct type
|
||||
* @param metadatum {@link MetadataValue} to resolve
|
||||
* @param parentItem Parent dspace object the metadata value belongs to
|
||||
* @param itemType The type of item this metadata value represents (will only be used when no related item can be found, as a fallback)
|
||||
*/
|
||||
resolveMetadataRepresentation(metadatum: MetadataValue, parentItem: DSpaceObject, itemType: string): Observable<MetadataRepresentation> {
|
||||
if (metadatum.isVirtual) {
|
||||
return this.findById(metadatum.virtualValue, true, false, followLink('leftItem'), followLink('rightItem')).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
switchMap((relRD: RemoteData<Relationship>) =>
|
||||
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(
|
||||
filter(([leftItem, rightItem]) => leftItem.hasCompleted && rightItem.hasCompleted),
|
||||
map(([leftItem, rightItem]) => {
|
||||
if (!leftItem.hasSucceeded || !rightItem.hasSucceeded) {
|
||||
return null;
|
||||
} else if (rightItem.hasSucceeded && leftItem.payload.id === parentItem.id) {
|
||||
return rightItem.payload;
|
||||
} else if (rightItem.payload.id === parentItem.id) {
|
||||
return leftItem.payload;
|
||||
}
|
||||
}),
|
||||
map((item: Item) => {
|
||||
if (hasValue(item)) {
|
||||
return Object.assign(new ItemMetadataRepresentation(metadatum), item);
|
||||
} else {
|
||||
return Object.assign(new MetadatumRepresentation(itemType), metadatum);
|
||||
}
|
||||
})
|
||||
)
|
||||
));
|
||||
} else {
|
||||
return observableOf(Object.assign(new MetadatumRepresentation(itemType), metadatum));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/no-namespace
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
import {
|
||||
RequestConfigureAction,
|
||||
|
@@ -4,7 +4,7 @@ import { HttpHeaders } from '@angular/common/http';
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map, take, tap } from 'rxjs/operators';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../../shared/empty.util';
|
||||
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
|
@@ -1,7 +1,17 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class UploaderService {
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DragService {
|
||||
private _overrideDragOverPage = false;
|
||||
|
||||
public overrideDragOverPage() {
|
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/no-namespace
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
|
||||
import { indexReducer, MetaIndexState } from './index.reducer';
|
||||
|
@@ -3,7 +3,6 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { IndexState, MetaIndexState } from './index.reducer';
|
||||
import * as parse from 'url-parse';
|
||||
import { IndexName } from './index-name.model';
|
||||
import { CoreState } from '../core-state.model';
|
||||
|
||||
@@ -21,17 +20,21 @@ import { CoreState } from '../core-state.model';
|
||||
*/
|
||||
export const getUrlWithoutEmbedParams = (url: string): string => {
|
||||
if (isNotEmpty(url)) {
|
||||
const parsed = parse(url);
|
||||
if (isNotEmpty(parsed.query)) {
|
||||
const parts = parsed.query.split(/[?|&]/)
|
||||
.filter((part: string) => isNotEmpty(part))
|
||||
.filter((part: string) => !(part.startsWith('embed=') || part.startsWith('embed.size=')));
|
||||
let args = '';
|
||||
if (isNotEmpty(parts)) {
|
||||
args = `?${parts.join('&')}`;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (isNotEmpty(parsed.search)) {
|
||||
const parts = parsed.search.split(/[?|&]/)
|
||||
.filter((part: string) => isNotEmpty(part))
|
||||
.filter((part: string) => !(part.startsWith('embed=') || part.startsWith('embed.size=')));
|
||||
let args = '';
|
||||
if (isNotEmpty(parts)) {
|
||||
args = `?${parts.join('&')}`;
|
||||
}
|
||||
url = new URLCombiner(parsed.origin, parsed.pathname, args).toString();
|
||||
return url;
|
||||
}
|
||||
url = new URLCombiner(parsed.origin, parsed.pathname, args).toString();
|
||||
return url;
|
||||
} catch (e) {
|
||||
// Ignore parsing errors. By default, we return the original string below.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,15 +47,19 @@ export const getUrlWithoutEmbedParams = (url: string): string => {
|
||||
*/
|
||||
export const getEmbedSizeParams = (url: string): { name: string, size: number }[] => {
|
||||
if (isNotEmpty(url)) {
|
||||
const parsed = parse(url);
|
||||
if (isNotEmpty(parsed.query)) {
|
||||
return parsed.query.split(/[?|&]/)
|
||||
.filter((part: string) => isNotEmpty(part))
|
||||
.map((part: string) => part.match(/^embed.size=([^=]+)=(\d+)$/))
|
||||
.filter((matches: RegExpMatchArray) => hasValue(matches) && hasValue(matches[1]) && hasValue(matches[2]))
|
||||
.map((matches: RegExpMatchArray) => {
|
||||
return { name: matches[1], size: Number(matches[2]) };
|
||||
});
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (isNotEmpty(parsed.search)) {
|
||||
return parsed.search.split(/[?|&]/)
|
||||
.filter((part: string) => isNotEmpty(part))
|
||||
.map((part: string) => part.match(/^embed.size=([^=]+)=(\d+)$/))
|
||||
.filter((matches: RegExpMatchArray) => hasValue(matches) && hasValue(matches[1]) && hasValue(matches[2]))
|
||||
.map((matches: RegExpMatchArray) => {
|
||||
return { name: matches[1], size: Number(matches[2]) };
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors. By default, we return an empty result below.
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/no-namespace
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
|
||||
import {
|
||||
|
@@ -40,7 +40,7 @@ export class LocaleService {
|
||||
protected translate: TranslateService,
|
||||
protected authService: AuthService,
|
||||
protected routeService: RouteService,
|
||||
@Inject(DOCUMENT) private document: any
|
||||
@Inject(DOCUMENT) protected document: any
|
||||
) {
|
||||
}
|
||||
|
||||
|
@@ -1,12 +1,31 @@
|
||||
import { LANG_ORIGIN, LocaleService } from './locale.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { combineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { map, mergeMap, take } from 'rxjs/operators';
|
||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { RouteService } from '../services/route.service';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
|
||||
@Injectable()
|
||||
export class ServerLocaleService extends LocaleService {
|
||||
|
||||
constructor(
|
||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||
@Inject(REQUEST) protected req: Request,
|
||||
protected cookie: CookieService,
|
||||
protected translate: TranslateService,
|
||||
protected authService: AuthService,
|
||||
protected routeService: RouteService,
|
||||
@Inject(DOCUMENT) protected document: any
|
||||
) {
|
||||
super(_window, cookie, translate, authService, routeService, document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the languages list of the user in Accept-Language format
|
||||
*
|
||||
@@ -50,6 +69,10 @@ export class ServerLocaleService extends LocaleService {
|
||||
if (isNotEmpty(epersonLang)) {
|
||||
languages.push(...epersonLang);
|
||||
}
|
||||
if (hasValue(this.req.headers['accept-language'])) {
|
||||
languages.push(...this.req.headers['accept-language'].split(',')
|
||||
);
|
||||
}
|
||||
return languages;
|
||||
})
|
||||
);
|
||||
|
@@ -8,7 +8,12 @@ import { Observable, of as observableOf, of } from 'rxjs';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { Item } from '../shared/item.model';
|
||||
|
||||
import { ItemMock, MockBitstream1, MockBitstream3 } from '../../shared/mocks/item.mock';
|
||||
import {
|
||||
ItemMock,
|
||||
MockBitstream1,
|
||||
MockBitstream3,
|
||||
MockBitstream2
|
||||
} from '../../shared/mocks/item.mock';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
@@ -24,6 +29,7 @@ import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
import { getMockStore } from '@ngrx/store/testing';
|
||||
import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions';
|
||||
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
||||
import { AppConfig } from '../../../config/app-config.interface';
|
||||
|
||||
describe('MetadataService', () => {
|
||||
let metadataService: MetadataService;
|
||||
@@ -44,6 +50,8 @@ describe('MetadataService', () => {
|
||||
let router: Router;
|
||||
let store;
|
||||
|
||||
let appConfig: AppConfig;
|
||||
|
||||
const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}};
|
||||
|
||||
|
||||
@@ -86,6 +94,14 @@ describe('MetadataService', () => {
|
||||
store = getMockStore({ initialState });
|
||||
spyOn(store, 'dispatch');
|
||||
|
||||
appConfig = {
|
||||
item: {
|
||||
bitstream: {
|
||||
pageSize: 5
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
|
||||
metadataService = new MetadataService(
|
||||
router,
|
||||
translateService,
|
||||
@@ -98,6 +114,7 @@ describe('MetadataService', () => {
|
||||
rootService,
|
||||
store,
|
||||
hardRedirectService,
|
||||
appConfig,
|
||||
authorizationService
|
||||
);
|
||||
});
|
||||
@@ -358,29 +375,66 @@ describe('MetadataService', () => {
|
||||
});
|
||||
}));
|
||||
|
||||
it('should link to first Bitstream with allowed format', fakeAsync(() => {
|
||||
const bitstreams = [MockBitstream3, MockBitstream3, MockBitstream1];
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
|
||||
(bitstreamDataService.findListByHref as jasmine.Spy).and.returnValues(
|
||||
...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)),
|
||||
);
|
||||
describe(`when there's a bitstream with an allowed format on the first page`, () => {
|
||||
let bitstreams;
|
||||
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
beforeEach(() => {
|
||||
bitstreams = [MockBitstream2, MockBitstream3, MockBitstream1];
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
|
||||
(bitstreamDataService.findListByHref as jasmine.Spy).and.returnValues(
|
||||
...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)),
|
||||
);
|
||||
});
|
||||
|
||||
it('should link to first Bitstream with allowed format', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/download'
|
||||
});
|
||||
}));
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download'
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when there's no bitstream with an allowed format on the first page`, () => {
|
||||
let bitstreams;
|
||||
|
||||
beforeEach(() => {
|
||||
bitstreams = [MockBitstream1, MockBitstream3, MockBitstream2];
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
|
||||
(bitstreamDataService.findListByHref as jasmine.Spy).and.returnValues(
|
||||
...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)),
|
||||
);
|
||||
});
|
||||
|
||||
it(`shouldn't add a citation_pdf_url meta tag`, fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download'
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('tagstore', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
|
@@ -1,14 +1,21 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, Inject } from '@angular/core';
|
||||
|
||||
import { Meta, MetaDefinition, Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BehaviorSubject, combineLatest, EMPTY, Observable, of as observableOf } from 'rxjs';
|
||||
import { expand, filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
concat as observableConcat,
|
||||
EMPTY
|
||||
} from 'rxjs';
|
||||
import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators';
|
||||
|
||||
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||
import { BitstreamDataService } from '../data/bitstream-data.service';
|
||||
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
|
||||
@@ -37,6 +44,7 @@ import { coreSelector } from '../core.selectors';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
||||
import { getDownloadableBitstream } from '../shared/bitstream.operators';
|
||||
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||
|
||||
/**
|
||||
* The base selector function to select the metaTag section in the store
|
||||
@@ -87,6 +95,7 @@ export class MetadataService {
|
||||
private rootService: RootDataService,
|
||||
private store: Store<CoreState>,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
@Inject(APP_CONFIG) private appConfig: AppConfig,
|
||||
private authorizationService: AuthorizationDataService
|
||||
) {
|
||||
}
|
||||
@@ -298,7 +307,13 @@ export class MetadataService {
|
||||
true,
|
||||
true,
|
||||
followLink('primaryBitstream'),
|
||||
followLink('bitstreams', {}, followLink('format')),
|
||||
followLink('bitstreams', {
|
||||
findListOptions: {
|
||||
// limit the number of bitstreams used to find the citation pdf url to the number
|
||||
// shown by default on an item page
|
||||
elementsPerPage: this.appConfig.item.bitstream.pageSize
|
||||
}
|
||||
}, followLink('format')),
|
||||
).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((bundle: Bundle) =>
|
||||
@@ -363,64 +378,45 @@ export class MetadataService {
|
||||
}
|
||||
|
||||
/**
|
||||
* For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream with a MIME type
|
||||
* For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream
|
||||
* with a MIME type.
|
||||
*
|
||||
* Note this will only check the current page (page size determined item.bitstream.pageSize in the
|
||||
* config) of bitstreams for performance reasons.
|
||||
* See https://github.com/DSpace/DSpace/issues/8648 for more info
|
||||
*
|
||||
* included in {@linkcode CITATION_PDF_URL_MIMETYPES}
|
||||
* @param bitstreamRd
|
||||
* @private
|
||||
*/
|
||||
private getFirstAllowedFormatBitstreamLink(bitstreamRd: RemoteData<PaginatedList<Bitstream>>): Observable<string> {
|
||||
return observableOf(bitstreamRd.payload).pipe(
|
||||
// Because there can be more than one page of bitstreams, this expand operator
|
||||
// will retrieve them in turn. Due to the take(1) at the bottom, it will only
|
||||
// retrieve pages until a match is found
|
||||
expand((paginatedList: PaginatedList<Bitstream>) => {
|
||||
if (hasNoValue(paginatedList.next)) {
|
||||
// If there's no next page, stop.
|
||||
return EMPTY;
|
||||
} else {
|
||||
// Otherwise retrieve the next page
|
||||
return this.bitstreamDataService.findListByHref(
|
||||
paginatedList.next,
|
||||
undefined,
|
||||
true,
|
||||
true,
|
||||
followLink('format')
|
||||
).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((next: RemoteData<PaginatedList<Bitstream>>) => {
|
||||
if (hasValue(next.payload)) {
|
||||
return next.payload;
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
// Return the array of bitstreams inside each paginated list
|
||||
map((paginatedList: PaginatedList<Bitstream>) => paginatedList.page),
|
||||
// Emit the bitstreams in the list one at a time
|
||||
switchMap((bitstreams: Bitstream[]) => bitstreams),
|
||||
// Retrieve the format for each bitstream
|
||||
switchMap((bitstream: Bitstream) => bitstream.format.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
// Keep the original bitstream, because it, not the format, is what we'll need
|
||||
// for the link at the end
|
||||
map((format: BitstreamFormat) => [bitstream, format])
|
||||
)),
|
||||
// Check if bitstream downloadable
|
||||
switchMap(([bitstream, format]: [Bitstream, BitstreamFormat]) => observableOf(bitstream).pipe(
|
||||
getDownloadableBitstream(this.authorizationService),
|
||||
map((bit: Bitstream) => [bit, format])
|
||||
)),
|
||||
// Filter out only pairs with whitelisted formats and non-null bitstreams, null from download check
|
||||
filter(([bitstream, format]: [Bitstream, BitstreamFormat]) =>
|
||||
hasValue(format) && hasValue(bitstream) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)),
|
||||
// We only need 1
|
||||
take(1),
|
||||
// Emit the link of the match
|
||||
map(([bitstream, ]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream))
|
||||
);
|
||||
if (hasValue(bitstreamRd.payload) && isNotEmpty(bitstreamRd.payload.page)) {
|
||||
// Retrieve the formats of all bitstreams in the page sequentially
|
||||
return observableConcat(
|
||||
...bitstreamRd.payload.page.map((bitstream: Bitstream) => bitstream.format.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
// Keep the original bitstream, because it, not the format, is what we'll need
|
||||
// for the link at the end
|
||||
map((format: BitstreamFormat) => [bitstream, format])
|
||||
))
|
||||
).pipe(
|
||||
// Verify that the bitstream is downloadable
|
||||
mergeMap(([bitstream, format]: [Bitstream, BitstreamFormat]) => observableOf(bitstream).pipe(
|
||||
getDownloadableBitstream(this.authorizationService),
|
||||
map((bit: Bitstream) => [bit, format])
|
||||
)),
|
||||
// Filter out only pairs with whitelisted formats and non-null bitstreams, null from download check
|
||||
filter(([bitstream, format]: [Bitstream, BitstreamFormat]) =>
|
||||
hasValue(format) && hasValue(bitstream) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)),
|
||||
// We only need 1
|
||||
take(1),
|
||||
// Emit the link of the match
|
||||
// tap((v) => console.log('result', v)),
|
||||
map(([bitstream, ]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream))
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import { filter, map, pairwise } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
import { RouterNavigationAction } from '@ngrx/router-store';
|
||||
import { RouterNavigationAction, ROUTER_NAVIGATION } from '@ngrx/router-store';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouteUpdateAction } from './router.actions';
|
||||
|
||||
@@ -14,7 +13,7 @@ export class RouterEffects {
|
||||
*/
|
||||
routeChange$ = createEffect(() => this.actions$
|
||||
.pipe(
|
||||
ofType(fromRouter.ROUTER_NAVIGATION),
|
||||
ofType(ROUTER_NAVIGATION),
|
||||
pairwise(),
|
||||
map((actions: RouterNavigationAction[]) =>
|
||||
actions.map((navigateAction) => {
|
||||
|
@@ -4,7 +4,7 @@ import { ActivatedRoute, NavigationEnd, Params, Router, RouterStateSnapshot, } f
|
||||
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { isEqual } from 'lodash';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import { AddParameterAction, SetParameterAction, SetParametersAction, SetQueryParameterAction, SetQueryParametersAction } from './route.actions';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
|
@@ -3,7 +3,7 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { HALEndpointService } from './hal-endpoint.service';
|
||||
import { EndpointMapRequest } from '../data/request.models';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
|
@@ -14,6 +14,11 @@ export class MediaViewerItem {
|
||||
*/
|
||||
format: string;
|
||||
|
||||
/**
|
||||
* Incoming Bitsream format mime type
|
||||
*/
|
||||
mimetype: string;
|
||||
|
||||
/**
|
||||
* Incoming Bitsream thumbnail
|
||||
*/
|
||||
|
@@ -5,7 +5,8 @@ import {
|
||||
MetadataValueFilter,
|
||||
MetadatumViewModel
|
||||
} from './metadata.models';
|
||||
import { groupBy, sortBy } from 'lodash';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
/**
|
||||
* Utility class for working with DSpace object metadata.
|
||||
|
@@ -226,7 +226,7 @@ export const metadataFieldsToString = () =>
|
||||
map((schema: MetadataSchema) => ({ field, schema }))
|
||||
);
|
||||
});
|
||||
return observableCombineLatest(fieldSchemaArray);
|
||||
return isNotEmpty(fieldSchemaArray) ? observableCombineLatest(fieldSchemaArray) : [[]];
|
||||
}),
|
||||
map((fieldSchemaArray: { field: MetadataField, schema: MetadataSchema }[]): string[] => {
|
||||
return fieldSchemaArray.map((fieldSchema: { field: MetadataField, schema: MetadataSchema }) => fieldSchema.schema.prefix + '.' + fieldSchema.field.toString());
|
||||
|
@@ -26,6 +26,8 @@ import { SearchConfigurationService } from './search-configuration.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { RequestEntry } from '../../data/request-entry.model';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
|
||||
import anything = jasmine.anything;
|
||||
|
||||
@Component({ template: '' })
|
||||
class DummyComponent {
|
||||
@@ -36,7 +38,7 @@ describe('SearchService', () => {
|
||||
let searchService: SearchService;
|
||||
const router = new RouterStub();
|
||||
const route = new ActivatedRouteStub();
|
||||
const searchConfigService = {paginationID: 'page-id'};
|
||||
const searchConfigService = { paginationID: 'page-id' };
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -103,7 +105,8 @@ describe('SearchService', () => {
|
||||
};
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
const searchConfigService = {paginationID: 'page-id'};
|
||||
const searchConfigService = { paginationID: 'page-id' };
|
||||
const requestService = getMockRequestService();
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -119,7 +122,7 @@ describe('SearchService', () => {
|
||||
providers: [
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: RequestService, useValue: getMockRequestService() },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: CommunityDataService, useValue: {} },
|
||||
@@ -138,13 +141,13 @@ describe('SearchService', () => {
|
||||
|
||||
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
|
||||
searchService.setViewMode(ViewMode.ListElement);
|
||||
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.ListElement }
|
||||
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.ListElement }
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
|
||||
searchService.setViewMode(ViewMode.GridElement);
|
||||
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.GridElement }
|
||||
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.GridElement }
|
||||
);
|
||||
});
|
||||
|
||||
@@ -191,5 +194,23 @@ describe('SearchService', () => {
|
||||
expect((searchService as any).rdb.buildFromHref).toHaveBeenCalledWith(endPoint);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getFacetValuesFor is called with a filterQuery', () => {
|
||||
it('should add the encoded filterQuery to the args list', () => {
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
const spyRequest = spyOn((searchService as any), 'request').and.stub();
|
||||
spyOn(requestService, 'send').and.returnValue(true);
|
||||
const searchFilterConfig = new SearchFilterConfig();
|
||||
searchFilterConfig._links = {
|
||||
self: {
|
||||
href: 'https://demo.dspace.org/',
|
||||
},
|
||||
};
|
||||
|
||||
searchService.getFacetValuesFor(searchFilterConfig, 1, undefined, 'filter&Query');
|
||||
|
||||
expect(spyRequest).toHaveBeenCalledWith(anything(), 'https://demo.dspace.org?page=0&size=5&prefix=filter%26Query');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -3,7 +3,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||
import { PaginatedList } from '../../data/paginated-list.model';
|
||||
import { ResponseParsingService } from '../../data/parsing.service';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
import { GetRequest } from '../../data/request.models';
|
||||
@@ -271,7 +270,7 @@ export class SearchService implements OnDestroy {
|
||||
let href;
|
||||
let args: string[] = [];
|
||||
if (hasValue(filterQuery)) {
|
||||
args.push(`prefix=${filterQuery}`);
|
||||
args.push(`prefix=${encodeURIComponent(filterQuery)}`);
|
||||
}
|
||||
if (hasValue(searchOptions)) {
|
||||
searchOptions = Object.assign(new PaginatedSearchOptions({}), searchOptions, {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { EquatableObject, excludeFromEquals, fieldsForEquals } from './equals.decorators';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
class Dog extends EquatableObject<Dog> {
|
||||
public name: string;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { CorrelationIdService } from './correlation-id.service';
|
||||
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
|
||||
import { UUIDService } from '../core/shared/uuid.service';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { MockStore } from '@ngrx/store/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { appReducers, AppState, storeModuleConfig } from '../app.reducer';
|
||||
|
@@ -5,7 +5,7 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||
import { find, map } from 'rxjs/operators';
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../shared/empty.util';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { Router } from '@angular/router';
|
||||
import { ProcessDataService } from '../core/data/processes/process-data.service';
|
||||
|
@@ -0,0 +1,15 @@
|
||||
<div class="flex-grow-1 ds-drop-list h-100" [class.disabled]="(draggingMdField$ | async) && (draggingMdField$ | async) !== mdField" cdkDropList (cdkDropListDropped)="drop($event)" role="table">
|
||||
<ds-dso-edit-metadata-value-headers role="presentation" [dsoType]="dsoType"></ds-dso-edit-metadata-value-headers>
|
||||
<ds-dso-edit-metadata-value *ngFor="let mdValue of form.fields[mdField]; let idx = index" role="presentation"
|
||||
[dso]="dso"
|
||||
[mdValue]="mdValue"
|
||||
[dsoType]="dsoType"
|
||||
[saving$]="saving$"
|
||||
[isOnlyValue]="form.fields[mdField].length === 1"
|
||||
(edit)="mdValue.editing = true"
|
||||
(confirm)="mdValue.confirmChanges($event); form.resetReinstatable(); valueSaved.emit()"
|
||||
(remove)="mdValue.change === DsoEditMetadataChangeTypeEnum.ADD ? form.remove(mdField, idx) : mdValue.change = DsoEditMetadataChangeTypeEnum.REMOVE; form.resetReinstatable(); valueSaved.emit()"
|
||||
(undo)="mdValue.change === DsoEditMetadataChangeTypeEnum.ADD ? form.remove(mdField, idx) : mdValue.discard(); valueSaved.emit()"
|
||||
(dragging)="$event ? draggingMdField$.next(mdField) : draggingMdField$.next(null)">
|
||||
</ds-dso-edit-metadata-value>
|
||||
</div>
|
@@ -0,0 +1,7 @@
|
||||
.ds-drop-list {
|
||||
background-color: var(--bs-gray-500);
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
@@ -0,0 +1,135 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { DsoEditMetadataFieldValuesComponent } from './dso-edit-metadata-field-values.component';
|
||||
import { DsoEditMetadataForm } from '../dso-edit-metadata-form';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { of } from 'rxjs/internal/observable/of';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('DsoEditMetadataFieldValuesComponent', () => {
|
||||
let component: DsoEditMetadataFieldValuesComponent;
|
||||
let fixture: ComponentFixture<DsoEditMetadataFieldValuesComponent>;
|
||||
|
||||
let form: DsoEditMetadataForm;
|
||||
let dso: DSpaceObject;
|
||||
let mdField: string;
|
||||
let draggingMdField$: BehaviorSubject<string>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
dso = Object.assign(new DSpaceObject(), {
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
Object.assign(new MetadataValue(), {
|
||||
value: 'Test Title',
|
||||
language: 'en',
|
||||
place: 0,
|
||||
}),
|
||||
],
|
||||
'dc.subject': [
|
||||
Object.assign(new MetadataValue(), {
|
||||
value: 'Subject One',
|
||||
language: 'en',
|
||||
place: 0,
|
||||
}),
|
||||
Object.assign(new MetadataValue(), {
|
||||
value: 'Subject Two',
|
||||
language: 'en',
|
||||
place: 1,
|
||||
}),
|
||||
Object.assign(new MetadataValue(), {
|
||||
value: 'Subject Three',
|
||||
language: 'en',
|
||||
place: 2,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
form = new DsoEditMetadataForm(dso.metadata);
|
||||
mdField = 'dc.subject';
|
||||
draggingMdField$ = new BehaviorSubject<string>(null);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DsoEditMetadataFieldValuesComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DsoEditMetadataFieldValuesComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.dso = dso;
|
||||
component.form = form;
|
||||
component.mdField = mdField;
|
||||
component.saving$ = of(false);
|
||||
component.draggingMdField$ = draggingMdField$;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when draggingMdField$ emits a value equal to mdField', () => {
|
||||
beforeEach(() => {
|
||||
draggingMdField$.next(mdField);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not disable the list', () => {
|
||||
expect(fixture.debugElement.query(By.css('.ds-drop-list.disabled'))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when draggingMdField$ emits a value different to mdField', () => {
|
||||
beforeEach(() => {
|
||||
draggingMdField$.next(`${mdField}.fake`);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should disable the list', () => {
|
||||
expect(fixture.debugElement.query(By.css('.ds-drop-list.disabled'))).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when draggingMdField$ emits null', () => {
|
||||
beforeEach(() => {
|
||||
draggingMdField$.next(null);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not disable the list', () => {
|
||||
expect(fixture.debugElement.query(By.css('.ds-drop-list.disabled'))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dropping a value on a different index', () => {
|
||||
beforeEach(() => {
|
||||
component.drop(Object.assign({
|
||||
previousIndex: 0,
|
||||
currentIndex: 2,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should physically move the relevant metadata value within the form', () => {
|
||||
expect(form.fields[mdField][0].newValue.value).toEqual('Subject Two');
|
||||
expect(form.fields[mdField][1].newValue.value).toEqual('Subject Three');
|
||||
expect(form.fields[mdField][2].newValue.value).toEqual('Subject One');
|
||||
});
|
||||
|
||||
it('should update the metadata values their new place to match the new physical order', () => {
|
||||
expect(form.fields[mdField][0].newValue.place).toEqual(0);
|
||||
expect(form.fields[mdField][1].newValue.place).toEqual(1);
|
||||
expect(form.fields[mdField][2].newValue.place).toEqual(2);
|
||||
});
|
||||
|
||||
it('should maintain the metadata values their original place in their original value so it can be used later to determine the patch operations', () => {
|
||||
expect(form.fields[mdField][0].originalValue.place).toEqual(1);
|
||||
expect(form.fields[mdField][1].originalValue.place).toEqual(2);
|
||||
expect(form.fields[mdField][2].originalValue.place).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,81 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { DsoEditMetadataChangeType, DsoEditMetadataForm, DsoEditMetadataValue } from '../dso-edit-metadata-form';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dso-edit-metadata-field-values',
|
||||
styleUrls: ['./dso-edit-metadata-field-values.component.scss'],
|
||||
templateUrl: './dso-edit-metadata-field-values.component.html',
|
||||
})
|
||||
/**
|
||||
* Component displaying table rows for each value for a certain metadata field within a form
|
||||
*/
|
||||
export class DsoEditMetadataFieldValuesComponent {
|
||||
/**
|
||||
* The parent {@link DSpaceObject} to display a metadata form for
|
||||
* Also used to determine metadata-representations in case of virtual metadata
|
||||
*/
|
||||
@Input() dso: DSpaceObject;
|
||||
/**
|
||||
* A dynamic form object containing all information about the metadata and the changes made to them, see {@link DsoEditMetadataForm}
|
||||
*/
|
||||
@Input() form: DsoEditMetadataForm;
|
||||
|
||||
/**
|
||||
* Metadata field to display values for
|
||||
*/
|
||||
@Input() mdField: string;
|
||||
|
||||
/**
|
||||
* Type of DSO we're displaying values for
|
||||
* Determines i18n messages
|
||||
*/
|
||||
@Input() dsoType: string;
|
||||
|
||||
/**
|
||||
* Observable to check if the form is being saved or not
|
||||
*/
|
||||
@Input() saving$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Tracks for which metadata-field a drag operation is taking place
|
||||
* Null when no drag is currently happening for any field
|
||||
*/
|
||||
@Input() draggingMdField$: BehaviorSubject<string>;
|
||||
|
||||
/**
|
||||
* Emit when the value has been saved within the form
|
||||
*/
|
||||
@Output() valueSaved: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* The DsoEditMetadataChangeType enumeration for access in the component's template
|
||||
* @type {DsoEditMetadataChangeType}
|
||||
*/
|
||||
public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType;
|
||||
|
||||
/**
|
||||
* Drop a value into a new position
|
||||
* Update the form's value array for the current field to match the dropped position
|
||||
* Update the values their place property to match the new order
|
||||
* Send an update to the parent
|
||||
* @param event
|
||||
*/
|
||||
drop(event: CdkDragDrop<any>) {
|
||||
const dragIndex = event.previousIndex;
|
||||
const dropIndex = event.currentIndex;
|
||||
// Move the value within its field
|
||||
moveItemInArray(this.form.fields[this.mdField], dragIndex, dropIndex);
|
||||
// Update all the values in this field their place property
|
||||
this.form.fields[this.mdField].forEach((value: DsoEditMetadataValue, index: number) => {
|
||||
value.newValue.place = index;
|
||||
value.confirmChanges();
|
||||
});
|
||||
// Update the form statuses
|
||||
this.form.resetReinstatable();
|
||||
this.valueSaved.emit();
|
||||
}
|
||||
}
|
@@ -0,0 +1,275 @@
|
||||
import { DsoEditMetadataChangeType, DsoEditMetadataForm } from './dso-edit-metadata-form';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { MetadataValue } from '../../core/shared/metadata.models';
|
||||
|
||||
describe('DsoEditMetadataForm', () => {
|
||||
let form: DsoEditMetadataForm;
|
||||
let dso: DSpaceObject;
|
||||
|
||||
beforeEach(() => {
|
||||
dso = Object.assign(new DSpaceObject(), {
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
Object.assign(new MetadataValue(), {
|
||||
value: 'Test Title',
|
||||
language: 'en',
|
||||
place: 0,
|
||||
}),
|
||||
],
|
||||
'dc.subject': [
|
||||
Object.assign(new MetadataValue(), {
|
||||
value: 'Subject One',
|
||||
language: 'en',
|
||||
place: 0,
|
||||
}),
|
||||
Object.assign(new MetadataValue(), {
|
||||
value: 'Subject Two',
|
||||
language: 'en',
|
||||
place: 1,
|
||||
}),
|
||||
Object.assign(new MetadataValue(), {
|
||||
value: 'Subject Three',
|
||||
language: 'en',
|
||||
place: 2,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
form = new DsoEditMetadataForm(dso.metadata);
|
||||
});
|
||||
|
||||
|
||||
describe('adding a new value', () => {
|
||||
beforeEach(() => {
|
||||
form.add();
|
||||
});
|
||||
|
||||
it('should add an empty value to \"newValue\" with no place yet and editing set to true', () => {
|
||||
expect(form.newValue).toBeDefined();
|
||||
expect(form.newValue.originalValue.place).toBeUndefined();
|
||||
expect(form.newValue.newValue.place).toBeUndefined();
|
||||
expect(form.newValue.editing).toBeTrue();
|
||||
});
|
||||
|
||||
it('should not mark the form as changed yet', () => {
|
||||
expect(form.hasChanges()).toEqual(false);
|
||||
});
|
||||
|
||||
describe('and assigning a value and metadata field to it', () => {
|
||||
let mdField: string;
|
||||
let value: string;
|
||||
let expectedPlace: number;
|
||||
|
||||
beforeEach(() => {
|
||||
mdField = 'dc.subject';
|
||||
value = 'Subject Four';
|
||||
form.newValue.newValue.value = value;
|
||||
form.setMetadataField(mdField);
|
||||
expectedPlace = form.fields[mdField].length - 1;
|
||||
});
|
||||
|
||||
it('should add the new value to the values of the relevant field', () => {
|
||||
expect(form.fields[mdField][expectedPlace].newValue.value).toEqual(value);
|
||||
});
|
||||
|
||||
it('should set its editing flag to false', () => {
|
||||
expect(form.fields[mdField][expectedPlace].editing).toBeFalse();
|
||||
});
|
||||
|
||||
it('should set both its original and new place to match its position in the value array', () => {
|
||||
expect(form.fields[mdField][expectedPlace].newValue.place).toEqual(expectedPlace);
|
||||
expect(form.fields[mdField][expectedPlace].originalValue.place).toEqual(expectedPlace);
|
||||
});
|
||||
|
||||
it('should clear \"newValue\"', () => {
|
||||
expect(form.newValue).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should mark the form as changed', () => {
|
||||
expect(form.hasChanges()).toEqual(true);
|
||||
});
|
||||
|
||||
describe('discard', () => {
|
||||
beforeEach(() => {
|
||||
form.discard();
|
||||
});
|
||||
|
||||
it('should remove the new value', () => {
|
||||
expect(form.fields[mdField][expectedPlace]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should mark the form as unchanged again', () => {
|
||||
expect(form.hasChanges()).toEqual(false);
|
||||
});
|
||||
|
||||
describe('reinstate', () => {
|
||||
beforeEach(() => {
|
||||
form.reinstate();
|
||||
});
|
||||
|
||||
it('should re-add the new value', () => {
|
||||
expect(form.fields[mdField][expectedPlace].newValue.value).toEqual(value);
|
||||
});
|
||||
|
||||
it('should mark the form as changed once again', () => {
|
||||
expect(form.hasChanges()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removing a value entirely (not just marking deleted)', () => {
|
||||
it('should remove the value on the correct index', () => {
|
||||
form.remove('dc.subject', 1);
|
||||
expect(form.fields['dc.subject'].length).toEqual(2);
|
||||
expect(form.fields['dc.subject'][0].newValue.value).toEqual('Subject One');
|
||||
expect(form.fields['dc.subject'][1].newValue.value).toEqual('Subject Three');
|
||||
});
|
||||
});
|
||||
|
||||
describe('moving a value', () => {
|
||||
beforeEach(() => {
|
||||
form.fields['dc.subject'][0].newValue.place = form.fields['dc.subject'][1].originalValue.place;
|
||||
form.fields['dc.subject'][1].newValue.place = form.fields['dc.subject'][0].originalValue.place;
|
||||
form.fields['dc.subject'][0].confirmChanges();
|
||||
form.fields['dc.subject'][1].confirmChanges();
|
||||
});
|
||||
|
||||
it('should mark the value as changed', () => {
|
||||
expect(form.fields['dc.subject'][0].hasChanges()).toEqual(true);
|
||||
expect(form.fields['dc.subject'][1].hasChanges()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should mark the form as changed', () => {
|
||||
expect(form.hasChanges()).toEqual(true);
|
||||
});
|
||||
|
||||
describe('discard', () => {
|
||||
beforeEach(() => {
|
||||
form.discard();
|
||||
});
|
||||
|
||||
it('should reset the moved values their places to their original values', () => {
|
||||
expect(form.fields['dc.subject'][0].newValue.place).toEqual(form.fields['dc.subject'][0].originalValue.place);
|
||||
expect(form.fields['dc.subject'][1].newValue.place).toEqual(form.fields['dc.subject'][1].originalValue.place);
|
||||
});
|
||||
|
||||
it('should mark the form as unchanged again', () => {
|
||||
expect(form.hasChanges()).toEqual(false);
|
||||
});
|
||||
|
||||
describe('reinstate', () => {
|
||||
beforeEach(() => {
|
||||
form.reinstate();
|
||||
});
|
||||
|
||||
it('should move the values to their new places again', () => {
|
||||
expect(form.fields['dc.subject'][0].newValue.place).toEqual(form.fields['dc.subject'][1].originalValue.place);
|
||||
expect(form.fields['dc.subject'][1].newValue.place).toEqual(form.fields['dc.subject'][0].originalValue.place);
|
||||
});
|
||||
|
||||
it('should mark the form as changed once again', () => {
|
||||
expect(form.hasChanges()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('marking a value deleted', () => {
|
||||
beforeEach(() => {
|
||||
form.fields['dc.title'][0].change = DsoEditMetadataChangeType.REMOVE;
|
||||
});
|
||||
|
||||
it('should mark the value as changed', () => {
|
||||
expect(form.fields['dc.title'][0].hasChanges()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should mark the form as changed', () => {
|
||||
expect(form.hasChanges()).toEqual(true);
|
||||
});
|
||||
|
||||
describe('discard', () => {
|
||||
beforeEach(() => {
|
||||
form.discard();
|
||||
});
|
||||
|
||||
it('should remove the deleted mark from the value', () => {
|
||||
expect(form.fields['dc.title'][0].change).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should mark the form as unchanged again', () => {
|
||||
expect(form.hasChanges()).toEqual(false);
|
||||
});
|
||||
|
||||
describe('reinstate', () => {
|
||||
beforeEach(() => {
|
||||
form.reinstate();
|
||||
});
|
||||
|
||||
it('should re-mark the value as deleted', () => {
|
||||
expect(form.fields['dc.title'][0].change).toEqual(DsoEditMetadataChangeType.REMOVE);
|
||||
});
|
||||
|
||||
it('should mark the form as changed once again', () => {
|
||||
expect(form.hasChanges()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('editing a value', () => {
|
||||
const value = 'New title';
|
||||
|
||||
beforeEach(() => {
|
||||
form.fields['dc.title'][0].editing = true;
|
||||
form.fields['dc.title'][0].newValue.value = value;
|
||||
});
|
||||
|
||||
it('should not mark the form as changed yet', () => {
|
||||
expect(form.hasChanges()).toEqual(false);
|
||||
});
|
||||
|
||||
describe('and confirming the changes', () => {
|
||||
beforeEach(() => {
|
||||
form.fields['dc.title'][0].confirmChanges(true);
|
||||
});
|
||||
|
||||
it('should mark the value as changed', () => {
|
||||
expect(form.fields['dc.title'][0].hasChanges()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should mark the form as changed', () => {
|
||||
expect(form.hasChanges()).toEqual(true);
|
||||
});
|
||||
|
||||
describe('discard', () => {
|
||||
beforeEach(() => {
|
||||
form.discard();
|
||||
});
|
||||
|
||||
it('should reset the changed value to its original value', () => {
|
||||
expect(form.fields['dc.title'][0].newValue.value).toEqual(form.fields['dc.title'][0].originalValue.value);
|
||||
});
|
||||
|
||||
it('should mark the form as unchanged again', () => {
|
||||
expect(form.hasChanges()).toEqual(false);
|
||||
});
|
||||
|
||||
describe('reinstate', () => {
|
||||
beforeEach(() => {
|
||||
form.reinstate();
|
||||
});
|
||||
|
||||
it('should put the changed value back in place', () => {
|
||||
expect(form.fields['dc.title'][0].newValue.value).toEqual(value);
|
||||
});
|
||||
|
||||
it('should mark the form as changed once again', () => {
|
||||
expect(form.hasChanges()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
453
src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-form.ts
Normal file
453
src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-form.ts
Normal file
@@ -0,0 +1,453 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { MetadataMap, MetadataValue } from '../../core/shared/metadata.models';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { MoveOperation, Operation } from 'fast-json-patch';
|
||||
import { MetadataPatchReplaceOperation } from '../../core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-replace-operation.model';
|
||||
import { MetadataPatchRemoveOperation } from '../../core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-remove-operation.model';
|
||||
import { MetadataPatchAddOperation } from '../../core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-add-operation.model';
|
||||
import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analyzer.service';
|
||||
import { MetadataPatchMoveOperation } from '../../core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model';
|
||||
|
||||
/**
|
||||
* Enumeration for the type of change occurring on a metadata value
|
||||
*/
|
||||
export enum DsoEditMetadataChangeType {
|
||||
UPDATE = 1,
|
||||
ADD = 2,
|
||||
REMOVE = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Class holding information about a metadata value and its changes within an edit form
|
||||
*/
|
||||
export class DsoEditMetadataValue {
|
||||
/**
|
||||
* The original metadata value (should stay the same!) used to compare changes with
|
||||
*/
|
||||
originalValue: MetadataValue;
|
||||
|
||||
/**
|
||||
* The new value, dynamically changing
|
||||
*/
|
||||
newValue: MetadataValue;
|
||||
|
||||
/**
|
||||
* A value that can be used to undo any discarding that took place
|
||||
*/
|
||||
reinstatableValue: MetadataValue;
|
||||
|
||||
/**
|
||||
* Whether or not this value is currently being edited or not
|
||||
*/
|
||||
editing = false;
|
||||
|
||||
/**
|
||||
* The type of change that's taking place on this metadata value
|
||||
* Empty if no changes are made
|
||||
*/
|
||||
change: DsoEditMetadataChangeType;
|
||||
|
||||
/**
|
||||
* A flag to keep track if the value has been reordered (place has changed)
|
||||
*/
|
||||
reordered = false;
|
||||
|
||||
/**
|
||||
* A type or change that can be used to undo any discarding that took place
|
||||
*/
|
||||
reinstatableChange: DsoEditMetadataChangeType;
|
||||
|
||||
constructor(value: MetadataValue, added = false) {
|
||||
this.originalValue = value;
|
||||
this.newValue = Object.assign(new MetadataValue(), value);
|
||||
if (added) {
|
||||
this.change = DsoEditMetadataChangeType.ADD;
|
||||
this.editing = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current changes made to the metadata value
|
||||
* This will set the type of change to UPDATE if the new metadata value's value and/or language are different from
|
||||
* the original value
|
||||
* It will also set the editing flag to false
|
||||
*/
|
||||
confirmChanges(finishEditing = false) {
|
||||
this.reordered = this.originalValue.place !== this.newValue.place;
|
||||
if (hasNoValue(this.change) || this.change === DsoEditMetadataChangeType.UPDATE) {
|
||||
if ((this.originalValue.value !== this.newValue.value || this.originalValue.language !== this.newValue.language)) {
|
||||
this.change = DsoEditMetadataChangeType.UPDATE;
|
||||
} else {
|
||||
this.change = undefined;
|
||||
}
|
||||
}
|
||||
if (finishEditing) {
|
||||
this.editing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the current value contains changes or not
|
||||
* If the metadata value contains changes, but they haven't been confirmed yet through confirmChanges(), this might
|
||||
* return false (which is desired)
|
||||
*/
|
||||
hasChanges(): boolean {
|
||||
return hasValue(this.change) || this.reordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard the current changes and mark the value and change type re-instatable by storing them in their relevant
|
||||
* properties
|
||||
*/
|
||||
discardAndMarkReinstatable(): void {
|
||||
if (this.change === DsoEditMetadataChangeType.UPDATE || this.reordered) {
|
||||
this.reinstatableValue = this.newValue;
|
||||
}
|
||||
this.reinstatableChange = this.change;
|
||||
this.discard(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard the current changes
|
||||
* Call discardAndMarkReinstatable() instead, if the discard should be re-instatable
|
||||
*/
|
||||
discard(keepPlace = true): void {
|
||||
this.change = undefined;
|
||||
const place = this.newValue.place;
|
||||
this.newValue = Object.assign(new MetadataValue(), this.originalValue);
|
||||
if (keepPlace) {
|
||||
this.newValue.place = place;
|
||||
}
|
||||
this.confirmChanges(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-instate (undo) the last discard by replacing the value and change type with their reinstate properties (if present)
|
||||
*/
|
||||
reinstate(): void {
|
||||
if (hasValue(this.reinstatableValue)) {
|
||||
this.newValue = this.reinstatableValue;
|
||||
this.reinstatableValue = undefined;
|
||||
}
|
||||
if (hasValue(this.reinstatableChange)) {
|
||||
this.change = this.reinstatableChange;
|
||||
this.reinstatableChange = undefined;
|
||||
}
|
||||
this.confirmChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if either the value or change type have a re-instatable property
|
||||
* This will be the case if a discard has taken place that undid changes to the value or type
|
||||
*/
|
||||
isReinstatable(): boolean {
|
||||
return hasValue(this.reinstatableValue) || hasValue(this.reinstatableChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the state of the re-instatable properties
|
||||
*/
|
||||
resetReinstatable() {
|
||||
this.reinstatableValue = undefined;
|
||||
this.reinstatableChange = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class holding information about the metadata of a DSpaceObject and its changes within an edit form
|
||||
*/
|
||||
export class DsoEditMetadataForm {
|
||||
/**
|
||||
* List of original metadata field keys (before any changes took place)
|
||||
*/
|
||||
originalFieldKeys: string[];
|
||||
|
||||
/**
|
||||
* List of current metadata field keys (includes new fields for values added by the user)
|
||||
*/
|
||||
fieldKeys: string[];
|
||||
|
||||
/**
|
||||
* Current state of the form
|
||||
* Key: Metadata field
|
||||
* Value: List of {@link DsoEditMetadataValue}s for the metadata field
|
||||
*/
|
||||
fields: {
|
||||
[mdField: string]: DsoEditMetadataValue[],
|
||||
};
|
||||
|
||||
/**
|
||||
* A map of previously added metadata values before a discard of the form took place
|
||||
* This can be used to re-instate the entire form to before the discard taking place
|
||||
*/
|
||||
reinstatableNewValues: {
|
||||
[mdField: string]: DsoEditMetadataValue[],
|
||||
};
|
||||
|
||||
/**
|
||||
* A (temporary) new metadata value added by the user, not belonging to a metadata field yet
|
||||
* This value will be finalised and added to a field using setMetadataField()
|
||||
*/
|
||||
newValue: DsoEditMetadataValue;
|
||||
|
||||
constructor(metadata: MetadataMap) {
|
||||
this.originalFieldKeys = [];
|
||||
this.fieldKeys = [];
|
||||
this.fields = {};
|
||||
this.reinstatableNewValues = {};
|
||||
Object.entries(metadata).forEach(([mdField, values]: [string, MetadataValue[]]) => {
|
||||
this.originalFieldKeys.push(mdField);
|
||||
this.fieldKeys.push(mdField);
|
||||
this.setValuesForFieldSorted(mdField, values.map((value: MetadataValue) => new DsoEditMetadataValue(value)));
|
||||
});
|
||||
this.sortFieldKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new temporary value for the user to edit
|
||||
*/
|
||||
add(): void {
|
||||
if (hasNoValue(this.newValue)) {
|
||||
this.newValue = new DsoEditMetadataValue(new MetadataValue(), true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the temporary value to a metadata field
|
||||
* Clear the temporary value afterwards
|
||||
* @param mdField
|
||||
*/
|
||||
setMetadataField(mdField: string): void {
|
||||
this.newValue.editing = false;
|
||||
this.addValueToField(this.newValue, mdField);
|
||||
// Set the place property to match the new value's position within its field
|
||||
const place = this.fields[mdField].length - 1;
|
||||
this.fields[mdField][place].originalValue.place = place;
|
||||
this.fields[mdField][place].newValue.place = place;
|
||||
this.newValue = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a value to a metadata field within the map
|
||||
* @param value
|
||||
* @param mdField
|
||||
* @private
|
||||
*/
|
||||
private addValueToField(value: DsoEditMetadataValue, mdField: string): void {
|
||||
if (isEmpty(this.fields[mdField])) {
|
||||
this.fieldKeys.push(mdField);
|
||||
this.sortFieldKeys();
|
||||
this.fields[mdField] = [];
|
||||
}
|
||||
this.fields[mdField].push(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a value from a metadata field on a given index (this actually removes the value, not just marking it deleted)
|
||||
* @param mdField
|
||||
* @param index
|
||||
*/
|
||||
remove(mdField: string, index: number): void {
|
||||
if (isNotEmpty(this.fields[mdField])) {
|
||||
this.fields[mdField].splice(index, 1);
|
||||
if (this.fields[mdField].length === 0) {
|
||||
this.fieldKeys.splice(this.fieldKeys.indexOf(mdField), 1);
|
||||
delete this.fields[mdField];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if at least one value within the form contains a change
|
||||
*/
|
||||
hasChanges(): boolean {
|
||||
return Object.values(this.fields).some((values: DsoEditMetadataValue[]) => values.some((value: DsoEditMetadataValue) => value.hasChanges()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a metadata field contains changes within its order (place property of values)
|
||||
* @param mdField
|
||||
*/
|
||||
hasOrderChanges(mdField: string): boolean {
|
||||
return this.fields[mdField].some((value: DsoEditMetadataValue) => value.originalValue.place !== value.newValue.place);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard all changes within the form and store their current values within re-instatable properties so they can be
|
||||
* undone afterwards
|
||||
*/
|
||||
discard(): void {
|
||||
this.resetReinstatable();
|
||||
// Discard changes from each value from each field
|
||||
Object.entries(this.fields).forEach(([field, values]: [string, DsoEditMetadataValue[]]) => {
|
||||
let removeFromIndex = -1;
|
||||
values.forEach((value: DsoEditMetadataValue, index: number) => {
|
||||
if (value.change === DsoEditMetadataChangeType.ADD) {
|
||||
if (isEmpty(this.reinstatableNewValues[field])) {
|
||||
this.reinstatableNewValues[field] = [];
|
||||
}
|
||||
this.reinstatableNewValues[field].push(value);
|
||||
if (removeFromIndex === -1) {
|
||||
removeFromIndex = index;
|
||||
}
|
||||
} else {
|
||||
value.discardAndMarkReinstatable();
|
||||
}
|
||||
});
|
||||
if (removeFromIndex > -1) {
|
||||
this.fields[field].splice(removeFromIndex, this.fields[field].length - removeFromIndex);
|
||||
}
|
||||
});
|
||||
// Delete new metadata fields
|
||||
this.fieldKeys.forEach((field: string) => {
|
||||
if (this.originalFieldKeys.indexOf(field) < 0) {
|
||||
delete this.fields[field];
|
||||
}
|
||||
});
|
||||
this.fieldKeys = [...this.originalFieldKeys];
|
||||
this.sortFieldKeys();
|
||||
// Reset the order of values within their fields to match their place property
|
||||
this.fieldKeys.forEach((field: string) => {
|
||||
this.setValuesForFieldSorted(field, this.fields[field]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the order of values within a metadata field to their original places
|
||||
* Update the actual array to match the place properties
|
||||
* @param mdField
|
||||
*/
|
||||
resetOrder(mdField: string) {
|
||||
this.fields[mdField].forEach((value: DsoEditMetadataValue) => {
|
||||
value.newValue.place = value.originalValue.place;
|
||||
value.confirmChanges();
|
||||
});
|
||||
this.setValuesForFieldSorted(mdField, this.fields[mdField]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort fieldKeys alphabetically
|
||||
* Should be called whenever a field is added to ensure the alphabetical order is kept
|
||||
*/
|
||||
sortFieldKeys() {
|
||||
this.fieldKeys.sort((a: string, b: string) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo any previously discarded changes
|
||||
*/
|
||||
reinstate(): void {
|
||||
// Reinstate each value
|
||||
Object.values(this.fields).forEach((values: DsoEditMetadataValue[]) => {
|
||||
values.forEach((value: DsoEditMetadataValue) => {
|
||||
value.reinstate();
|
||||
});
|
||||
});
|
||||
// Re-add new values
|
||||
Object.entries(this.reinstatableNewValues).forEach(([field, values]: [string, DsoEditMetadataValue[]]) => {
|
||||
values.forEach((value: DsoEditMetadataValue) => {
|
||||
this.addValueToField(value, field);
|
||||
});
|
||||
});
|
||||
// Reset the order of values within their fields to match their place property
|
||||
this.fieldKeys.forEach((field: string) => {
|
||||
this.setValuesForFieldSorted(field, this.fields[field]);
|
||||
});
|
||||
this.reinstatableNewValues = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if at least one value contains a re-instatable property, meaning a discard can be reversed
|
||||
*/
|
||||
isReinstatable(): boolean {
|
||||
return isNotEmpty(this.reinstatableNewValues) ||
|
||||
Object.values(this.fields)
|
||||
.some((values: DsoEditMetadataValue[]) => values
|
||||
.some((value: DsoEditMetadataValue) => value.isReinstatable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the state of the re-instatable properties and values
|
||||
*/
|
||||
resetReinstatable(): void {
|
||||
this.reinstatableNewValues = {};
|
||||
Object.values(this.fields).forEach((values: DsoEditMetadataValue[]) => {
|
||||
values.forEach((value: DsoEditMetadataValue) => {
|
||||
value.resetReinstatable();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values of a metadata field and sort them by their newValue's place property
|
||||
* @param mdField
|
||||
* @param values
|
||||
*/
|
||||
private setValuesForFieldSorted(mdField: string, values: DsoEditMetadataValue[]) {
|
||||
this.fields[mdField] = values.sort((a: DsoEditMetadataValue, b: DsoEditMetadataValue) => a.newValue.place - b.newValue.place);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the json PATCH operations for the current changes within this form
|
||||
* For each metadata field, it'll return operations in the following order: replace, remove (from last to first place), add and move
|
||||
* This order is important, as each operation is executed in succession of the previous one
|
||||
*/
|
||||
getOperations(moveAnalyser: ArrayMoveChangeAnalyzer<number>): Operation[] {
|
||||
const operations: Operation[] = [];
|
||||
Object.entries(this.fields).forEach(([field, values]: [string, DsoEditMetadataValue[]]) => {
|
||||
const replaceOperations: MetadataPatchReplaceOperation[] = [];
|
||||
const removeOperations: MetadataPatchRemoveOperation[] = [];
|
||||
const addOperations: MetadataPatchAddOperation[] = [];
|
||||
[...values]
|
||||
.sort((a: DsoEditMetadataValue, b: DsoEditMetadataValue) => a.originalValue.place - b.originalValue.place)
|
||||
.forEach((value: DsoEditMetadataValue) => {
|
||||
if (hasValue(value.change)) {
|
||||
if (value.change === DsoEditMetadataChangeType.UPDATE) {
|
||||
// Only changes to value or language are considered "replace" operations. Changes to place are considered "move", which is processed below.
|
||||
if (value.originalValue.value !== value.newValue.value || value.originalValue.language !== value.newValue.language) {
|
||||
replaceOperations.push(new MetadataPatchReplaceOperation(field, value.originalValue.place, {
|
||||
value: value.newValue.value,
|
||||
language: value.newValue.language,
|
||||
}));
|
||||
}
|
||||
} else if (value.change === DsoEditMetadataChangeType.REMOVE) {
|
||||
removeOperations.push(new MetadataPatchRemoveOperation(field, value.originalValue.place));
|
||||
} else if (value.change === DsoEditMetadataChangeType.ADD) {
|
||||
addOperations.push(new MetadataPatchAddOperation(field, {
|
||||
value: value.newValue.value,
|
||||
language: value.newValue.language,
|
||||
}));
|
||||
} else {
|
||||
console.warn('Illegal metadata change state detected for', value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
operations.push(...replaceOperations
|
||||
.map((operation: MetadataPatchReplaceOperation) => operation.toOperation()));
|
||||
operations.push(...removeOperations
|
||||
// Sort remove operations backwards first, because they get executed in order. This avoids one removal affecting the next.
|
||||
.sort((a: MetadataPatchRemoveOperation, b: MetadataPatchRemoveOperation) => b.place - a.place)
|
||||
.map((operation: MetadataPatchRemoveOperation) => operation.toOperation()));
|
||||
operations.push(...addOperations
|
||||
.map((operation: MetadataPatchAddOperation) => operation.toOperation()));
|
||||
});
|
||||
// Calculate and add the move operations that need to happen in order to move value from their old place to their new within the field
|
||||
// This uses an ArrayMoveChangeAnalyzer
|
||||
Object.entries(this.fields).forEach(([field, values]: [string, DsoEditMetadataValue[]]) => {
|
||||
// Exclude values marked for removal, because operations are executed in order (remove first, then move)
|
||||
const valuesWithoutRemoved = values.filter((value: DsoEditMetadataValue) => value.change !== DsoEditMetadataChangeType.REMOVE);
|
||||
const moveOperations = moveAnalyser
|
||||
.diff(
|
||||
[...valuesWithoutRemoved]
|
||||
.sort((a: DsoEditMetadataValue, b: DsoEditMetadataValue) => a.originalValue.place - b.originalValue.place)
|
||||
.map((value: DsoEditMetadataValue) => value.originalValue.place),
|
||||
[...valuesWithoutRemoved]
|
||||
.sort((a: DsoEditMetadataValue, b: DsoEditMetadataValue) => a.newValue.place - b.newValue.place)
|
||||
.map((value: DsoEditMetadataValue) => value.originalValue.place))
|
||||
.map((operation: MoveOperation) => new MetadataPatchMoveOperation(field, +operation.from.substr(1), +operation.path.substr(1)).toOperation());
|
||||
operations.push(...moveOperations);
|
||||
});
|
||||
return operations;
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
<div class="d-flex flex-row ds-field-row ds-header-row">
|
||||
<div class="lbl-cell">{{ dsoType + '.edit.metadata.headers.field' | translate }}</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex flex-row">
|
||||
<div class="flex-grow-1 ds-flex-cell ds-value-cell"><b class="dont-break-out preserve-line-breaks">{{ dsoType + '.edit.metadata.headers.value' | translate }}</b></div>
|
||||
<div class="ds-flex-cell ds-lang-cell"><b>{{ dsoType + '.edit.metadata.headers.language' | translate }}</b></div>
|
||||
<div class="text-center ds-flex-cell ds-edit-cell"><b>{{ dsoType + '.edit.metadata.headers.edit' | translate }}</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,12 @@
|
||||
.lbl-cell {
|
||||
min-width: var(--ds-dso-edit-field-width);
|
||||
max-width: var(--ds-dso-edit-field-width);
|
||||
background-color: var(--bs-gray-100);
|
||||
font-weight: bold;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--bs-gray-200);
|
||||
}
|
||||
|
||||
.ds-header-row {
|
||||
background-color: var(--bs-gray-100);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user