Merge branch 'main' into w2p-88082_password-registration-link-fixes-main

This commit is contained in:
Kristof De Langhe
2022-03-03 12:36:30 +01:00
130 changed files with 24894 additions and 15656 deletions

View File

@@ -63,13 +63,14 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
inFocus$: BehaviorSubject<boolean>;
constructor(protected menuService: MenuService,
constructor(
protected menuService: MenuService,
protected injector: Injector,
private variableService: CSSVariableService,
private authService: AuthService,
private modalService: NgbModal,
protected variableService: CSSVariableService,
protected authService: AuthService,
protected modalService: NgbModal,
public authorizationService: AuthorizationDataService,
private scriptDataService: ScriptDataService,
protected scriptDataService: ScriptDataService,
public route: ActivatedRoute
) {
super(menuService, injector, authorizationService, route);

View File

@@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { AdminSidebarComponent } from './admin-sidebar.component';
/**
* Themed wrapper for AdminSidebarComponent
*/
@Component({
selector: 'ds-themed-admin-sidebar',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedAdminSidebarComponent extends ThemedComponent<AdminSidebarComponent> {
protected getComponentName(): string {
return 'AdminSidebarComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/admin/admin-sidebar/admin-sidebar.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import('./admin-sidebar.component');
}
}

View File

@@ -32,6 +32,12 @@ export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: st
};
}
export const HOME_PAGE_PATH = 'admin';
export function getHomePageRoute() {
return `/${HOME_PAGE_PATH}`;
}
export const ADMIN_MODULE_PATH = 'admin';
export function getAdminModuleRoute() {

View File

@@ -56,6 +56,7 @@ import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component';
import { ThemedAdminSidebarComponent } from './admin/admin-sidebar/themed-admin-sidebar.component';
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
@@ -171,6 +172,7 @@ const DECLARATIONS = [
HeaderNavbarWrapperComponent,
ThemedHeaderNavbarWrapperComponent,
AdminSidebarComponent,
ThemedAdminSidebarComponent,
AdminSidebarSectionComponent,
ExpandableAdminSidebarSectionComponent,
FooterComponent,

View File

@@ -12,7 +12,7 @@
</div>
</div>
</div>
<ds-form [formId]="'edit-bitstream-form-id'"
<ds-form *ngIf="formGroup" [formId]="'edit-bitstream-form-id'"
[formGroup]="formGroup"
[formModel]="formModel"
[formLayout]="formLayout"

View File

@@ -6,3 +6,7 @@
}
}
}
:host ::ng-deep ds-dynamic-form-control-container > div > label {
margin-top: 1.75rem;
}

View File

@@ -22,6 +22,8 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } f
import { getEntityEditRoute } from '../../item-page/item-page-routing-paths';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { Item } from '../../core/shared/item.model';
import { MetadataValueFilter } from '../../core/shared/metadata.models';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
@@ -31,24 +33,27 @@ let notificationsService: NotificationsService;
let formService: DynamicFormService;
let bitstreamService: BitstreamDataService;
let bitstreamFormatService: BitstreamFormatDataService;
let dsoNameService: DSONameService;
let bitstream: Bitstream;
let selectedFormat: BitstreamFormat;
let allFormats: BitstreamFormat[];
let router: Router;
describe('EditBitstreamPageComponent', () => {
let comp: EditBitstreamPageComponent;
let fixture: ComponentFixture<EditBitstreamPageComponent>;
let comp: EditBitstreamPageComponent;
let fixture: ComponentFixture<EditBitstreamPageComponent>;
beforeEach(waitForAsync(() => {
describe('EditBitstreamPageComponent', () => {
beforeEach(() => {
allFormats = [
Object.assign({
id: '1',
shortDescription: 'Unknown',
description: 'Unknown format',
supportLevel: BitstreamFormatSupportLevel.Unknown,
mimetype: 'application/octet-stream',
_links: {
self: { href: 'format-selflink-1' }
self: {href: 'format-selflink-1'}
}
}),
Object.assign({
@@ -56,8 +61,9 @@ describe('EditBitstreamPageComponent', () => {
shortDescription: 'PNG',
description: 'Portable Network Graphics',
supportLevel: BitstreamFormatSupportLevel.Known,
mimetype: 'image/png',
_links: {
self: { href: 'format-selflink-2' }
self: {href: 'format-selflink-2'}
}
}),
Object.assign({
@@ -65,19 +71,14 @@ describe('EditBitstreamPageComponent', () => {
shortDescription: 'GIF',
description: 'Graphics Interchange Format',
supportLevel: BitstreamFormatSupportLevel.Known,
mimetype: 'image/gif',
_links: {
self: { href: 'format-selflink-3' }
self: {href: 'format-selflink-3'}
}
})
] as BitstreamFormat[];
selectedFormat = allFormats[1];
notificationsService = jasmine.createSpyObj('notificationsService',
{
info: infoNotification,
warning: warningNotification,
success: successNotification
}
);
formService = Object.assign({
createFormGroup: (fModel: DynamicFormControlModel[]) => {
const controls = {};
@@ -90,156 +91,418 @@ describe('EditBitstreamPageComponent', () => {
return undefined;
}
});
bitstream = Object.assign(new Bitstream(), {
metadata: {
'dc.description': [
{
value: 'Bitstream description'
}
],
'dc.title': [
{
value: 'Bitstream title'
}
]
},
format: createSuccessfulRemoteDataObject$(selectedFormat),
_links: {
self: 'bitstream-selflink'
},
bundle: createSuccessfulRemoteDataObject$({
item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), {
uuid: 'some-uuid'
}))
})
});
bitstreamService = jasmine.createSpyObj('bitstreamService', {
findById: createSuccessfulRemoteDataObject$(bitstream),
update: createSuccessfulRemoteDataObject$(bitstream),
updateFormat: createSuccessfulRemoteDataObject$(bitstream),
commitUpdates: {},
patch: {}
});
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats))
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule],
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
providers: [
{ provide: NotificationsService, useValue: notificationsService },
{ provide: DynamicFormService, useValue: formService },
{ provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), snapshot: { queryParams: {} } } },
{ provide: BitstreamDataService, useValue: bitstreamService },
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
ChangeDetectorRef
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EditBitstreamPageComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
router = TestBed.inject(Router);
spyOn(router, 'navigate');
notificationsService = jasmine.createSpyObj('notificationsService',
{
info: infoNotification,
warning: warningNotification,
success: successNotification
}
);
});
describe('on startup', () => {
let rawForm;
describe('EditBitstreamPageComponent no IIIF fields', () => {
beforeEach(waitForAsync(() => {
const bundleName = 'ORIGINAL';
bitstream = Object.assign(new Bitstream(), {
metadata: {
'dc.description': [
{
value: 'Bitstream description'
}
],
'dc.title': [
{
value: 'Bitstream title'
}
]
},
format: createSuccessfulRemoteDataObject$(selectedFormat),
_links: {
self: 'bitstream-selflink'
},
bundle: createSuccessfulRemoteDataObject$({
item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), {
uuid: 'some-uuid',
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
return undefined;
},
}))
})
});
bitstreamService = jasmine.createSpyObj('bitstreamService', {
findById: createSuccessfulRemoteDataObject$(bitstream),
update: createSuccessfulRemoteDataObject$(bitstream),
updateFormat: createSuccessfulRemoteDataObject$(bitstream),
commitUpdates: {},
patch: {}
});
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats))
});
dsoNameService = jasmine.createSpyObj('dsoNameService', {
getName: bundleName
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule],
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
providers: [
{provide: NotificationsService, useValue: notificationsService},
{provide: DynamicFormService, useValue: formService},
{provide: ActivatedRoute,
useValue: {
data: observableOf({bitstream: createSuccessfulRemoteDataObject(bitstream)}),
snapshot: {queryParams: {}}
}
},
{provide: BitstreamDataService, useValue: bitstreamService},
{provide: DSONameService, useValue: dsoNameService},
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
ChangeDetectorRef
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
rawForm = comp.formGroup.getRawValue();
fixture = TestBed.createComponent(EditBitstreamPageComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
router = TestBed.inject(Router);
spyOn(router, 'navigate');
});
it('should fill in the bitstream\'s title', () => {
expect(rawForm.fileNamePrimaryContainer.fileName).toEqual(bitstream.name);
});
describe('on startup', () => {
let rawForm;
it('should fill in the bitstream\'s description', () => {
expect(rawForm.descriptionContainer.description).toEqual(bitstream.firstMetadataValue('dc.description'));
});
it('should select the correct format', () => {
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id);
});
it('should put the \"New Format\" input on invisible', () => {
expect(comp.formLayout.newFormat.grid.host).toContain('invisible');
});
});
describe('when an unknown format is selected', () => {
beforeEach(() => {
comp.updateNewFormatLayout(allFormats[0].id);
});
it('should remove the invisible class from the \"New Format\" input', () => {
expect(comp.formLayout.newFormat.grid.host).not.toContain('invisible');
});
});
describe('onSubmit', () => {
describe('when selected format hasn\'t changed', () => {
beforeEach(() => {
comp.onSubmit();
rawForm = comp.formGroup.getRawValue();
});
it('should call update', () => {
expect(bitstreamService.update).toHaveBeenCalled();
it('should fill in the bitstream\'s title', () => {
expect(rawForm.fileNamePrimaryContainer.fileName).toEqual(bitstream.name);
});
it('should commit the updates', () => {
expect(bitstreamService.commitUpdates).toHaveBeenCalled();
it('should fill in the bitstream\'s description', () => {
expect(rawForm.descriptionContainer.description).toEqual(bitstream.firstMetadataValue('dc.description'));
});
it('should select the correct format', () => {
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id);
});
it('should put the \"New Format\" input on invisible', () => {
expect(comp.formLayout.newFormat.grid.host).toContain('invisible');
});
});
describe('when selected format has changed', () => {
describe('when an unknown format is selected', () => {
beforeEach(() => {
comp.formGroup.patchValue({
formatContainer: {
selectedFormat: allFormats[2].id
}
comp.updateNewFormatLayout(allFormats[0].id);
});
it('should remove the invisible class from the \"New Format\" input', () => {
expect(comp.formLayout.newFormat.grid.host).not.toContain('invisible');
});
});
describe('onSubmit', () => {
describe('when selected format hasn\'t changed', () => {
beforeEach(() => {
comp.onSubmit();
});
it('should call update', () => {
expect(bitstreamService.update).toHaveBeenCalled();
});
it('should commit the updates', () => {
expect(bitstreamService.commitUpdates).toHaveBeenCalled();
});
});
describe('when selected format has changed', () => {
beforeEach(() => {
comp.formGroup.patchValue({
formatContainer: {
selectedFormat: allFormats[2].id
}
});
fixture.detectChanges();
comp.onSubmit();
});
it('should call update', () => {
expect(bitstreamService.update).toHaveBeenCalled();
});
it('should call updateFormat', () => {
expect(bitstreamService.updateFormat).toHaveBeenCalled();
});
it('should commit the updates', () => {
expect(bitstreamService.commitUpdates).toHaveBeenCalled();
});
});
});
describe('when the cancel button is clicked', () => {
it('should call navigateToItemEditBitstreams method', () => {
spyOn(comp, 'navigateToItemEditBitstreams');
comp.onCancel();
expect(comp.navigateToItemEditBitstreams).toHaveBeenCalled();
});
});
describe('when navigateToItemEditBitstreams is called, and the component has an itemId', () => {
it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
comp.itemId = 'some-uuid1';
comp.navigateToItemEditBitstreams();
expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']);
});
});
describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => {
it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
comp.itemId = undefined;
comp.navigateToItemEditBitstreams();
expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']);
});
});
});
describe('EditBitstreamPageComponent with IIIF fields', () => {
const bundleName = 'ORIGINAL';
beforeEach(waitForAsync(() => {
bitstream = Object.assign(new Bitstream(), {
metadata: {
'dc.description': [
{
value: 'Bitstream description'
}
],
'dc.title': [
{
value: 'Bitstream title'
}
],
'iiif.label': [
{
value: 'chapter one'
}
],
'iiif.toc': [
{
value: 'chapter one'
}
],
'iiif.image.width': [
{
value: '2400'
}
],
'iiif.image.height': [
{
value: '2800'
}
],
},
format: createSuccessfulRemoteDataObject$(allFormats[1]),
_links: {
self: 'bitstream-selflink'
},
bundle: createSuccessfulRemoteDataObject$({
item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), {
uuid: 'some-uuid',
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
return 'True';
}
}))
})
});
bitstreamService = jasmine.createSpyObj('bitstreamService', {
findById: createSuccessfulRemoteDataObject$(bitstream),
update: createSuccessfulRemoteDataObject$(bitstream),
updateFormat: createSuccessfulRemoteDataObject$(bitstream),
commitUpdates: {},
patch: {}
});
dsoNameService = jasmine.createSpyObj('dsoNameService', {
getName: bundleName
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule],
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
providers: [
{provide: NotificationsService, useValue: notificationsService},
{provide: DynamicFormService, useValue: formService},
{
provide: ActivatedRoute,
useValue: {
data: observableOf({bitstream: createSuccessfulRemoteDataObject(bitstream)}),
snapshot: {queryParams: {}}
}
},
{provide: BitstreamDataService, useValue: bitstreamService},
{provide: DSONameService, useValue: dsoNameService},
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
ChangeDetectorRef
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EditBitstreamPageComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
router = TestBed.inject(Router);
spyOn(router, 'navigate');
});
describe('on startup', () => {
let rawForm;
beforeEach(() => {
rawForm = comp.formGroup.getRawValue();
});
it('should set isIIIF to true', () => {
expect(comp.isIIIF).toBeTrue();
});
it('should fill in the iiif label', () => {
expect(rawForm.iiifLabelContainer.iiifLabel).toEqual('chapter one');
});
it('should fill in the iiif toc', () => {
expect(rawForm.iiifTocContainer.iiifToc).toEqual('chapter one');
});
it('should fill in the iiif width', () => {
expect(rawForm.iiifWidthContainer.iiifWidth).toEqual('2400');
});
it('should fill in the iiif height', () => {
expect(rawForm.iiifHeightContainer.iiifHeight).toEqual('2800');
});
});
});
describe('ignore OTHERCONTENT bundle', () => {
const bundleName = 'OTHERCONTENT';
beforeEach(waitForAsync(() => {
bitstream = Object.assign(new Bitstream(), {
metadata: {
'dc.description': [
{
value: 'Bitstream description'
}
],
'dc.title': [
{
value: 'Bitstream title'
}
],
'iiif.label': [
{
value: 'chapter one'
}
],
'iiif.toc': [
{
value: 'chapter one'
}
],
'iiif.image.width': [
{
value: '2400'
}
],
'iiif.image.height': [
{
value: '2800'
}
],
},
format: createSuccessfulRemoteDataObject$(allFormats[2]),
_links: {
self: 'bitstream-selflink'
},
bundle: createSuccessfulRemoteDataObject$({
item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), {
uuid: 'some-uuid',
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
return 'True';
}
}))
})
});
bitstreamService = jasmine.createSpyObj('bitstreamService', {
findById: createSuccessfulRemoteDataObject$(bitstream),
update: createSuccessfulRemoteDataObject$(bitstream),
updateFormat: createSuccessfulRemoteDataObject$(bitstream),
commitUpdates: {},
patch: {}
});
dsoNameService = jasmine.createSpyObj('dsoNameService', {
getName: bundleName
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule],
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
providers: [
{provide: NotificationsService, useValue: notificationsService},
{provide: DynamicFormService, useValue: formService},
{provide: ActivatedRoute,
useValue: {
data: observableOf({bitstream: createSuccessfulRemoteDataObject(bitstream)}),
snapshot: {queryParams: {}}
}
},
{provide: BitstreamDataService, useValue: bitstreamService},
{provide: DSONameService, useValue: dsoNameService},
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
ChangeDetectorRef
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EditBitstreamPageComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
comp.onSubmit();
router = TestBed.inject(Router);
spyOn(router, 'navigate');
});
it('should call update', () => {
expect(bitstreamService.update).toHaveBeenCalled();
});
describe('EditBitstreamPageComponent with IIIF fields', () => {
let rawForm;
it('should call updateFormat', () => {
expect(bitstreamService.updateFormat).toHaveBeenCalled();
});
beforeEach(() => {
rawForm = comp.formGroup.getRawValue();
});
it('should commit the updates', () => {
expect(bitstreamService.commitUpdates).toHaveBeenCalled();
it('should NOT set isIIIF to true', () => {
expect(comp.isIIIF).toBeFalse();
});
it('should put the \"IIIF Label\" input not to be shown', () => {
expect(rawForm.iiifLabelContainer).toBeFalsy();
});
});
});
});
describe('when the cancel button is clicked', () => {
it('should call navigateToItemEditBitstreams method', () => {
spyOn(comp, 'navigateToItemEditBitstreams');
comp.onCancel();
expect(comp.navigateToItemEditBitstreams).toHaveBeenCalled();
});
});
describe('when navigateToItemEditBitstreams is called, and the component has an itemId', () => {
it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
comp.itemId = 'some-uuid1';
comp.navigateToItemEditBitstreams();
expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']);
});
});
describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => {
it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
comp.itemId = undefined;
comp.navigateToItemEditBitstreams();
expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']);
});
});
});

View File

@@ -1,16 +1,27 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnDestroy,
OnInit
} from '@angular/core';
import { Bitstream } from '../../core/shared/bitstream.model';
import { ActivatedRoute, Router } from '@angular/router';
import { map, mergeMap, switchMap } from 'rxjs/operators';
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
import {
combineLatest,
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
Subscription
} from 'rxjs';
import {
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicFormLayout,
DynamicFormService,
DynamicInputModel,
DynamicSelectModel,
DynamicTextAreaModel
DynamicSelectModel
} from '@ng-dynamic-forms/core';
import { FormGroup } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
@@ -28,13 +39,19 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { hasValue, isNotEmpty, isEmpty } from '../../shared/empty.util';
import { Metadata } from '../../core/shared/metadata.utils';
import { Location } from '@angular/common';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { getEntityEditRoute, getItemEditRoute } from '../../item-page/item-page-routing-paths';
import { Bundle } from '../../core/shared/bundle.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { Item } from '../../core/shared/item.model';
import {
DsDynamicInputModel
} from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
@Component({
selector: 'ds-edit-bitstream-page',
@@ -94,6 +111,26 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/
NOTIFICATIONS_PREFIX = 'bitstream.edit.notifications.';
/**
* IIIF image width metadata key
*/
IMAGE_WIDTH_METADATA = 'iiif.image.width';
/**
* IIIF image height metadata key
*/
IMAGE_HEIGHT_METADATA = 'iiif.image.height';
/**
* IIIF table of contents metadata key
*/
IIIF_TOC_METADATA = 'iiif.toc';
/**
* IIIF label metadata key
*/
IIIF_LABEL_METADATA = 'iiif.label';
/**
* Options for fetching all bitstream formats
*/
@@ -102,7 +139,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
/**
* The Dynamic Input Model for the file's name
*/
fileNameModel = new DynamicInputModel({
fileNameModel = new DsDynamicInputModel({
hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '',
id: 'fileName',
name: 'fileName',
required: true,
@@ -118,14 +156,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
* The Dynamic Switch Model for the file's name
*/
primaryBitstreamModel = new DynamicCustomSwitchModel({
id: 'primaryBitstream',
name: 'primaryBitstream'
});
id: 'primaryBitstream',
name: 'primaryBitstream'
}
);
/**
* The Dynamic TextArea Model for the file's description
*/
descriptionModel = new DynamicTextAreaModel({
descriptionModel = new DsDynamicTextAreaModel({
hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '',
id: 'description',
name: 'description',
rows: 10
@@ -147,10 +187,87 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
name: 'newFormat'
});
/**
* The Dynamic Input Model for the iiif label
*/
iiifLabelModel = new DsDynamicInputModel({
hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '',
id: 'iiifLabel',
name: 'iiifLabel'
},
{
grid: {
host: 'col col-lg-6 d-inline-block'
}
});
iiifLabelContainer = new DynamicFormGroupModel({
id: 'iiifLabelContainer',
group: [this.iiifLabelModel]
},{
grid: {
host: 'form-row'
}
});
iiifTocModel = new DsDynamicInputModel({
hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '',
id: 'iiifToc',
name: 'iiifToc',
},{
grid: {
host: 'col col-lg-6 d-inline-block'
}
});
iiifTocContainer = new DynamicFormGroupModel({
id: 'iiifTocContainer',
group: [this.iiifTocModel]
},{
grid: {
host: 'form-row'
}
});
iiifWidthModel = new DsDynamicInputModel({
hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '',
id: 'iiifWidth',
name: 'iiifWidth',
},{
grid: {
host: 'col col-lg-6 d-inline-block'
}
});
iiifWidthContainer = new DynamicFormGroupModel({
id: 'iiifWidthContainer',
group: [this.iiifWidthModel]
},{
grid: {
host: 'form-row'
}
});
iiifHeightModel = new DsDynamicInputModel({
hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '',
id: 'iiifHeight',
name: 'iiifHeight'
},{
grid: {
host: 'col col-lg-6 d-inline-block'
}
});
iiifHeightContainer = new DynamicFormGroupModel({
id: 'iiifHeightContainer',
group: [this.iiifHeightModel]
},{
grid: {
host: 'form-row'
}
});
/**
* All input models in a simple array for easier iterations
*/
inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel, this.newFormatModel];
inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel,
this.newFormatModel];
/**
* The dynamic form fields used for editing the information of a bitstream
@@ -163,7 +280,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
this.fileNameModel,
this.primaryBitstreamModel
]
}),
},{
grid: {
host: 'form-row'
}
}),
new DynamicFormGroupModel({
id: 'descriptionContainer',
group: [
@@ -254,18 +375,27 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/
entityType: string;
/**
* Set to true when the parent item supports IIIF.
*/
isIIIF = false;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
constructor(private route: ActivatedRoute,
private router: Router,
private changeDetectorRef: ChangeDetectorRef,
private location: Location,
private formService: DynamicFormService,
private translate: TranslateService,
private bitstreamService: BitstreamDataService,
private dsoNameService: DSONameService,
private notificationsService: NotificationsService,
private bitstreamFormatService: BitstreamFormatDataService) {
}
@@ -277,7 +407,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
* - Translate the form labels and hints
*/
ngOnInit(): void {
this.formGroup = this.formService.createFormGroup(this.formModel);
this.itemId = this.route.snapshot.queryParams.itemId;
this.entityType = this.route.snapshot.queryParams.entityType;
@@ -301,13 +430,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
).subscribe(([bitstream, allFormats]) => {
this.bitstream = bitstream as Bitstream;
this.formats = allFormats.page;
this.updateFormatModel();
this.updateForm(this.bitstream);
this.setIiifStatus(this.bitstream);
})
);
this.updateFieldTranslations();
this.subs.push(
this.translate.onLangChange
.subscribe(() => {
@@ -316,6 +442,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
);
}
/**
* Initializes the form.
*/
setForm() {
this.formGroup = this.formService.createFormGroup(this.formModel);
this.updateFormatModel();
this.updateForm(this.bitstream);
this.updateFieldTranslations();
}
/**
* Update the current form values with bitstream properties
* @param bitstream
@@ -333,6 +469,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined
}
});
if (this.isIIIF) {
this.formGroup.patchValue({
iiifLabelContainer: {
iiifLabel: bitstream.firstMetadataValue(this.IIIF_LABEL_METADATA)
},
iiifTocContainer: {
iiifToc: bitstream.firstMetadataValue(this.IIIF_TOC_METADATA)
},
iiifWidthContainer: {
iiifWidth: bitstream.firstMetadataValue(this.IMAGE_WIDTH_METADATA)
},
iiifHeightContainer: {
iiifHeight: bitstream.firstMetadataValue(this.IMAGE_HEIGHT_METADATA)
}
});
}
this.bitstream.format.pipe(
getAllSucceededRemoteDataPayload()
).subscribe((format: BitstreamFormat) => {
@@ -467,6 +619,32 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
const primary = rawForm.fileNamePrimaryContainer.primaryBitstream;
Metadata.setFirstValue(newMetadata, 'dc.title', rawForm.fileNamePrimaryContainer.fileName);
Metadata.setFirstValue(newMetadata, 'dc.description', rawForm.descriptionContainer.description);
if (this.isIIIF) {
// It's helpful to remove these metadata elements entirely when the form value is empty.
// This avoids potential issues on the REST side and makes it possible to do things like
// remove an existing "table of contents" entry.
if (isEmpty(rawForm.iiifLabelContainer.iiifLabel)) {
delete newMetadata[this.IIIF_LABEL_METADATA];
} else {
Metadata.setFirstValue(newMetadata, this.IIIF_LABEL_METADATA, rawForm.iiifLabelContainer.iiifLabel);
}
if (isEmpty(rawForm.iiifTocContainer.iiifToc)) {
delete newMetadata[this.IIIF_TOC_METADATA];
} else {
Metadata.setFirstValue(newMetadata, this.IIIF_TOC_METADATA, rawForm.iiifTocContainer.iiifToc);
}
if (isEmpty(rawForm.iiifWidthContainer.iiifWidth)) {
delete newMetadata[this.IMAGE_WIDTH_METADATA];
} else {
Metadata.setFirstValue(newMetadata, this.IMAGE_WIDTH_METADATA, rawForm.iiifWidthContainer.iiifWidth);
}
if (isEmpty(rawForm.iiifHeightContainer.iiifHeight)) {
delete newMetadata[this.IMAGE_HEIGHT_METADATA];
} else {
Metadata.setFirstValue(newMetadata, this.IMAGE_HEIGHT_METADATA, rawForm.iiifHeightContainer.iiifHeight);
}
}
if (isNotEmpty(rawForm.formatContainer.newFormat)) {
Metadata.setFirstValue(newMetadata, 'dc.format', rawForm.formatContainer.newFormat);
}
@@ -497,6 +675,58 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
}
}
/**
* Verifies that the parent item is iiif-enabled. Checks bitstream mimetype to be
* sure it's an image, excluding bitstreams in the THUMBNAIL or OTHERCONTENT bundles.
* @param bitstream
*/
setIiifStatus(bitstream: Bitstream) {
const regexExcludeBundles = /OTHERCONTENT|THUMBNAIL|LICENSE/;
const regexIIIFItem = /true|yes/i;
const isImage$ = this.bitstream.format.pipe(
getFirstSucceededRemoteData(),
map((format: RemoteData<BitstreamFormat>) => format.payload.mimetype.includes('image/')));
const isIIIFBundle$ = this.bitstream.bundle.pipe(
getFirstSucceededRemoteData(),
map((bundle: RemoteData<Bundle>) =>
this.dsoNameService.getName(bundle.payload).match(regexExcludeBundles) == null));
const isEnabled$ = this.bitstream.bundle.pipe(
getFirstSucceededRemoteData(),
map((bundle: RemoteData<Bundle>) => bundle.payload.item.pipe(
getFirstSucceededRemoteData(),
map((item: RemoteData<Item>) =>
(item.payload.firstMetadataValue('dspace.iiif.enabled') &&
item.payload.firstMetadataValue('dspace.iiif.enabled').match(regexIIIFItem) !== null)
))));
const iiifSub = combineLatest(
isImage$,
isIIIFBundle$,
isEnabled$
).subscribe(([isImage, isIIIFBundle, isEnabled]) => {
if (isImage && isIIIFBundle && isEnabled) {
this.isIIIF = true;
this.inputModels.push(this.iiifLabelModel);
this.formModel.push(this.iiifLabelContainer);
this.inputModels.push(this.iiifTocModel);
this.formModel.push(this.iiifTocContainer);
this.inputModels.push(this.iiifWidthModel);
this.formModel.push(this.iiifWidthContainer);
this.inputModels.push(this.iiifHeightModel);
this.formModel.push(this.iiifHeightContainer);
}
this.setForm();
this.changeDetectorRef.detectChanges();
});
this.subs.push(iiifSub);
}
/**
* Unsubscribe from open subscriptions
*/

View File

@@ -18,7 +18,7 @@
</ds-comcol-page-content>
</header>
<!-- Browse-By Links -->
<ds-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-comcol-page-browse-by>
<ds-themed-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-themed-comcol-page-browse-by>
</ng-container></ng-container>
<section class="comcol-page-browse-section">

View File

@@ -40,10 +40,10 @@
</div>
<section class="comcol-page-browse-section">
<!-- Browse-By Links -->
<ds-comcol-page-browse-by
<ds-themed-comcol-page-browse-by
[id]="collection.id"
[contentType]="collection.type">
</ds-comcol-page-browse-by>
</ds-themed-comcol-page-browse-by>
<ng-container *ngVar="(itemRD$ | async) as itemRD">
<div class="mt-4" *ngIf="itemRD?.hasSucceeded" @fadeIn>

View File

@@ -1,4 +1,4 @@
<div class="container">
<h2>{{ 'communityList.title' | translate }}</h2>
<ds-community-list></ds-community-list>
<ds-themed-community-list></ds-themed-community-list>
</div>

View File

@@ -5,12 +5,14 @@ import { CommunityListPageComponent } from './community-list-page.component';
import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
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';
const DECLARATIONS = [
CommunityListPageComponent,
CommunityListComponent,
ThemedCommunityListPageComponent
ThemedCommunityListPageComponent,
ThemedCommunityListComponent
];
/**
* The page which houses a title and the community list, as described in community-list.component

View File

@@ -0,0 +1,23 @@
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { CommunityListComponent } from './community-list.component';
import { Component } from '@angular/core';
@Component({
selector: 'ds-themed-community-list',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})export class ThemedCommunityListComponent extends ThemedComponent<CommunityListComponent> {
protected getComponentName(): string {
return 'CommunityListComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/community-list-page/community-list/community-list.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./community-list.component`);
}
}

View File

@@ -26,8 +26,8 @@
</div>
<section class="comcol-page-browse-section">
<!-- Browse-By Links -->
<ds-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
</ds-comcol-page-browse-by>
<ds-themed-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
</ds-themed-comcol-page-browse-by>
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list>
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>

View File

@@ -3,5 +3,6 @@ export enum AuthMethodType {
Shibboleth = 'shibboleth',
Ldap = 'ldap',
Ip = 'ip',
X509 = 'x509'
X509 = 'x509',
Oidc = 'oidc'
}

View File

@@ -29,6 +29,11 @@ export class AuthMethod {
this.authMethodType = AuthMethodType.Password;
break;
}
case 'oidc': {
this.authMethodType = AuthMethodType.Oidc;
this.location = location;
break;
}
default: {
break;

View File

@@ -0,0 +1,45 @@
/**
* Model class for an Item Access Condition
*/
export class AccessesConditionOption {
/**
* The name for this Access Condition
*/
name: string;
/**
* The groupName for this Access Condition
*/
groupName: string;
/**
* A boolean representing if this Access Condition has a start date
*/
hasStartDate: boolean;
/**
* A boolean representing if this Access Condition has an end date
*/
hasEndDate: boolean;
/**
* Maximum value of the start date
*/
endDateLimit?: string;
/**
* Maximum value of the end date
*/
startDateLimit?: string;
/**
* Maximum value of the start date
*/
maxStartDate?: string;
/**
* Maximum value of the end date
*/
maxEndDate?: string;
}

View File

@@ -0,0 +1,42 @@
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { ConfigObject } from './config.model';
import { AccessesConditionOption } from './config-accesses-conditions-options.model';
import { SUBMISSION_ACCESSES_TYPE } from './config-type';
import { HALLink } from '../../shared/hal-link.model';
/**
* Class for the configuration describing the item accesses condition
*/
@typedObject
@inheritSerialization(ConfigObject)
export class SubmissionAccessModel extends ConfigObject {
static type = SUBMISSION_ACCESSES_TYPE;
/**
* A list of available item access conditions
*/
@autoserialize
accessConditionOptions: AccessesConditionOption[];
/**
* Boolean that indicates whether the current item must be findable via search or browse.
*/
@autoserialize
discoverable: boolean;
/**
* Boolean that indicates whether or not the user can change the discoverable flag.
*/
@autoserialize
canChangeDiscoverable: boolean;
/**
* The links to all related resources returned by the rest api.
*/
@deserialize
_links: {
self: HALLink
};
}

View File

@@ -0,0 +1,10 @@
import { inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { SUBMISSION_ACCESSES_TYPE } from './config-type';
import { SubmissionAccessModel } from './config-submission-access.model';
@typedObject
@inheritSerialization(SubmissionAccessModel)
export class SubmissionAccessesModel extends SubmissionAccessModel {
static type = SUBMISSION_ACCESSES_TYPE;
}

View File

@@ -15,3 +15,5 @@ export const SUBMISSION_SECTION_TYPE = new ResourceType('submissionsection');
export const SUBMISSION_UPLOADS_TYPE = new ResourceType('submissionuploads');
export const SUBMISSION_UPLOAD_TYPE = new ResourceType('submissionupload');
export const SUBMISSION_ACCESSES_TYPE = new ResourceType('submissionaccessoption');

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@angular/core';
import { ConfigService } from './config.service';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { dataService } from '../cache/builders/build-decorators';
import { SUBMISSION_ACCESSES_TYPE } from './models/config-type';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { ConfigObject } from './models/config.model';
import { SubmissionAccessesModel } from './models/config-submission-accesses.model';
import { RemoteData } from '../data/remote-data';
import { Observable } from 'rxjs';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
/**
* Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process.
*/
@Injectable()
@dataService(SUBMISSION_ACCESSES_TYPE)
export class SubmissionAccessesConfigService extends ConfigService {
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<SubmissionAccessesModel>
) {
super(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionaccessoptions');
}
findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable<RemoteData<SubmissionAccessesModel>> {
return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig<ConfigObject>[]) as Observable<RemoteData<SubmissionAccessesModel>>;
}
}

View File

@@ -2,11 +2,7 @@ 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 { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { EffectsModule } from '@ngrx/effects';
import { Action, StoreConfig, StoreModule } from '@ngrx/store';
@@ -77,6 +73,7 @@ import { MetadataSchema } from './metadata/metadata-schema.model';
import { MetadataService } from './metadata/metadata.service';
import { RegistryService } from './registry/registry.service';
import { RoleService } from './roles/role.service';
import { FeedbackDataService } from './feedback/feedback-data.service';
import { ApiService } from './services/api.service';
import { ServerResponseService } from './services/server-response.service';
@@ -164,6 +161,7 @@ import { Root } from './data/root.model';
import { SearchConfig } from './shared/search/search-filters/search-config.model';
import { SequenceService } from './shared/sequence.service';
import { GroupDataService } from './eperson/group-data.service';
import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -286,7 +284,8 @@ const PROVIDERS = [
VocabularyService,
VocabularyTreeviewService,
SequenceService,
GroupDataService
GroupDataService,
FeedbackDataService,
];
/**
@@ -345,7 +344,8 @@ export const models =
Registration,
UsageReport,
Root,
SearchConfig
SearchConfig,
SubmissionAccessesModel
];
@NgModule({

View File

@@ -3,7 +3,7 @@ import { compare } from 'fast-json-patch';
import { Operation } from 'fast-json-patch';
import { getClassForType } from '../cache/builders/build-decorators';
import { TypedObject } from '../cache/object-cache.reducer';
import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
import { DSpaceNotNullSerializer } from '../dspace-rest/dspace-not-null.serializer';
import { ChangeAnalyzer } from './change-analyzer';
/**
@@ -22,8 +22,8 @@ export class DefaultChangeAnalyzer<T extends TypedObject> implements ChangeAnaly
* The second object to compare
*/
diff(object1: T, object2: T): Operation[] {
const serializer1 = new DSpaceSerializer(getClassForType(object1.type));
const serializer2 = new DSpaceSerializer(getClassForType(object2.type));
const serializer1 = new DSpaceNotNullSerializer(getClassForType(object1.type));
const serializer2 = new DSpaceNotNullSerializer(getClassForType(object2.type));
return compare(serializer1.serialize(object1), serializer2.serialize(object2));
}
}

View File

@@ -27,4 +27,5 @@ export enum FeatureID {
CanDeleteVersion = 'canDeleteVersion',
CanCreateVersion = 'canCreateVersion',
CanViewUsageStatistics = 'canViewUsageStatistics',
CanSendFeedback = 'canSendFeedback',
}

View File

@@ -0,0 +1,76 @@
import { Deserialize, Serialize } from 'cerialize';
import { Serializer } from '../serializer';
import { GenericConstructor } from '../shared/generic-constructor';
/**
* This Serializer turns responses from DSpace's REST API
* to models and vice versa, but with all fields with null value removed for the Serialized objects
*/
export class DSpaceNotNullSerializer<T> implements Serializer<T> {
/**
* Create a new DSpaceNotNullSerializer instance
*
* @param modelType a class or interface to indicate
* the kind of model this serializer should work with
*/
constructor(private modelType: GenericConstructor<T>) {
}
/**
* Convert a model in to the format expected by the backend, but with all fields with null value removed
*
* @param model The model to serialize
* @returns An object to send to the backend
*/
serialize(model: T): any {
return getSerializedObjectWithoutNullFields(Serialize(model, this.modelType));
}
/**
* Convert an array of models in to the format expected by the backend, but with all fields with null value removed
*
* @param models The array of models to serialize
* @returns An object to send to the backend
*/
serializeArray(models: T[]): any {
return getSerializedObjectWithoutNullFields(Serialize(models, this.modelType));
}
/**
* Convert a response from the backend in to a model.
*
* @param response An object returned by the backend
* @returns a model of type T
*/
deserialize(response: any): T {
if (Array.isArray(response)) {
throw new Error('Expected a single model, use deserializeArray() instead');
}
return Deserialize(response, this.modelType) as T;
}
/**
* Convert a response from the backend in to an array of models
*
* @param response An object returned by the backend
* @returns an array of models of type T
*/
deserializeArray(response: any): T[] {
if (!Array.isArray(response)) {
throw new Error('Expected an Array, use deserialize() instead');
}
return Deserialize(response, this.modelType) as T[];
}
}
function getSerializedObjectWithoutNullFields(serializedObjectBefore): any {
const copySerializedObject = {};
for (const [key, value] of Object.entries(serializedObjectBefore)) {
if (value !== null) {
copySerializedObject[key] = value;
}
}
return copySerializedObject;
}

View File

@@ -0,0 +1,88 @@
import { FeedbackDataService } from './feedback-data.service';
import { HALLink } from '../shared/hal-link.model';
import { Item } from '../shared/item.model';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { Feedback } from './models/feedback.model';
describe('FeedbackDataService', () => {
let service: FeedbackDataService;
let requestService;
let halService;
let rdbService;
let notificationsService;
let http;
let comparator;
let objectCache;
let store;
let item;
let bundleLink;
let bundleHALLink;
const feedbackPayload = Object.assign(new Feedback(), {
email: 'test@email.com',
message: 'message',
page: '/home'
});
function initTestService(): FeedbackDataService {
bundleLink = '/items/0fdc0cd7-ff8c-433d-b33c-9b56108abc07/bundles';
bundleHALLink = new HALLink();
bundleHALLink.href = bundleLink;
item = new Item();
item._links = {
bundles: bundleHALLink
};
requestService = getMockRequestService();
halService = new HALEndpointServiceStub('url') as any;
rdbService = {} as RemoteDataBuildService;
notificationsService = {} as NotificationsService;
http = {} as HttpClient;
comparator = new DSOChangeAnalyzer() as any;
objectCache = {
addPatch: () => {
/* empty */
},
getObjectBySelfLink: () => {
/* empty */
}
} as any;
store = {} as Store<CoreState>;
return new FeedbackDataService(
requestService,
rdbService,
store,
objectCache,
halService,
notificationsService,
http,
comparator,
);
}
beforeEach(() => {
service = initTestService();
});
describe('getFeedback', () => {
beforeEach(() => {
spyOn(service, 'getFeedback');
service.getFeedback('3');
});
it('should call getFeedback with the feedback link', () => {
expect(service.getFeedback).toHaveBeenCalledWith('3');
});
});
});

View File

@@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { DataService } from '../data/data.service';
import { Feedback } from './models/feedback.model';
import { FEEDBACK } from './models/feedback.resource-type';
import { dataService } from '../cache/builders/build-decorators';
import { RequestService } from '../data/request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
/**
* Service for checking and managing the feedback
*/
@Injectable()
@dataService(FEEDBACK)
export class FeedbackDataService extends DataService<Feedback> {
protected linkPath = 'feedbacks';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<any>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DSOChangeAnalyzer<Feedback>,
) {
super();
}
/**
* Get feedback from its id
* @param uuid string the id of the feedback
*/
getFeedback(uuid: string): Observable<Feedback> {
return this.findById(uuid).pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
);
}
}

View File

@@ -0,0 +1,20 @@
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
import { FeatureID } from '../data/feature-authorization/feature-id';
import { Injectable } from '@angular/core';
/**
* An guard for redirecting users to the feedback page if user is authorized
*/
@Injectable()
export class FeedbackGuard implements CanActivate {
constructor(private authorizationService: AuthorizationDataService) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
return this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
}
}

View File

@@ -0,0 +1,34 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { DSpaceObject } from '../../shared/dspace-object.model';
import { HALLink } from '../../shared/hal-link.model';
import { FEEDBACK } from './feedback.resource-type';
@typedObject
@inheritSerialization(DSpaceObject)
export class Feedback extends DSpaceObject {
static type = FEEDBACK;
/**
* The email address
*/
@autoserialize
public email: string;
/**
* A string representing message the user inserted
*/
@autoserialize
public message: string;
/**
* A string representing the page from which the user came from
*/
@autoserialize
public page: string;
_links: {
self: HALLink;
};
}

View File

@@ -0,0 +1,9 @@
import { ResourceType } from '../../shared/resource-type';
/**
* The resource type for Feedback
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const FEEDBACK = new ResourceType('feedback');

View File

@@ -0,0 +1,25 @@
/**
* An interface to represent an access condition.
*/
export class AccessConditionObject {
/**
* The access condition id
*/
id: string;
/**
* The access condition name
*/
name: string;
/**
* Possible start date of the access condition
*/
startDate: string;
/**
* Possible end date of the access condition
*/
endDate: string;
}

View File

@@ -0,0 +1,9 @@
import { ResourceType } from '../../shared/resource-type';
/**
* The resource type for Accesses section
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const SUBMISSION_ACCESSES = new ResourceType('submissionaccesses');

View File

@@ -0,0 +1,8 @@
import { AccessConditionObject } from './access-condition.model';
/**
* An interface to represent item's access condition.
*/
export class SubmissionItemAccessConditionObject extends AccessConditionObject {
}

View File

@@ -1,25 +1,8 @@
import { AccessConditionObject } from './access-condition.model';
/**
* An interface to represent bitstream's access condition.
*/
export class SubmissionUploadFileAccessConditionObject {
export class SubmissionUploadFileAccessConditionObject extends AccessConditionObject {
/**
* The access condition id
*/
id: string;
/**
* The access condition name
*/
name: string;
/**
* Possible start date of the access condition
*/
startDate: string;
/**
* Possible end date of the access condition
*/
endDate: string;
}

View File

@@ -0,0 +1,21 @@
import { SubmissionItemAccessConditionObject } from './submission-item-access-condition.model';
/**
* An interface to represent the submission's item accesses condition.
*/
export interface WorkspaceitemSectionAccessesObject {
/**
* The access condition id
*/
id: string;
/**
* Boolean that indicates whether the current item must be findable via search or browse.
*/
discoverable: boolean;
/**
* A list of available item access conditions
*/
accessConditions: SubmissionItemAccessConditionObject[];
}

View File

@@ -1,3 +1,4 @@
import { WorkspaceitemSectionAccessesObject } from './workspaceitem-section-accesses.model';
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
@@ -19,4 +20,5 @@ export type WorkspaceitemSectionDataType
| WorkspaceitemSectionFormObject
| WorkspaceitemSectionLicenseObject
| WorkspaceitemSectionCcLicenseObject
| WorkspaceitemSectionAccessesObject
| string;

View File

@@ -75,6 +75,10 @@
<a class="text-white"
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
</li>
<li>
<a class="text-white"
routerLink="info/feedback">{{ 'footer.link.feedback' | translate}}</a>
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,45 @@
<div class="row row-offcanvas row-offcanvas-right">
<div class="col-xs-12 col-sm-12 col-md-9 main-content">
<form class="primary" [formGroup]="feedbackForm" (ngSubmit)="createFeedback()">
<h2>{{ 'info.feedback.head' | translate }}</h2>
<p>{{ 'info.feedback.info' | translate }}</p>
<fieldset class="col p-0">
<div class="row">
<div class="control-group col-sm-12">
<label class="control-label" for="email">{{ 'info.feedback.email-label' | translate }}&nbsp;</label>
<input id="email" class="form-control" name="email" type="text" value="" formControlName="email" autofocus="autofocus" title="{{ 'info.feedback.email_help' | translate }}">
<small class="text-muted">{{ 'info.feedback.email_help' | translate }}</small>
</div>
</div>
<ng-container *ngIf="feedbackForm.controls.email.invalid && (feedbackForm.controls.email.dirty || feedbackForm.controls.email.touched)"
class="alert">
<ds-error *ngIf="feedbackForm.controls.email.errors?.required" message="{{'info.feedback.error.email.required' | translate}}"></ds-error>
<ds-error *ngIf="feedbackForm.controls.email.errors?.pattern" message="{{'info.feedback.error.email.required' | translate}}"></ds-error>
</ng-container>
<div class="row">
<div class="control-group col-sm-12">
<label class="control-label" for="comments">{{ 'info.feedback.comments' | translate }}:&nbsp;</label>
<textarea id="comments" formControlName="message" class="form-control" name="message" cols="20" rows="5"> </textarea>
</div>
</div>
<ng-container *ngIf="feedbackForm.controls.message.invalid && (feedbackForm.controls.message.dirty || feedbackForm.controls.message.touched)"
class="alert">
<ds-error *ngIf="feedbackForm.controls.message.errors?.required" message="{{'info.feedback.error.message.required' | translate}}"></ds-error>
</ng-container>
<div class="row">
<div class="control-group col-sm-12">
<label class="control-label" for="page">{{ 'info.feedback.page-label' | translate }}&nbsp;</label>
<input id="page" readonly class="form-control" name="page" type="text" value="" formControlName="page" autofocus="autofocus" title="{{ 'info.feedback.page_help' | translate }}">
<small class="text-muted">{{ 'info.feedback.page_help' | translate }}</small>
</div>
</div>
<div class="row py-2">
<div class="control-group col-sm-12 text-right">
<button [disabled]="!feedbackForm.valid" class="btn btn-primary" name="submit" type="submit">{{ 'info.feedback.send' | translate }}</button>
</div>
</div>
</fieldset>
</form>
</div>
</div>

View File

@@ -0,0 +1,3 @@
ds-error{
color:red;
}

View File

@@ -0,0 +1,97 @@
import { EPersonMock } from '../../../shared/testing/eperson.mock';
import { FeedbackDataService } from '../../../core/feedback/feedback-data.service';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FeedbackFormComponent } from './feedback-form.component';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { RouteService } from '../../../core/services/route.service';
import { routeServiceStub } from '../../../shared/testing/route-service.stub';
import { FormBuilder } from '@angular/forms';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { AuthService } from '../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../shared/testing/auth-service.stub';
import { of } from 'rxjs';
import { Feedback } from '../../../core/feedback/models/feedback.model';
import { Router } from '@angular/router';
import { RouterMock } from '../../../shared/mocks/router.mock';
import { NativeWindowService } from '../../../core/services/window.service';
import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref';
describe('FeedbackFormComponent', () => {
let component: FeedbackFormComponent;
let fixture: ComponentFixture<FeedbackFormComponent>;
let de: DebugElement;
const notificationService = new NotificationsServiceStub();
const feedbackDataServiceStub = jasmine.createSpyObj('feedbackDataService', {
create: of(new Feedback())
});
const authService: AuthServiceStub = Object.assign(new AuthServiceStub(), {
getAuthenticatedUserFromStore: () => {
return of(EPersonMock);
}
});
const routerStub = new RouterMock();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [FeedbackFormComponent],
providers: [
{ provide: RouteService, useValue: routeServiceStub },
{ provide: FormBuilder, useValue: new FormBuilder() },
{ provide: NotificationsService, useValue: notificationService },
{ provide: FeedbackDataService, useValue: feedbackDataServiceStub },
{ provide: AuthService, useValue: authService },
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
{ provide: Router, useValue: routerStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FeedbackFormComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have page value', () => {
expect(component.feedbackForm.controls.page.value).toEqual('http://localhost/home');
});
it('should have email if ePerson', () => {
expect(component.feedbackForm.controls.email.value).toEqual('test@test.com');
});
it('should have disabled button', () => {
expect(de.query(By.css('button')).nativeElement.disabled).toBeTrue();
});
describe('when message is inserted', () => {
beforeEach(() => {
component.feedbackForm.patchValue({ message: 'new feedback' });
fixture.detectChanges();
});
it('should not have disabled button', () => {
expect(de.query(By.css('button')).nativeElement.disabled).toBeFalse();
});
it('on submit should call createFeedback of feedbackDataServiceStub service', () => {
component.createFeedback();
fixture.detectChanges();
expect(feedbackDataServiceStub.create).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,83 @@
import { RemoteData } from '../../../core/data/remote-data';
import { NoContent } from '../../../core/shared/NoContent.model';
import { FeedbackDataService } from '../../../core/feedback/feedback-data.service';
import { Component, Inject, OnInit } from '@angular/core';
import { RouteService } from '../../../core/services/route.service';
import { FormBuilder, Validators } from '@angular/forms';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '../../../core/auth/auth.service';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { Router } from '@angular/router';
import { getHomePageRoute } from '../../../app-routing-paths';
import { take } from 'rxjs/operators';
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
@Component({
selector: 'ds-feedback-form',
templateUrl: './feedback-form.component.html',
styleUrls: ['./feedback-form.component.scss']
})
/**
* Component displaying the contents of the Feedback Statement
*/
export class FeedbackFormComponent implements OnInit {
/**
* Form builder created used from the feedback from
*/
feedbackForm = this.fb.group({
email: ['', [Validators.required, Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$')]],
message: ['', Validators.required],
page: [''],
});
constructor(
@Inject(NativeWindowService) protected _window: NativeWindowRef,
public routeService: RouteService,
private fb: FormBuilder,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
private feedbackDataService: FeedbackDataService,
private authService: AuthService,
private router: Router) {
}
/**
* On init check if user is logged in and use its email if so
*/
ngOnInit() {
this.authService.getAuthenticatedUserFromStore().pipe(take(1)).subscribe((user: EPerson) => {
if (!!user) {
this.feedbackForm.patchValue({ email: user.email });
}
});
this.routeService.getPreviousUrl().pipe(take(1)).subscribe((url: string) => {
if (!url) {
url = getHomePageRoute();
}
const relatedUrl = new URLCombiner(this._window.nativeWindow.origin, url).toString();
this.feedbackForm.patchValue({ page: relatedUrl });
});
}
/**
* Function to create the feedback from form values
*/
createFeedback(): void {
const url = this.feedbackForm.value.page.replace(this._window.nativeWindow.origin, '');
this.feedbackDataService.create(this.feedbackForm.value).pipe(getFirstCompletedRemoteData()).subscribe((response: RemoteData<NoContent>) => {
if (response.isSuccess) {
this.notificationsService.success(this.translate.instant('info.feedback.create.success'));
this.feedbackForm.reset();
this.router.navigateByUrl(url);
}
});
}
}

View File

@@ -0,0 +1,3 @@
<div class="container">
<ds-feedback-form></ds-feedback-form>
</div>

View File

@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FeedbackComponent } from './feedback.component';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('FeedbackComponent', () => {
let component: FeedbackComponent;
let fixture: ComponentFixture<FeedbackComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [FeedbackComponent],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FeedbackComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'ds-feedback',
templateUrl: './feedback.component.html',
styleUrls: ['./feedback.component.scss']
})
/**
* Component displaying the Feedback Statement
*/
export class FeedbackComponent {
}

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { FeedbackComponent } from './feedback.component';
/**
* Themed wrapper for FeedbackComponent
*/
@Component({
selector: 'ds-themed-feedback',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedFeedbackComponent extends ThemedComponent<FeedbackComponent> {
protected getComponentName(): string {
return 'FeedbackComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/info/feedback/feedback.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./feedback.component`);
}
}

View File

@@ -2,6 +2,7 @@ import { getInfoModulePath } from '../app-routing-paths';
export const END_USER_AGREEMENT_PATH = 'end-user-agreement';
export const PRIVACY_PATH = 'privacy';
export const FEEDBACK_PATH = 'feedback';
export function getEndUserAgreementPath() {
return getSubPath(END_USER_AGREEMENT_PATH);
@@ -11,6 +12,10 @@ export function getPrivacyPath() {
return getSubPath(PRIVACY_PATH);
}
export function getFeedbackPath() {
return getSubPath(FEEDBACK_PATH);
}
function getSubPath(path: string) {
return `${getInfoModulePath()}/${path}`;
}

View File

@@ -1,9 +1,12 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { PRIVACY_PATH, END_USER_AGREEMENT_PATH } from './info-routing-paths';
import { PRIVACY_PATH, END_USER_AGREEMENT_PATH, FEEDBACK_PATH } from './info-routing-paths';
import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component';
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
import { FeedbackGuard } from '../core/feedback/feedback.guard';
@NgModule({
imports: [
@@ -22,6 +25,15 @@ import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' }
}
]),
RouterModule.forChild([
{
path: FEEDBACK_PATH,
component: ThemedFeedbackComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'info.feedback.title', breadcrumbKey: 'info.feedback' },
canActivate: [FeedbackGuard]
}
])
]
})

View File

@@ -8,6 +8,11 @@ import { PrivacyComponent } from './privacy/privacy.component';
import { PrivacyContentComponent } from './privacy/privacy-content/privacy-content.component';
import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component';
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
import { FeedbackComponent } from './feedback/feedback.component';
import { FeedbackFormComponent } from './feedback/feedback-form/feedback-form.component';
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
import { FeedbackGuard } from '../core/feedback/feedback.guard';
const DECLARATIONS = [
EndUserAgreementComponent,
@@ -15,21 +20,25 @@ const DECLARATIONS = [
EndUserAgreementContentComponent,
PrivacyComponent,
PrivacyContentComponent,
ThemedPrivacyComponent
ThemedPrivacyComponent,
FeedbackComponent,
FeedbackFormComponent,
ThemedFeedbackComponent
];
@NgModule({
imports: [
CommonModule,
SharedModule,
InfoRoutingModule
InfoRoutingModule,
],
declarations: [
...DECLARATIONS
],
exports: [
...DECLARATIONS
]
],
providers: [FeedbackGuard]
})
export class InfoModule {
}

View File

@@ -3,7 +3,7 @@ import { ItemType } from '../../../core/shared/item-relationships/item-type.mode
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { Item } from '../../../core/shared/item.model';
import { RouterStub } from '../../../shared/testing/router.stub';
import { of as observableOf } from 'rxjs';
import { of as observableOf, EMPTY } from 'rxjs';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -24,6 +24,8 @@ import { RelationshipType } from '../../../core/shared/item-relationships/relati
import { EntityTypeService } from '../../../core/data/entity-type.service';
import { getItemEditRoute } from '../../item-page-routing-paths';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { RelationshipTypeService } from '../../../core/data/relationship-type.service';
import { LinkService } from '../../../core/cache/builders/link.service';
let comp: ItemDeleteComponent;
let fixture: ComponentFixture<ItemDeleteComponent>;
@@ -40,6 +42,7 @@ let mockItemDataService: ItemDataService;
let routeStub;
let objectUpdatesServiceStub;
let relationshipService;
let linkService;
let entityTypeService;
let notificationsServiceStub;
let typesSelection;
@@ -52,7 +55,12 @@ describe('ItemDeleteComponent', () => {
uuid: 'fake-uuid',
handle: 'fake/handle',
lastModified: '2018',
isWithdrawn: true
isWithdrawn: true,
metadata: {
'dspace.entity.type': [
{ value: 'Person' }
]
}
});
itemType = Object.assign(new ItemType(), {
@@ -129,6 +137,12 @@ describe('ItemDeleteComponent', () => {
}
);
linkService = jasmine.createSpyObj('linkService',
{
resolveLinks: relationships[0],
}
);
notificationsServiceStub = new NotificationsServiceStub();
TestBed.configureTestingModule({
@@ -142,6 +156,8 @@ describe('ItemDeleteComponent', () => {
{ provide: ObjectUpdatesService, useValue: objectUpdatesServiceStub },
{ provide: RelationshipService, useValue: relationshipService },
{ provide: EntityTypeService, useValue: entityTypeService },
{ provide: RelationshipTypeService, useValue: {} },
{ provide: LinkService, useValue: linkService },
], schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
@@ -166,25 +182,45 @@ describe('ItemDeleteComponent', () => {
});
describe('performAction', () => {
it('should call delete function from the ItemDataService', () => {
spyOn(comp, 'notify');
comp.performAction();
expect(mockItemDataService.delete)
.toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id));
expect(comp.notify).toHaveBeenCalled();
describe(`when there are entitytypes`, () => {
it('should call delete function from the ItemDataService', () => {
spyOn(comp, 'notify');
comp.performAction();
expect(mockItemDataService.delete)
.toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id));
expect(comp.notify).toHaveBeenCalled();
});
it('should call delete function from the ItemDataService with empty types', () => {
spyOn(comp, 'notify');
jasmine.getEnv().allowRespy(true);
spyOn(entityTypeService, 'getEntityTypeRelationships').and.returnValue([]);
comp.ngOnInit();
comp.performAction();
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id, []);
expect(comp.notify).toHaveBeenCalled();
});
});
it('should call delete function from the ItemDataService with empty types', () => {
describe(`when there are no entity types`, () => {
beforeEach(() => {
(comp as any).entityTypeService = jasmine.createSpyObj('entityTypeService',
{
getEntityTypeByLabel: EMPTY,
}
);
});
spyOn(comp, 'notify');
jasmine.getEnv().allowRespy(true);
spyOn(entityTypeService, 'getEntityTypeRelationships').and.returnValue([]);
comp.ngOnInit();
comp.performAction();
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id, []);
expect(comp.notify).toHaveBeenCalled();
it('should call delete function from the ItemDataService', () => {
spyOn(comp, 'notify');
comp.performAction();
expect(mockItemDataService.delete)
.toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id));
expect(comp.notify).toHaveBeenCalled();
});
});
});
describe('notify', () => {

View File

@@ -1,12 +1,14 @@
import { Component, Input, OnInit } from '@angular/core';
import {defaultIfEmpty, filter, map, switchMap, take} from 'rxjs/operators';
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { defaultIfEmpty, filter, map, switchMap, take } from 'rxjs/operators';
import {
AbstractSimpleItemActionComponent
} from '../simple-item-action/abstract-simple-item-action.component';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import {
combineLatest as observableCombineLatest,
combineLatest,
Observable,
of as observableOf
of as observableOf, Subscription
} from 'rxjs';
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
import { VirtualMetadata } from '../virtual-metadata/virtual-metadata.component';
@@ -32,6 +34,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
import { getItemEditRoute } from '../../item-page-routing-paths';
import { RemoteData } from '../../../core/data/remote-data';
import { NoContent } from '../../../core/shared/NoContent.model';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
@Component({
selector: 'ds-item-delete',
@@ -42,7 +45,7 @@ import { NoContent } from '../../../core/shared/NoContent.model';
*/
export class ItemDeleteComponent
extends AbstractSimpleItemActionComponent
implements OnInit {
implements OnInit, OnDestroy {
/**
* The current url of this page
@@ -60,7 +63,7 @@ export class ItemDeleteComponent
* A list of the relationship types for which this item has relations as an observable.
* The list doesn't contain duplicates.
*/
types$: Observable<RelationshipType[]>;
types$: BehaviorSubject<RelationshipType[]> = new BehaviorSubject([]);
/**
* A map which stores the relationships of this item for each type as observable lists
@@ -84,6 +87,11 @@ export class ItemDeleteComponent
*/
public modalRef: NgbModalRef;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
*/
private subs: Subscription[] = [];
constructor(protected route: ActivatedRoute,
protected router: Router,
protected notificationsService: NotificationsService,
@@ -113,8 +121,8 @@ export class ItemDeleteComponent
this.url = this.router.url;
const label = this.item.firstMetadataValue('dspace.entity.type');
if (label !== undefined) {
this.types$ = this.entityTypeService.getEntityTypeByLabel(label).pipe(
if (isNotEmpty(label)) {
this.subs.push(this.entityTypeService.getEntityTypeByLabel(label).pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)),
@@ -138,16 +146,14 @@ export class ItemDeleteComponent
),
);
})
);
} else {
this.types$ = observableOf([]);
).subscribe((types: RelationshipType[]) => this.types$.next(types)));
}
this.types$.pipe(
this.subs.push(this.types$.pipe(
take(1),
).subscribe((types) =>
this.objectUpdatesService.initialize(this.url, types, this.item.lastModified)
);
));
}
/**
@@ -327,7 +333,7 @@ export class ItemDeleteComponent
*/
performAction() {
this.types$.pipe(
this.subs.push(this.types$.pipe(
switchMap((types) =>
combineLatest(
types.map((type) => this.isSelected(type))
@@ -339,13 +345,14 @@ export class ItemDeleteComponent
map((selectedTypes) => selectedTypes.map((type) => type.id)),
)
),
).subscribe((types) => {
this.itemDataService.delete(this.item.id, types).pipe(getFirstCompletedRemoteData()).subscribe(
(rd: RemoteData<NoContent>) => {
this.notify(rd.hasSucceeded);
}
);
});
switchMap((types) =>
this.itemDataService.delete(this.item.id, types).pipe(getFirstCompletedRemoteData())
)
).subscribe(
(rd: RemoteData<NoContent>) => {
this.notify(rd.hasSucceeded);
}
));
}
/**
@@ -361,4 +368,14 @@ export class ItemDeleteComponent
this.router.navigate([getItemEditRoute(this.item)]);
}
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -2,9 +2,9 @@
<ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"></ds-my-dspace-new-submission>
</div>
<ds-search *ngIf="configuration && context"
<ds-themed-search *ngIf="configuration && context"
[configuration]="configuration"
[configurationList]="(configurationList$ | async)"
[context]="context"
[viewModeList]="viewModeList"
></ds-search>
></ds-themed-search>

View File

@@ -6,7 +6,7 @@
<div id="collapsingNav">
<ul class="navbar-nav mr-auto shadow-none">
<ng-container *ngFor="let section of (sections | async)">
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
</ng-container>
</ul>
</div>

View File

@@ -1,5 +1,5 @@
<div class="outer-wrapper" *ngIf="!shouldShowFullscreenLoader; else fullScreenLoader">
<ds-admin-sidebar></ds-admin-sidebar>
<ds-themed-admin-sidebar></ds-themed-admin-sidebar>
<div class="inner-wrapper" [@slideSidebarPadding]="{
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}

View File

@@ -27,7 +27,6 @@ import { Router } from '@angular/router';
})
export class ConfigurationSearchPageComponent extends SearchComponent {
constructor(protected service: SearchService,
protected sidebarService: SidebarService,
protected windowService: HostWindowService,
@@ -36,5 +35,4 @@ export class ConfigurationSearchPageComponent extends SearchComponent {
protected router: Router) {
super(service, sidebarService, windowService, searchConfigService, routeService, router);
}
}

View File

@@ -1,2 +1,2 @@
<ds-search></ds-search>
<ds-themed-search></ds-themed-search>
<ds-search-tracker></ds-search-tracker>

View File

@@ -1,3 +1,8 @@
<div class="container">
<h3>{{'bitstream.download.page' | translate:{bitstream: (bitstream$ | async)?.name} }}</h3>
<h3>{{'bitstream.download.page' | translate:{bitstream: (bitstream$ | async)?.name} }}</h3>
<div class="pt-3">
<button (click)="back()" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {{'bitstream.download.page.back' | translate}}
</button>
</div>
</div>

View File

@@ -12,6 +12,7 @@ import { FileService } from '../../core/shared/file.service';
import { HardRedirectService } from '../../core/services/hard-redirect.service';
import { getForbiddenRoute } from '../../app-routing-paths';
import { RemoteData } from '../../core/data/remote-data';
import { Location } from '@angular/common';
@Component({
selector: 'ds-bitstream-download-page',
@@ -33,10 +34,15 @@ export class BitstreamDownloadPageComponent implements OnInit {
private auth: AuthService,
private fileService: FileService,
private hardRedirectService: HardRedirectService,
private location: Location,
) {
}
back(): void {
this.location.back();
}
ngOnInit(): void {
this.bitstreamRD$ = this.route.data.pipe(

View File

@@ -0,0 +1,33 @@
import { Component, Input } from '@angular/core';
import { ComcolPageBrowseByComponent } from './comcol-page-browse-by.component';
import { ThemedComponent } from '../../theme-support/themed.component';
/**
* Themed wrapper for ComcolPageBrowseByComponent
*/
@Component({
selector: 'ds-themed-comcol-page-browse-by',
styleUrls: [],
templateUrl: '../../theme-support/themed.component.html',
})
export class ThemedComcolPageBrowseByComponent extends ThemedComponent<ComcolPageBrowseByComponent> {
/**
* The ID of the Community or Collection
*/
@Input() id: string;
@Input() contentType: string;
inAndOutputNames: (keyof ComcolPageBrowseByComponent & keyof this)[] = ['id', 'contentType'];
protected getComponentName(): string {
return 'ComcolPageBrowseByComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../../themes/${themeName}/app/shared/comcol-page-browse-by/comcol-page-browse-by.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import('./comcol-page-browse-by.component');
}
}

View File

@@ -9,6 +9,7 @@ import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/cre
import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component';
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component';
import { ThemedComcolPageBrowseByComponent } from './comcol-page-browse-by/themed-comcol-page-browse-by.component';
import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
import { SharedModule } from '../shared.module';
import { FormModule } from '../form/form.module';
@@ -23,6 +24,7 @@ const COMPONENTS = [
EditComColPageComponent,
DeleteComColPageComponent,
ComcolPageBrowseByComponent,
ThemedComcolPageBrowseByComponent,
ComcolRoleComponent,
];

View File

@@ -16,6 +16,7 @@ import { NotificationsService } from '../../../notifications/notifications.servi
import { TranslateService } from '@ngx-translate/core';
import { Collection } from '../../../../core/shared/collection.model';
import { FindListOptions } from '../../../../core/data/request.models';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
@Component({
selector: 'ds-authorized-collection-selector',
@@ -31,11 +32,14 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
*/
@Input() entityType: string;
constructor(protected searchService: SearchService,
protected collectionDataService: CollectionDataService,
protected notifcationsService: NotificationsService,
protected translate: TranslateService) {
super(searchService, notifcationsService, translate);
constructor(
protected searchService: SearchService,
protected collectionDataService: CollectionDataService,
protected notifcationsService: NotificationsService,
protected translate: TranslateService,
protected dsoNameService: DSONameService,
) {
super(searchService, notifcationsService, translate, dsoNameService);
}
/**

View File

@@ -22,7 +22,7 @@
<button *ngFor="let listEntry of (listEntries$ | async)"
class="list-group-item list-group-item-action border-0 list-entry"
[ngClass]="{'bg-primary': listEntry.indexableObject.id === currentDSOId}"
title="{{ listEntry.indexableObject.name }}"
title="{{ getName(listEntry) }}"
dsHoverClass="ds-hover"
(click)="onSelect.emit(listEntry.indexableObject)" #listEntryElement>
<ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode"

View File

@@ -34,6 +34,7 @@ import { SearchResult } from '../../search/models/search-result.model';
import { RemoteData } from '../../../core/data/remote-data';
import { NotificationsService } from '../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
@Component({
selector: 'ds-dso-selector',
@@ -126,9 +127,12 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
*/
public subs: Subscription[] = [];
constructor(protected searchService: SearchService,
protected notifcationsService: NotificationsService,
protected translate: TranslateService) {
constructor(
protected searchService: SearchService,
protected notifcationsService: NotificationsService,
protected translate: TranslateService,
protected dsoNameService: DSONameService,
) {
}
/**
@@ -257,4 +261,8 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
getName(searchResult: SearchResult<DSpaceObject>): string {
return this.dsoNameService.getName(searchResult.indexableObject);
}
}

View File

@@ -14,7 +14,8 @@
<div>
<ng-container #componentViewContainer></ng-container>
</div>
<small *ngIf="hasHint && ((model.repeatable === false && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
<small *ngIf="hasHint && ((!model.repeatable && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<!-- In case of repeatable fields show empty space for all elements except the first -->
<div *ngIf="context?.index !== null

View File

@@ -15,8 +15,8 @@
[cdkDragDisabled]="dragDisabled"
[cdkDragPreviewClass]="'ds-submission-reorder-dragging'">
<!-- Item content -->
<div class="drag-handle" [class.invisible]="dragDisabled" tabindex="0">
<i class="drag-icon fas fa-grip-vertical fa-fw" ></i>
<div class="drag-handle" [class.drag-disable]="dragDisabled" tabindex="0">
<i class="drag-icon fas fa-grip-vertical fa-fw" [class.drag-disable]="dragDisabled" ></i>
</div>
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
<ds-dynamic-form-control-container *ngFor="let _model of groupModel.group"

View File

@@ -3,7 +3,15 @@
:host {
display: block;
}
.drag-disable {
visibility: hidden !important;
&:hover, &:focus {
cursor: default;
.drag-icon {
visibility: hidden !important;
}
}
}
.cdk-drag {
margin-left: calc(-2.3 * var(--bs-spacer));
margin-right: calc(-0.5 * var(--bs-spacer));

View File

@@ -1,4 +1,4 @@
<ds-search *ngIf="this.relationship.searchConfiguration && context"
<ds-themed-search *ngIf="this.relationship.searchConfiguration && context"
[configuration]="this.relationship.searchConfiguration"
[context]="context"
[fixedFilterQuery]="this.relationship.filter"
@@ -60,4 +60,4 @@
</div>
</div>
</ds-search>
</ds-themed-search>

View File

@@ -0,0 +1,3 @@
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToOidc()">
<i class="fas fa-sign-in-alt"></i> {{"login.form.oidc" | translate}}
</button>

View File

@@ -0,0 +1,155 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { Store, StoreModule } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { EPersonMock } from '../../../testing/eperson.mock';
import { authReducer } from '../../../../core/auth/auth.reducer';
import { AuthService } from '../../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../testing/auth-service.stub';
import { storeModuleConfig } from '../../../../app.reducer';
import { AuthMethod } from '../../../../core/auth/models/auth.method';
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
import { LogInOidcComponent } from './log-in-oidc.component';
import { NativeWindowService } from '../../../../core/services/window.service';
import { RouterStub } from '../../../testing/router.stub';
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
describe('LogInOidcComponent', () => {
let component: LogInOidcComponent;
let fixture: ComponentFixture<LogInOidcComponent>;
let page: Page;
let user: EPerson;
let componentAsAny: any;
let setHrefSpy;
let oidcBaseUrl;
let location;
let initialState: any;
let hardRedirectService: HardRedirectService;
beforeEach(() => {
user = EPersonMock;
oidcBaseUrl = 'dspace-rest.test/oidc?redirectUrl=';
location = oidcBaseUrl + 'http://dspace-angular.test/home';
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
getCurrentRoute: {},
redirect: {}
});
initialState = {
core: {
auth: {
authenticated: false,
loaded: false,
blocking: false,
loading: false,
authMethods: []
}
}
};
});
beforeEach(waitForAsync(() => {
// refine the test module by declaring the test component
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
TranslateModule.forRoot()
],
declarations: [
LogInOidcComponent
],
providers: [
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Oidc, location) },
{ provide: 'isStandalonePage', useValue: true },
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
{ provide: Router, useValue: new RouterStub() },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
{ provide: HardRedirectService, useValue: hardRedirectService },
provideMockStore({ initialState }),
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
})
.compileComponents();
}));
beforeEach(() => {
// create component and test fixture
fixture = TestBed.createComponent(LogInOidcComponent);
// get test component from the fixture
component = fixture.componentInstance;
componentAsAny = component;
// create page
page = new Page(component, fixture);
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
});
it('should set the properly a new redirectUrl', () => {
const currentUrl = 'http://dspace-angular.test/collections/12345';
componentAsAny._window.nativeWindow.location.href = currentUrl;
fixture.detectChanges();
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
component.redirectToOidc();
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
});
it('should not set a new redirectUrl', () => {
const currentUrl = 'http://dspace-angular.test/home';
componentAsAny._window.nativeWindow.location.href = currentUrl;
fixture.detectChanges();
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
component.redirectToOidc();
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
});
});
/**
* I represent the DOM elements and attach spies.
*
* @class Page
*/
class Page {
public emailInput: HTMLInputElement;
public navigateSpy: jasmine.Spy;
public passwordInput: HTMLInputElement;
constructor(private component: LogInOidcComponent, private fixture: ComponentFixture<LogInOidcComponent>) {
// use injector to get services
const injector = fixture.debugElement.injector;
const store = injector.get(Store);
// add spies
this.navigateSpy = spyOn(store, 'dispatch');
}
}

View File

@@ -0,0 +1,110 @@
import { Component, Inject, OnInit, } from '@angular/core';
import { Observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { renderAuthMethodFor } from '../log-in.methods-decorator';
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
import { AuthMethod } from '../../../../core/auth/models/auth.method';
import { CoreState } from '../../../../core/core.reducers';
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
import { isNotNull, isEmpty } from '../../../empty.util';
import { AuthService } from '../../../../core/auth/auth.service';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
import { take } from 'rxjs/operators';
import { URLCombiner } from '../../../../core/url-combiner/url-combiner';
@Component({
selector: 'ds-log-in-oidc',
templateUrl: './log-in-oidc.component.html',
})
@renderAuthMethodFor(AuthMethodType.Oidc)
export class LogInOidcComponent implements OnInit {
/**
* The authentication method data.
* @type {AuthMethod}
*/
public authMethod: AuthMethod;
/**
* True if the authentication is loading.
* @type {boolean}
*/
public loading: Observable<boolean>;
/**
* The oidc authentication location url.
* @type {string}
*/
public location: string;
/**
* Whether user is authenticated.
* @type {Observable<string>}
*/
public isAuthenticated: Observable<boolean>;
/**
* @constructor
* @param {AuthMethod} injectedAuthMethodModel
* @param {boolean} isStandalonePage
* @param {NativeWindowRef} _window
* @param {AuthService} authService
* @param {HardRedirectService} hardRedirectService
* @param {Store<State>} store
*/
constructor(
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
@Inject('isStandalonePage') public isStandalonePage: boolean,
@Inject(NativeWindowService) protected _window: NativeWindowRef,
private authService: AuthService,
private hardRedirectService: HardRedirectService,
private store: Store<CoreState>
) {
this.authMethod = injectedAuthMethodModel;
}
ngOnInit(): void {
// set isAuthenticated
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
// set loading
this.loading = this.store.pipe(select(isAuthenticationLoading));
// set location
this.location = decodeURIComponent(this.injectedAuthMethodModel.location);
}
redirectToOidc() {
this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => {
if (!this.isStandalonePage) {
redirectRoute = this.hardRedirectService.getCurrentRoute();
} else if (isEmpty(redirectRoute)) {
redirectRoute = '/';
}
const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString();
let oidcServerUrl = this.location;
const myRegexp = /\?redirectUrl=(.*)/g;
const match = myRegexp.exec(this.location);
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;
// Check whether the current page is different from the redirect url received from rest
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
// change the redirect url with the current page url
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
oidcServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
}
// redirect to oidc authentication url
this.hardRedirectService.redirect(oidcServerUrl);
});
}
}

View File

@@ -7,7 +7,8 @@ export const MockWindow = {
get href() {
return this._href;
}
}
},
origin: 'http://localhost'
};
export class NativeWindowRefMock {

View File

@@ -0,0 +1,87 @@
import { SubmissionFormsConfigService } from '../../core/config/submission-forms-config.service';
import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
const configRes = Object.assign(new SubmissionFormsModel(), {
'id': 'AccessConditionDefaultConfiguration',
'canChangeDiscoverable': true,
'accessConditionOptions': [
{
'name': 'openaccess',
'hasStartDate': false,
'hasEndDate': false
},
{
'name': 'lease',
'hasStartDate': false,
'hasEndDate': true,
'maxEndDate': '2022-06-20T12:17:44.420+00:00'
},
{
'name': 'embargo',
'hasStartDate': true,
'hasEndDate': false,
'maxStartDate': '2024-12-20T12:17:44.420+00:00'
},
{
'name': 'administrator',
'hasStartDate': false,
'hasEndDate': false
}
],
'type': 'submissionaccessoption',
'_links': {
'self': {
'href': 'http://localhost:8080/server/api/config/submissionaccessoptions/AccessConditionDefaultConfiguration'
}
}
});
const configResNotChangeDiscoverable = Object.assign(new SubmissionFormsModel(), {
'id': 'AccessConditionDefaultConfiguration',
'canChangeDiscoverable': false,
'accessConditionOptions': [
{
'name': 'openaccess',
'hasStartDate': false,
'hasEndDate': false
},
{
'name': 'lease',
'hasStartDate': false,
'hasEndDate': true,
'maxEndDate': '2022-06-20T12:17:44.420+00:00'
},
{
'name': 'embargo',
'hasStartDate': true,
'hasEndDate': false,
'maxStartDate': '2024-12-20T12:17:44.420+00:00'
},
{
'name': 'administrator',
'hasStartDate': false,
'hasEndDate': false
}
],
'type': 'submissionaccessoption',
'_links': {
'self': {
'href': 'http://localhost:8080/server/api/config/submissionaccessoptions/AccessConditionDefaultConfiguration'
}
}
});
export function getSubmissionAccessesConfigService(): SubmissionFormsConfigService {
return jasmine.createSpyObj('SubmissionAccessesConfigService', {
findByHref: createSuccessfulRemoteDataObject$(configRes),
});
}
export function getSubmissionAccessesConfigNotChangeDiscoverableService(): SubmissionFormsConfigService {
return jasmine.createSpyObj('SubmissionAccessesConfigService', {
findByHref: createSuccessfulRemoteDataObject$(configResNotChangeDiscoverable),
});
}

View File

@@ -0,0 +1,13 @@
import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model';
import { of as observableOf } from 'rxjs';
const dataRes = Object.assign(new SubmissionFormsModel(), {
'id': 'AccessConditionDefaultConfiguration',
'accessConditions': [],
});
export function getSectionAccessesService() {
return jasmine.createSpyObj('SectionAccessesService', {
getAccessesData: observableOf(dataRes),
});
}

View File

@@ -1654,7 +1654,7 @@ export const mockFileFormData = {
},
},
{
accessConditionGroup:{
accessConditionGroup: {
name: [
{
value: 'lease',
@@ -1723,3 +1723,94 @@ export const mockFileFormData = {
}
]
};
export const mockAccessesFormData = {
discoverable: true,
accessCondition: [
{
accessConditionGroup: {
name: [
{
value: 'openaccess',
language: null,
authority: null,
display: 'openaccess',
confidence: -1,
place: 0,
otherInformation: null
}
],
},
},
{
accessConditionGroup: {
name: [
{
value: 'lease',
language: null,
authority: null,
display: 'lease',
confidence: -1,
place: 0,
otherInformation: null
}
],
endDate: [
{
value: {
year: 2019,
month: 1,
day: 16
},
language: null,
authority: null,
display: {
year: 2019,
month: 1,
day: 16
},
confidence: -1,
place: 0,
otherInformation: null
}
],
}
},
{
accessConditionGroup: {
name: [
{
value: 'embargo',
language: null,
authority: null,
display: 'lease',
confidence: -1,
place: 0,
otherInformation: null
}
],
startDate: [
{
value: {
year: 2019,
month: 1,
day: 16
},
language: null,
authority: null,
display: {
year: 2019,
month: 1,
day: 16
},
confidence: -1,
place: 0,
otherInformation: null
}
],
}
}
]
};

View File

@@ -1,18 +1,19 @@
:host ::ng-deep {
--ds-wrapper-grid-spacing: calc(var(--bs-spacer) / 2);
div.thumbnail > .thumbnail-content {
height: var(--ds-card-thumbnail-height);
width: 100%;
display: block;
min-width: 100%;
min-height: 100%;
object-fit: cover;
object-position: 50% 15%;
}
div.card {
margin-top: var(--ds-wrapper-grid-spacing);
margin-bottom: var(--ds-wrapper-grid-spacing);
div.thumbnail > .thumbnail-content {
height: var(--ds-card-thumbnail-height);
width: 100%;
display: block;
min-width: 100%;
min-height: 100%;
object-fit: cover;
object-position: 50% 15%;
}
}
}
@@ -26,4 +27,3 @@
padding-right: var(--ds-wrapper-grid-spacing);
}
}

View File

@@ -1,10 +1,10 @@
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
<div class="alert alert-success w-100" role="alert">
<h4 class="alert-heading">Approved</h4>
<ds-item-list-preview *ngIf="workflowitem"
<ds-themed-item-list-preview *ngIf="workflowitem"
[item]="(workflowitem?.item | async)?.payload"
[object]="object"
[status]="status"
[showSubmitter]="showSubmitter"></ds-item-list-preview>
[showSubmitter]="showSubmitter"></ds-themed-item-list-preview>
</div>
</ng-container>

View File

@@ -1,10 +1,10 @@
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
<div class="alert alert-secondary w-100" role="alert">
<h4 class="alert-heading">Declined</h4>
<ds-item-list-preview *ngIf="workflowitem"
<ds-themed-item-list-preview *ngIf="workflowitem"
[item]="(workflowitem?.item | async)?.payload"
[object]="object"
[status]="status"
[showSubmitter]="showSubmitter"></ds-item-list-preview>
[showSubmitter]="showSubmitter"></ds-themed-item-list-preview>
</div>
</ng-container>

View File

@@ -1,9 +1,9 @@
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
<ds-item-list-preview *ngIf="workflowitem"
<ds-themed-item-list-preview *ngIf="workflowitem"
[item]="(workflowitem?.item | async)?.payload"
[object]="object"
[showSubmitter]="showSubmitter"
[status]="status"></ds-item-list-preview>
[status]="status"></ds-themed-item-list-preview>
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-claimed-task-actions>
</ng-container>

View File

@@ -0,0 +1,38 @@
import { Component, Input } from '@angular/core';
import { ThemedComponent } from '../../../theme-support/themed.component';
import { ItemListPreviewComponent } from './item-list-preview.component';
import { Item } from '../../../../core/shared/item.model';
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
import { SearchResult } from '../../../search/models/search-result.model';
/**
* Themed wrapper for ItemListPreviewComponent
*/
@Component({
selector: 'ds-themed-item-list-preview',
styleUrls: [],
templateUrl: '../../../theme-support/themed.component.html',
})
export class ThemedItemListPreviewComponent extends ThemedComponent<ItemListPreviewComponent> {
protected inAndOutputNames: (keyof ItemListPreviewComponent & keyof this)[] = ['item', 'object', 'status', 'showSubmitter'];
@Input() item: Item;
@Input() object: SearchResult<any>;
@Input() status: MyDspaceItemStatusType;
@Input() showSubmitter = false;
protected getComponentName(): string {
return 'ItemListPreviewComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../../../themes/${themeName}/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import('./item-list-preview.component');
}
}

View File

@@ -1,5 +1,5 @@
<ds-item-list-preview [item]="dso"
<ds-themed-item-list-preview [item]="dso"
[object]="object"
[status]="status"></ds-item-list-preview>
[status]="status"></ds-themed-item-list-preview>
<ds-item-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-item-actions>

View File

@@ -1,8 +1,8 @@
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
<ds-item-list-preview *ngIf="workflowitem"
<ds-themed-item-list-preview *ngIf="workflowitem"
[item]="(workflowitem?.item | async)?.payload"
[object]="object"
[showSubmitter]="showSubmitter"
[status]="status"></ds-item-list-preview>
[status]="status"></ds-themed-item-list-preview>
<ds-pool-task-actions id="actions" *ngIf="workflowitem" [object]="dso" (processCompleted)="this.reloadedObject.emit($event.reloadedObject)"></ds-pool-task-actions>
</ng-container>

View File

@@ -1,8 +1,8 @@
<ng-container *ngIf="item$ | async">
<ds-item-list-preview
<ds-themed-item-list-preview
[item]="item$ | async"
[object]="object"
[status]="status"></ds-item-list-preview>
[status]="status"></ds-themed-item-list-preview>
<ds-workflowitem-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-workflowitem-actions>
</ng-container>

View File

@@ -1,8 +1,8 @@
<ng-container *ngIf="item$ | async">
<ds-item-list-preview
<ds-themed-item-list-preview
[item]="item$ | async"
[object]="object"
[status]="status"></ds-item-list-preview>
[status]="status"></ds-themed-item-list-preview>
<ds-workspaceitem-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-workspaceitem-actions>
</ng-container>

View File

@@ -47,11 +47,23 @@ describe('SearchFormComponent', () => {
el = de.nativeElement;
});
it('should not display scopes when empty', () => {
it('should not display scopes when showScopeSelector is false', fakeAsync(() => {
comp.showScopeSelector = false;
fixture.detectChanges();
const select = de.query(By.css('select'));
expect(select).toBeNull();
});
tick();
expect(de.query(By.css('.scope-button'))).toBeFalsy();
}));
it('should display scopes when showScopeSelector is true', fakeAsync(() => {
comp.showScopeSelector = true;
fixture.detectChanges();
tick();
expect(de.query(By.css('.scope-button'))).toBeTruthy();
}));
it('should display set query value in input field', fakeAsync(() => {
const testString = 'This is a test query';

View File

@@ -78,7 +78,7 @@
[query]="(searchOptions$ | async)?.query"
[scope]="(searchOptions$ | async)?.scope"
[currentUrl]="searchLink"
[showScopeSelector]="true"
[showScopeSelector]="showScopeSelector"
[inPlaceSearch]="inPlaceSearch"
[searchPlaceholder]="searchFormPlaceholder | translate">
</ds-search-form>

View File

@@ -134,6 +134,11 @@ export class SearchComponent implements OnInit {
*/
@Input() viewModeList: ViewMode[];
/**
* Defines whether or not to show the scope selector
*/
@Input() showScopeSelector = false;
/**
* The current configuration used during the search
*/

View File

@@ -28,9 +28,11 @@ import { MissingTranslationHelper } from '../translate/missing-translation.helpe
import { SharedModule } from '../shared.module';
import { SearchResultsComponent } from './search-results/search-results.component';
import { SearchComponent } from './search.component';
import { ThemedSearchComponent } from './themed-search.component';
const COMPONENTS = [
SearchComponent,
ThemedSearchComponent,
SearchResultsComponent,
SearchSidebarComponent,
SearchSettingsComponent,

View File

@@ -0,0 +1,75 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { ThemedComponent } from '../theme-support/themed.component';
import { SearchComponent } from './search.component';
import { SearchConfigurationOption } from './search-switch-configuration/search-configuration-option.model';
import { Context } from '../../core/shared/context.model';
import { CollectionElementLinkType } from '../object-collection/collection-element-link.type';
import { SelectionConfig } from './search-results/search-results.component';
import { ViewMode } from '../../core/shared/view-mode.model';
import { SearchObjects } from './models/search-objects.model';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { ListableObject } from '../object-collection/shared/listable-object.model';
/**
* Themed wrapper for SearchComponent
*/
@Component({
selector: 'ds-themed-search',
styleUrls: [],
templateUrl: '../theme-support/themed.component.html',
})
export class ThemedSearchComponent extends ThemedComponent<SearchComponent> {
protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', 'paginationId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showSidebar', 'showViewModes', 'useUniquePageId', 'viewModeList', 'resultFound', 'deselectObject', 'selectObject'];
@Input() configurationList: SearchConfigurationOption[] = [];
@Input() context: Context = Context.Search;
@Input() configuration = 'default';
@Input() fixedFilterQuery: string;
@Input() useCachedVersionIfAvailable = true;
@Input() inPlaceSearch = true;
@Input() linkType: CollectionElementLinkType;
@Input() paginationId = 'spc';
@Input() searchEnabled = true;
@Input() sideBarWidth = 3;
@Input() searchFormPlaceholder = 'search.search-form.placeholder';
@Input() selectable = false;
@Input() selectionConfig: SelectionConfig;
@Input() showSidebar = true;
@Input() showViewModes = true;
@Input() useUniquePageId: false;
@Input() viewModeList: ViewMode[];
@Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter<SearchObjects<DSpaceObject>>();
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
protected getComponentName(): string {
return 'SearchComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/shared/search/search.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import('./search.component');
}
}

View File

@@ -7,13 +7,8 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
import { NouisliderModule } from 'ng2-nouislider';
import {
NgbDatepickerModule,
NgbDropdownModule,
NgbNavModule,
NgbPaginationModule,
NgbTimepickerModule,
NgbTooltipModule,
NgbTypeaheadModule
NgbDatepickerModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbTimepickerModule, NgbTooltipModule,
NgbTypeaheadModule,
} from '@ng-bootstrap/ng-bootstrap';
import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core';
import { NgxPaginationModule } from 'ngx-pagination';
@@ -176,6 +171,8 @@ import { ItemVersionsDeleteModalComponent } from './item/item-versions/item-vers
import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component';
import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
import { DsSelectComponent } from './ds-select/ds-select.component';
import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component';
import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -251,6 +248,7 @@ const COMPONENTS = [
UploaderComponent,
FileDropzoneNoUploaderComponent,
ItemListPreviewComponent,
ThemedItemListPreviewComponent,
MyDSpaceItemStatusComponent,
ItemSubmitterComponent,
ItemDetailPreviewComponent,
@@ -307,6 +305,7 @@ const COMPONENTS = [
ImportableListItemControlComponent,
LogInShibbolethComponent,
LogInOidcComponent,
LogInPasswordComponent,
LogInContainerComponent,
ItemVersionsComponent,
@@ -378,6 +377,7 @@ const ENTRY_COMPONENTS = [
ItemMetadataRepresentationListElementComponent,
LogInPasswordComponent,
LogInShibbolethComponent,
LogInOidcComponent,
BundleListElementComponent,
ClaimedTaskActionsApproveComponent,
ClaimedTaskActionsRejectComponent,

View File

@@ -0,0 +1,100 @@
import { FormControl, FormGroup } from '@angular/forms';
import { DynamicCheckboxModel, DynamicSelectModel } from '@ng-dynamic-forms/core';
export const accessConditionChangeEvent = {
$event: {
bubbles: true,
cancelBubble: false,
cancelable: false,
composed: false,
currentTarget: null,
defaultPrevented: false,
eventPhase: 0,
isTrusted: true,
returnValue: true,
timeStamp: 143042.8999999999,
type: 'change',
},
context: null,
control: new FormControl({
errors: null,
pristine: false,
status: 'VALID',
statusChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
touched: true,
value: { year: 2021, month: 12, day: 30 },
valueChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
_updateOn: 'change',
}),
group: new FormGroup({}),
model: new DynamicSelectModel({
additional: null,
asyncValidators: null,
controlTooltip: null,
errorMessages: { required: 'submission.sections.upload.form.date-required-until' },
hidden: false,
hint: null,
id: 'endDate',
label: 'submission.sections.upload.form.until-label',
labelTooltip: null,
name: 'endDate',
placeholder: 'Until',
prefix: null,
relations: [],
required: true,
suffix: null,
tabIndex: null,
updateOn: null,
validators: { required: null },
}),
type: 'change'
};
export const checkboxChangeEvent = {
$event: {
bubbles: true,
cancelBubble: false,
cancelable: false,
composed: false,
currentTarget: null,
defaultPrevented: false,
eventPhase: 0,
isTrusted: true,
returnValue: true,
timeStamp: 143042.8999999999,
type: 'change',
},
context: null,
control: new FormControl({
errors: null,
pristine: false,
status: 'VALID',
statusChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
touched: true,
value: { year: 2021, month: 12, day: 30 },
valueChanges: { _isScalar: false, observers: [], closed: false, isStopped: false, hasError: false },
_updateOn: 'change',
}),
group: new FormGroup({}),
model: new DynamicCheckboxModel({
additional: null,
asyncValidators: null,
controlTooltip: null,
errorMessages: null,
hidden: false,
hint: null,
id: 'discoverable',
indeterminate: false,
label: 'Discoverable',
labelPosition: null,
labelTooltip: null,
name: 'discoverable',
relations: [],
required: false,
tabIndex: null,
updateOn: null,
validators: { required: null },
}),
type: 'change'
};

View File

@@ -29,7 +29,10 @@ export const routeServiceStub: any = {
return observableOf({});
},
getHistory: () => {
return observableOf(['/home','/collection/123','/home']);
return observableOf(['/home', '/collection/123', '/home']);
},
getPreviousUrl: () => {
return observableOf('/home');
}
/* tslint:enable:no-empty */
};

View File

@@ -46,6 +46,9 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
// if an input or output has changed
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
this.connectInputsAndOutputs();
if (this.compRef?.instance && 'ngOnChanges' in this.compRef?.instance) {
(this.compRef.instance as any).ngOnChanges(changes);
}
}
}

View File

@@ -155,7 +155,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
// init submission state
this.subs.push(
this.halService.getEndpoint('workspaceitems').pipe(
this.halService.getEndpoint(this.submissionService.getSubmissionObjectLinkName()).pipe(
filter((href: string) => isNotEmpty(href)),
distinctUntilChanged())
.subscribe((endpointURL) => {

View File

@@ -205,7 +205,7 @@ export class SubmissionObjectEffects {
action.payload.submissionId,
'sections') as Observable<SubmissionObject[]>;
} else {
response$ = this.submissionObjectService.findById(action.payload.submissionId).pipe(
response$ = this.submissionObjectService.findById(action.payload.submissionId, false, true).pipe(
getFirstSucceededRemoteDataPayload(),
map((submissionObject: SubmissionObject) => [submissionObject])
);
@@ -356,6 +356,8 @@ export class SubmissionObjectEffects {
* The submission object retrieved from REST
* @param submissionId
* The submission id
* @param forms
* The forms state
* @param notify
* A boolean that indicate if show notification or not
* @return SubmissionObjectAction[]
@@ -365,7 +367,7 @@ export class SubmissionObjectEffects {
currentState: SubmissionObjectEntry,
response: SubmissionObject[],
submissionId: string,
forms,
forms: FormState,
notify: boolean = true): SubmissionObjectAction[] {
const mappedActions = [];

View File

@@ -0,0 +1,7 @@
<ds-form *ngIf="!!formModel" #formRef="formComponent"
[formId]="formId"
[formModel]="formModel"
[displaySubmit]="false"
[displayCancel]="false"
(dfChange)="onChange($event)"
(removeArrayItem)="onRemove($event)"></ds-form>

View File

@@ -0,0 +1,5 @@
::ng-deep .access-condition-group {
position: relative;
top: -2.3rem;
margin-bottom: -2.3rem;
}

View File

@@ -0,0 +1,204 @@
import { FormService } from '../../../shared/form/form.service';
import { ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { SubmissionSectionAccessesComponent } from './section-accesses.component';
import { SectionsService } from '../sections.service';
import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { SubmissionAccessesConfigService } from '../../../core/config/submission-accesses-config.service';
import {
getSubmissionAccessesConfigNotChangeDiscoverableService,
getSubmissionAccessesConfigService
} from '../../../shared/mocks/section-accesses-config.service.mock';
import { SectionAccessesService } from './section-accesses.service';
import { SectionFormOperationsService } from '../form/section-form-operations.service';
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
import { getSectionAccessesService } from '../../../shared/mocks/section-accesses.service.mock';
import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock';
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service.stub';
import { BrowserModule } from '@angular/platform-browser';
import { of as observableOf } from 'rxjs';
import { Store } from '@ngrx/store';
import { FormComponent } from '../../../shared/form/form.component';
import {
DynamicCheckboxModel,
DynamicDatePickerModel,
DynamicFormArrayModel,
DynamicSelectModel
} from '@ng-dynamic-forms/core';
import { AppState } from '../../../app.reducer';
import { getMockFormService } from '../../../shared/mocks/form-service.mock';
import { mockAccessesFormData } from '../../../shared/mocks/submission.mock';
import { accessConditionChangeEvent, checkboxChangeEvent } from '../../../shared/testing/form-event.stub';
describe('SubmissionSectionAccessesComponent', () => {
let component: SubmissionSectionAccessesComponent;
let fixture: ComponentFixture<SubmissionSectionAccessesComponent>;
const sectionsServiceStub = new SectionsServiceStub();
// const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex);
const builderService: FormBuilderService = getMockFormBuilderService();
const submissionAccessesConfigService = getSubmissionAccessesConfigService();
const sectionAccessesService = getSectionAccessesService();
const sectionFormOperationsService = getMockFormOperationsService();
const operationsBuilder = jasmine.createSpyObj('operationsBuilder', {
add: undefined,
remove: undefined,
replace: undefined,
});
let formService: any;
const storeStub = jasmine.createSpyObj('store', ['dispatch']);
const sectionData = {
header: 'submit.progressbar.accessCondition',
config: 'http://localhost:8080/server/api/config/submissionaccessoptions/AccessConditionDefaultConfiguration',
mandatory: true,
sectionType: 'accessCondition',
collapsed: false,
enabled: true,
data: {
discoverable: true,
accessConditions: []
},
errorsToShow: [],
serverValidationErrors: [],
isLoading: false,
isValid: true
};
describe('First with canChangeDiscoverable true', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
BrowserModule,
TranslateModule.forRoot()
],
declarations: [SubmissionSectionAccessesComponent, FormComponent],
providers: [
{ provide: SectionsService, useValue: sectionsServiceStub },
{ provide: FormBuilderService, useValue: builderService },
{ provide: SubmissionAccessesConfigService, useValue: submissionAccessesConfigService },
{ provide: SectionAccessesService, useValue: sectionAccessesService },
{ provide: SectionFormOperationsService, useValue: sectionFormOperationsService },
{ provide: JsonPatchOperationsBuilder, useValue: operationsBuilder },
{ provide: TranslateService, useValue: getMockTranslateService() },
{ provide: FormService, useValue: getMockFormService() },
{ provide: Store, useValue: storeStub },
{ provide: SubmissionJsonPatchOperationsService, useValue: SubmissionJsonPatchOperationsServiceStub },
{ provide: 'sectionDataProvider', useValue: sectionData },
{ provide: 'submissionIdProvider', useValue: '1508' },
]
})
.compileComponents();
});
beforeEach(inject([Store], (store: Store<AppState>) => {
fixture = TestBed.createComponent(SubmissionSectionAccessesComponent);
component = fixture.componentInstance;
formService = TestBed.inject(FormService);
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(observableOf(true));
formService.getFormData.and.returnValue(observableOf(mockAccessesFormData));
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have created formModel', () => {
expect(component.formModel).toBeTruthy();
});
it('should have formModel length should be 2', () => {
expect(component.formModel.length).toEqual(2);
});
it('formModel should have 1 model type checkbox and 1 model type array', () => {
expect(component.formModel[0] instanceof DynamicCheckboxModel).toBeTrue();
expect(component.formModel[1] instanceof DynamicFormArrayModel).toBeTrue();
});
it('formModel type array should have formgroup with 1 input and 2 datepickers', () => {
const formModel: any = component.formModel[1];
const formGroup = formModel.groupFactory()[0].group;
expect(formGroup[0] instanceof DynamicSelectModel).toBeTrue();
expect(formGroup[1] instanceof DynamicDatePickerModel).toBeTrue();
expect(formGroup[2] instanceof DynamicDatePickerModel).toBeTrue();
});
it('when checkbox changed it should call operationsBuilder replace function', () => {
component.onChange(checkboxChangeEvent);
fixture.detectChanges();
expect(operationsBuilder.replace).toHaveBeenCalled();
});
it('when dropdown select changed it should call operationsBuilder add function', () => {
component.onChange(accessConditionChangeEvent);
fixture.detectChanges();
expect(operationsBuilder.add).toHaveBeenCalled();
});
});
describe('when canDescoverable is false', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
BrowserModule,
TranslateModule.forRoot()
],
declarations: [SubmissionSectionAccessesComponent, FormComponent],
providers: [
{ provide: SectionsService, useValue: sectionsServiceStub },
{ provide: FormBuilderService, useValue: builderService },
{ provide: SubmissionAccessesConfigService, useValue: getSubmissionAccessesConfigNotChangeDiscoverableService() },
{ provide: SectionAccessesService, useValue: sectionAccessesService },
{ provide: SectionFormOperationsService, useValue: sectionFormOperationsService },
{ provide: JsonPatchOperationsBuilder, useValue: operationsBuilder },
{ provide: TranslateService, useValue: getMockTranslateService() },
{ provide: FormService, useValue: getMockFormService() },
{ provide: Store, useValue: storeStub },
{ provide: SubmissionJsonPatchOperationsService, useValue: SubmissionJsonPatchOperationsServiceStub },
{ provide: 'sectionDataProvider', useValue: sectionData },
{ provide: 'submissionIdProvider', useValue: '1508' },
]
})
.compileComponents();
});
beforeEach(inject([Store], (store: Store<AppState>) => {
fixture = TestBed.createComponent(SubmissionSectionAccessesComponent);
component = fixture.componentInstance;
formService = TestBed.inject(FormService);
formService.validateAllFormFields.and.callFake(() => null);
formService.isValid.and.returnValue(observableOf(true));
formService.getFormData.and.returnValue(observableOf(mockAccessesFormData));
fixture.detectChanges();
}));
it('should have formModel length should be 1', () => {
expect(component.formModel.length).toEqual(1);
});
it('formModel should have only 1 model type array', () => {
expect(component.formModel[0] instanceof DynamicFormArrayModel).toBeTrue();
});
});
});

View File

@@ -0,0 +1,379 @@
import { SectionAccessesService } from './section-accesses.service';
import { Component, Inject, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { filter, map, mergeMap, take } from 'rxjs/operators';
import { combineLatest, Observable, of, Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { renderSectionFor } from '../sections-decorator';
import { SectionsType } from '../sections-type';
import { SectionDataObject } from '../models/section-data.model';
import { SectionsService } from '../sections.service';
import { SectionModelComponent } from '../models/section.model';
import {
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX,
DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER,
DynamicCheckboxModel,
DynamicDatePickerModel,
DynamicFormArrayModel,
DynamicFormControlEvent,
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicSelectModel,
MATCH_ENABLED,
OR_OPERATOR
} from '@ng-dynamic-forms/core';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import {
ACCESS_CONDITION_GROUP_CONFIG,
ACCESS_CONDITION_GROUP_LAYOUT,
ACCESS_CONDITIONS_FORM_ARRAY_CONFIG,
ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT,
ACCESS_FORM_CHECKBOX_CONFIG,
ACCESS_FORM_CHECKBOX_LAYOUT,
FORM_ACCESS_CONDITION_END_DATE_CONFIG,
FORM_ACCESS_CONDITION_END_DATE_LAYOUT,
FORM_ACCESS_CONDITION_START_DATE_CONFIG,
FORM_ACCESS_CONDITION_START_DATE_LAYOUT,
FORM_ACCESS_CONDITION_TYPE_CONFIG,
FORM_ACCESS_CONDITION_TYPE_LAYOUT
} from './section-accesses.model';
import { hasValue, isNotEmpty, isNotNull } from '../../../shared/empty.util';
import { WorkspaceitemSectionAccessesObject } from '../../../core/submission/models/workspaceitem-section-accesses.model';
import { SubmissionAccessesConfigService } from '../../../core/config/submission-accesses-config.service';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { FormComponent } from '../../../shared/form/form.component';
import { FormService } from '../../../shared/form/form.service';
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { SectionFormOperationsService } from '../form/section-form-operations.service';
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
import { AccessesConditionOption } from '../../../core/config/models/config-accesses-conditions-options.model';
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
import { dateToISOFormat } from '../../../shared/date.util';
/**
* This component represents a section for managing item's access conditions.
*/
@Component({
selector: 'ds-section-accesses',
templateUrl: './section-accesses.component.html',
styleUrls: ['./section-accesses.component.scss']
})
@renderSectionFor(SectionsType.AccessesCondition)
export class SubmissionSectionAccessesComponent extends SectionModelComponent {
/**
* The FormComponent reference
*/
@ViewChild('formRef') public formRef: FormComponent;
/**
* List of available access conditions that could be set to item
*/
public availableAccessConditionOptions: AccessesConditionOption[]; // List of accessConditions that an user can select
/**
* The form id
* @type {string}
*/
public formId: string;
/**
* The accesses section data
* @type {WorkspaceitemSectionAccessesObject}
*/
public accessesData: WorkspaceitemSectionAccessesObject;
/**
* The form model
* @type {DynamicFormControlModel[]}
*/
public formModel: DynamicFormControlModel[];
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
/**
* The [[JsonPatchOperationPathCombiner]] object
* @type {JsonPatchOperationPathCombiner}
*/
protected pathCombiner: JsonPatchOperationPathCombiner;
/**
* Defines if the access discoverable property can be managed
*/
public canChangeDiscoverable: boolean;
/**
* Initialize instance variables
*
* @param {SectionsService} sectionService
* @param {SectionDataObject} injectedSectionData
* @param {FormService} formService
* @param {JsonPatchOperationsBuilder} operationsBuilder
* @param {SectionFormOperationsService} formOperationsService
* @param {FormBuilderService} formBuilderService
* @param {TranslateService} translate
* @param {SubmissionAccessesConfigService} accessesConfigService
* @param {SectionAccessesService} accessesService
* @param {SubmissionJsonPatchOperationsService} operationsService
* @param {string} injectedSubmissionId
*/
constructor(
protected sectionService: SectionsService,
private formBuilderService: FormBuilderService,
private accessesConfigService: SubmissionAccessesConfigService,
private accessesService: SectionAccessesService,
protected formOperationsService: SectionFormOperationsService,
protected operationsBuilder: JsonPatchOperationsBuilder,
private formService: FormService,
private translate: TranslateService,
private operationsService: SubmissionJsonPatchOperationsService,
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
@Inject('submissionIdProvider') public injectedSubmissionId: string) {
super(undefined, injectedSectionData, injectedSubmissionId);
}
/**
* Initialize form model values
*
* @param formModel
* The form model
*/
public initModelData(formModel: DynamicFormControlModel[]) {
this.accessesData.accessConditions.forEach((accessCondition, index) => {
Array.of('name', 'startDate', 'endDate')
.filter((key) => accessCondition.hasOwnProperty(key) && isNotEmpty(accessCondition[key]))
.forEach((key) => {
const metadataModel: any = this.formBuilderService.findById(key, formModel, index);
if (metadataModel) {
if (metadataModel.type === DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER) {
const date = new Date(accessCondition[key]);
metadataModel.value = {
year: date.getUTCFullYear(),
month: date.getUTCMonth() + 1,
day: date.getUTCDate()
};
} else {
metadataModel.value = accessCondition[key];
}
}
});
});
}
/**
* Method called when a form dfChange event is fired.
* Dispatch form operations based on changes.
*/
onChange(event: DynamicFormControlEvent) {
if (event.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX) {
const path = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event);
const value = this.formOperationsService.getFieldValueFromChangeEvent(event);
this.operationsBuilder.replace(this.pathCombiner.getPath(path), value.value, true);
} else {
if (event.model.id === FORM_ACCESS_CONDITION_TYPE_CONFIG.id) {
// Clear previous state when switching through different access conditions
const startDateControl: FormControl = event.control.parent.get('startDate') as FormControl;
const endDateControl: FormControl = event.control.parent.get('endDate') as FormControl;
startDateControl?.markAsUntouched();
endDateControl?.markAsUntouched();
startDateControl?.setValue(null);
endDateControl?.setValue(null);
event.control.parent.markAsDirty();
}
// validate form
this.formService.validateAllFormFields(this.formRef.formGroup);
this.formService.isValid(this.formId).pipe(
take(1),
filter((isValid) => isValid),
mergeMap(() => this.formService.getFormData(this.formId)),
take(1)
).subscribe((formData: any) => {
const accessConditionsToSave = [];
formData.accessCondition
.map((accessConditions) => accessConditions.accessConditionGroup)
.filter((accessCondition) => isNotEmpty(accessCondition))
.forEach((accessCondition) => {
let accessConditionOpt;
this.availableAccessConditionOptions
.filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value)
.forEach((element) => accessConditionOpt = element);
if (accessConditionOpt) {
const currentAccessCondition = Object.assign({}, accessCondition);
currentAccessCondition.name = this.retrieveValueFromField(accessCondition.name);
/* When start and end date fields are deactivated, their values may be still present in formData,
therefore it is necessary to delete them if they're not allowed by the current access condition option. */
if (!accessConditionOpt.hasStartDate) {
delete currentAccessCondition.startDate;
} else if (accessCondition.startDate) {
const startDate = this.retrieveValueFromField(accessCondition.startDate);
currentAccessCondition.startDate = dateToISOFormat(startDate);
}
if (!accessConditionOpt.hasEndDate) {
delete currentAccessCondition.endDate;
} else if (accessCondition.endDate) {
const endDate = this.retrieveValueFromField(accessCondition.endDate);
currentAccessCondition.endDate = dateToISOFormat(endDate);
}
accessConditionsToSave.push(currentAccessCondition);
}
});
this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true);
});
}
}
/**
* Method called when a form removeArrayItem event is fired.
* Dispatch remove form operations based on changes.
*/
onRemove(event: DynamicFormControlEvent) {
const fieldIndex = this.formOperationsService.getArrayIndexFromEvent(event);
const fieldPath = 'accessConditions/' + fieldIndex;
this.operationsBuilder.remove(this.pathCombiner.getPath(fieldPath));
}
/**
* Unsubscribe from all subscriptions
*/
onSectionDestroy() {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
/**
* Initialize all instance variables and retrieve collection default access conditions
*/
protected onSectionInit(): void {
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id);
this.formId = this.formService.getUniqueId(this.sectionData.id);
const config$ = this.accessesConfigService.findByHref(this.sectionData.config, true, false).pipe(
getFirstSucceededRemoteData(),
map((config) => config.payload),
);
const accessData$ = this.accessesService.getAccessesData(this.submissionId, this.sectionData.id);
combineLatest([config$, accessData$]).subscribe(([config, accessData]) => {
this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : [];
this.canChangeDiscoverable = !!config.canChangeDiscoverable;
this.accessesData = accessData;
this.formModel = this.buildFileEditForm();
});
}
/**
* Get section status
*
* @return Observable<boolean>
* the section status
*/
protected getSectionStatus(): Observable<boolean> {
return of(true);
}
/**
* Initialize form model
*/
protected buildFileEditForm() {
const formModel: DynamicFormControlModel[] = [];
if (this.canChangeDiscoverable) {
const discoverableCheckboxConfig = Object.assign({}, ACCESS_FORM_CHECKBOX_CONFIG, {
label: this.translate.instant('submission.sections.accesses.form.discoverable-label'),
hint: this.translate.instant('submission.sections.accesses.form.discoverable-description'),
value: this.accessesData.discoverable
});
formModel.push(
new DynamicCheckboxModel(discoverableCheckboxConfig, ACCESS_FORM_CHECKBOX_LAYOUT)
);
}
const accessConditionTypeModelConfig = Object.assign({}, FORM_ACCESS_CONDITION_TYPE_CONFIG);
const accessConditionsArrayConfig = Object.assign({}, ACCESS_CONDITIONS_FORM_ARRAY_CONFIG);
const accessConditionTypeOptions = [];
for (const accessCondition of this.availableAccessConditionOptions) {
accessConditionTypeOptions.push(
{
label: accessCondition.name,
value: accessCondition.name
}
);
}
accessConditionTypeModelConfig.options = accessConditionTypeOptions;
// Dynamically assign of relation in config. For startdate, endDate, groups.
const hasStart = [];
const hasEnd = [];
const hasGroups = [];
this.availableAccessConditionOptions.forEach((condition) => {
const showStart: boolean = condition.hasStartDate === true;
const showEnd: boolean = condition.hasEndDate === true;
const showGroups: boolean = showStart || showEnd;
if (showStart) {
hasStart.push({ id: 'name', value: condition.name });
}
if (showEnd) {
hasEnd.push({ id: 'name', value: condition.name });
}
if (showGroups) {
hasGroups.push({ id: 'name', value: condition.name });
}
});
const confStart = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasStart }] };
const confEnd = { relations: [{ match: MATCH_ENABLED, operator: OR_OPERATOR, when: hasEnd }] };
accessConditionsArrayConfig.groupFactory = () => {
const type = new DynamicSelectModel(accessConditionTypeModelConfig, FORM_ACCESS_CONDITION_TYPE_LAYOUT);
const startDateConfig = Object.assign({}, FORM_ACCESS_CONDITION_START_DATE_CONFIG, confStart);
const endDateConfig = Object.assign({}, FORM_ACCESS_CONDITION_END_DATE_CONFIG, confEnd);
const startDate = new DynamicDatePickerModel(startDateConfig, FORM_ACCESS_CONDITION_START_DATE_LAYOUT);
const endDate = new DynamicDatePickerModel(endDateConfig, FORM_ACCESS_CONDITION_END_DATE_LAYOUT);
const accessConditionGroupConfig = Object.assign({}, ACCESS_CONDITION_GROUP_CONFIG);
accessConditionGroupConfig.group = [type];
if (hasStart.length > 0) {
accessConditionGroupConfig.group.push(startDate);
}
if (hasEnd.length > 0) {
accessConditionGroupConfig.group.push(endDate);
}
return [new DynamicFormGroupModel(accessConditionGroupConfig, ACCESS_CONDITION_GROUP_LAYOUT)];
};
// Number of access conditions blocks in form
accessConditionsArrayConfig.initialCount = isNotEmpty(this.accessesData.accessConditions) ? this.accessesData.accessConditions.length : 1;
formModel.push(
new DynamicFormArrayModel(accessConditionsArrayConfig, ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT)
);
this.initModelData(formModel);
return formModel;
}
protected retrieveValueFromField(field: any) {
const temp = Array.isArray(field) ? field[0] : field;
return (temp) ? temp.value : undefined;
}
}

View File

@@ -0,0 +1,123 @@
import {
DynamicDatePickerModelConfig,
DynamicFormArrayModelConfig,
DynamicFormControlLayout,
DynamicFormGroupModelConfig,
DynamicSelectModelConfig,
MATCH_ENABLED,
OR_OPERATOR,
} from '@ng-dynamic-forms/core';
import { DynamicCheckboxModelConfig } from '@ng-dynamic-forms/core/lib/model/checkbox/dynamic-checkbox.model';
export const ACCESS_FORM_CHECKBOX_CONFIG: DynamicCheckboxModelConfig = {
id: 'discoverable',
name: 'discoverable'
};
export const ACCESS_FORM_CHECKBOX_LAYOUT = {
element: {
container: 'custom-control custom-checkbox pl-1',
control: 'custom-control-input',
label: 'custom-control-label pt-1'
}
};
export const ACCESS_CONDITION_GROUP_CONFIG: DynamicFormGroupModelConfig = {
id: 'accessConditionGroup',
group: []
};
export const ACCESS_CONDITION_GROUP_LAYOUT: DynamicFormControlLayout = {
element: {
host: 'form-group access-condition-group col',
container: 'pl-1 pr-1',
control: 'form-row '
}
};
export const ACCESS_CONDITIONS_FORM_ARRAY_CONFIG: DynamicFormArrayModelConfig = {
id: 'accessCondition',
groupFactory: null,
};
export const ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT: DynamicFormControlLayout = {
grid: {
group: 'form-row pt-4',
}
};
export const FORM_ACCESS_CONDITION_TYPE_CONFIG: DynamicSelectModelConfig<any> = {
id: 'name',
label: 'submission.sections.accesses.form.access-condition-label',
hint: 'submission.sections.accesses.form.access-condition-hint',
options: []
};
export const FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayout = {
element: {
host: 'col-12',
label: 'col-form-label name-label'
}
};
export const FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'startDate',
label: 'submission.sections.accesses.form.from-label',
hint: 'submission.sections.accesses.form.from-hint',
placeholder: 'submission.sections.accesses.form.from-placeholder',
inline: false,
toggleIcon: 'far fa-calendar-alt',
relations: [
{
match: MATCH_ENABLED,
operator: OR_OPERATOR,
when: []
}
],
required: true,
validators: {
required: null
},
errorMessages: {
required: 'submission.sections.accesses.form.date-required-from'
}
};
export const FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = {
element: {
label: 'col-form-label'
},
grid: {
host: 'col-6'
}
};
export const FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'endDate',
label: 'submission.sections.accesses.form.until-label',
hint: 'submission.sections.accesses.form.until-hint',
placeholder: 'submission.sections.accesses.form.until-placeholder',
inline: false,
toggleIcon: 'far fa-calendar-alt',
relations: [
{
match: MATCH_ENABLED,
operator: OR_OPERATOR,
when: []
}
],
required: true,
validators: {
required: null
},
errorMessages: {
required: 'submission.sections.accesses.form.date-required-until'
}
};
export const FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = {
element: {
label: 'col-form-label'
},
grid: {
host: 'col-6'
}
};

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { distinctUntilChanged, filter } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { SubmissionState } from '../../submission.reducers';
import { isNotUndefined } from '../../../shared/empty.util';
import { submissionSectionDataFromIdSelector } from '../../selectors';
import { WorkspaceitemSectionAccessesObject } from '../../../core/submission/models/workspaceitem-section-accesses.model';
/**
* A service that provides methods to handle submission item's accesses condition state.
*/
@Injectable()
export class SectionAccessesService {
/**
* Initialize service variables
*
* @param {Store<SubmissionState>} store
*/
constructor(private store: Store<SubmissionState>) { }
/**
* Return item's accesses condition state.
*
* @param submissionId
* The submission id
* @param sectionId
* The section id
* @returns {Observable}
* Emits bitstream's metadata
*/
public getAccessesData(submissionId: string, sectionId: string): Observable<WorkspaceitemSectionAccessesObject> {
return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe(
filter((state) => isNotUndefined(state)),
distinctUntilChanged());
}
}

Some files were not shown because too many files have changed in this diff Show More