Merge branch 'master' into 460-coll-pages

This commit is contained in:
lhenze
2019-10-01 12:09:15 -04:00
36 changed files with 524 additions and 222 deletions

View File

@@ -81,7 +81,6 @@ const components = [
SearchFilterService, SearchFilterService,
SearchFixedFilterService, SearchFixedFilterService,
ConfigurationSearchPageGuard, ConfigurationSearchPageGuard,
SearchFilterService,
SearchConfigurationService SearchConfigurationService
], ],
entryComponents: [ entryComponents: [

View File

@@ -19,6 +19,7 @@ import { Observable } from 'rxjs/internal/Observable';
import { FindAllOptions } from './request.models'; import { FindAllOptions } from './request.models';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list'; import { PaginatedList } from './paginated-list';
import { SearchParam } from '../cache/models/search-param.model';
@Injectable() @Injectable()
export class CollectionDataService extends ComColDataService<Collection> { export class CollectionDataService extends ComColDataService<Collection> {
@@ -40,6 +41,36 @@ export class CollectionDataService extends ComColDataService<Collection> {
super(); super();
} }
/**
* Get all collections the user is authorized to submit to
*
* @param options The [[FindAllOptions]] object
* @return Observable<RemoteData<PaginatedList<Collection>>>
* collection list
*/
getAuthorizedCollection(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
const searchHref = 'findAuthorized';
return this.searchBy(searchHref, options).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
}
/**
* Get all collections the user is authorized to submit to, by community
*
* @param communityId The community id
* @param options The [[FindAllOptions]] object
* @return Observable<RemoteData<PaginatedList<Collection>>>
* collection list
*/
getAuthorizedCollectionByCommunity(communityId: string, options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
const searchHref = 'findAuthorizedByCommunity';
options.searchParams = [new SearchParam('uuid', communityId)];
return this.searchBy(searchHref, options).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
}
/** /**
* Find whether there is a collection whom user has authorization to submit to * Find whether there is a collection whom user has authorization to submit to
* *

View File

@@ -3,7 +3,7 @@ import { HttpHeaders } from '@angular/common/http';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable, race as observableRace } from 'rxjs'; import { Observable, race as observableRace } from 'rxjs';
import { filter, find, map, mergeMap, take } from 'rxjs/operators'; import { filter, map, mergeMap, take } from 'rxjs/operators';
import { cloneDeep, remove } from 'lodash'; import { cloneDeep, remove } from 'lodash';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
@@ -262,12 +262,13 @@ export class RequestService {
*/ */
private clearRequestsOnTheirWayToTheStore(request: GetRequest) { private clearRequestsOnTheirWayToTheStore(request: GetRequest) {
this.getByHref(request.href).pipe( this.getByHref(request.href).pipe(
find((re: RequestEntry) => hasValue(re))) filter((re: RequestEntry) => hasValue(re)),
.subscribe((re: RequestEntry) => { take(1)
if (!re.responsePending) { ).subscribe((re: RequestEntry) => {
remove(this.requestsOnTheirWayToTheStore, (item) => item === request.href); if (!re.responsePending) {
} remove(this.requestsOnTheirWayToTheStore, (item) => item === request.href);
}); }
});
} }
/** /**

View File

@@ -128,7 +128,10 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
// Iterate over all workspaceitem's sections // Iterate over all workspaceitem's sections
Object.keys(item.sections) Object.keys(item.sections)
.forEach((sectionId) => { .forEach((sectionId) => {
if (typeof item.sections[sectionId] === 'object' && isNotEmpty(item.sections[sectionId])) { if (typeof item.sections[sectionId] === 'object' && (isNotEmpty(item.sections[sectionId]) &&
// When Upload section is disabled, add to submission only if there are files
(!item.sections[sectionId].hasOwnProperty('files') || isNotEmpty((item.sections[sectionId] as any).files)))) {
const normalizedSectionData = Object.create({}); const normalizedSectionData = Object.create({});
// Iterate over all sections property // Iterate over all sections property
Object.keys(item.sections[sectionId]) Object.keys(item.sections[sectionId])

View File

@@ -8,3 +8,14 @@
background-image: none !important; background-image: none !important;
line-height: 1.5; line-height: 1.5;
} }
.navbar ::ng-deep {
a {
color: $header-icon-color;
&:hover, &focus {
color: darken($header-icon-color, 15%);
}
}
}

View File

@@ -2,6 +2,7 @@ import { isObject, uniqueId } from 'lodash';
import { hasValue, isNotEmpty } from '../../empty.util'; import { hasValue, isNotEmpty } from '../../empty.util';
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
import { ConfidenceType } from '../../../core/integration/models/confidence-type'; import { ConfidenceType } from '../../../core/integration/models/confidence-type';
import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
export interface ChipsItemIcon { export interface ChipsItemIcon {
metadata: string; metadata: string;
@@ -62,7 +63,7 @@ export class ChipsItem {
if (this._item.hasOwnProperty(icon.metadata) if (this._item.hasOwnProperty(icon.metadata)
&& (((typeof this._item[icon.metadata] === 'string') && hasValue(this._item[icon.metadata])) && (((typeof this._item[icon.metadata] === 'string') && hasValue(this._item[icon.metadata]))
|| (this._item[icon.metadata] as FormFieldMetadataValueObject).hasValue()) || (this._item[icon.metadata] as FormFieldMetadataValueObject).hasValue())
&& !(this._item[icon.metadata] as FormFieldMetadataValueObject).hasPlaceholder()) { && !this.hasPlaceholder(this._item[icon.metadata])) {
if ((icon.visibleWhenAuthorityEmpty if ((icon.visibleWhenAuthorityEmpty
|| (this._item[icon.metadata] as FormFieldMetadataValueObject).confidence !== ConfidenceType.CF_UNSET) || (this._item[icon.metadata] as FormFieldMetadataValueObject).confidence !== ConfidenceType.CF_UNSET)
&& isNotEmpty(icon.style)) { && isNotEmpty(icon.style)) {
@@ -109,4 +110,9 @@ export class ChipsItem {
this.display = value; this.display = value;
} }
private hasPlaceholder(value: any) {
return (typeof value === 'string') ? (value === PLACEHOLDER_PARENT_METADATA) :
(value as FormFieldMetadataValueObject).hasPlaceholder()
}
} }

View File

@@ -14,7 +14,8 @@
<ng-container #componentViewContainer></ng-container> <ng-container #componentViewContainer></ng-container>
<small *ngIf="hasHint" class="text-muted" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small> <small *ngIf="hasHint && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]"> <div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate:model.validators }}</small> <small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate:model.validators }}</small>

View File

@@ -1,4 +1,7 @@
import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core'; import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core';
import { Subject } from 'rxjs';
import { isNotEmpty } from '../../../../empty.util'; import { isNotEmpty } from '../../../../empty.util';
import { DsDynamicInputModel } from './ds-dynamic-input.model'; import { DsDynamicInputModel } from './ds-dynamic-input.model';
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
@@ -16,12 +19,16 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
@serializable() separator: string; @serializable() separator: string;
@serializable() hasLanguages = false; @serializable() hasLanguages = false;
isCustomGroup = true; isCustomGroup = true;
valueUpdates: Subject<string>;
constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) { constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout); super(config, layout);
this.separator = config.separator + ' '; this.separator = config.separator + ' ';
this.valueUpdates = new Subject<string>();
this.valueUpdates.subscribe((value: string) => this.value = value);
} }
get value() { get value() {

View File

@@ -28,6 +28,7 @@ export class DsDynamicInputModel extends DynamicInputModel {
constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) { constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout); super(config, layout);
this.hint = config.hint;
this.readOnly = config.readOnly; this.readOnly = config.readOnly;
this.value = config.value; this.value = config.value;
this.language = config.language; this.language = config.language;
@@ -57,11 +58,7 @@ export class DsDynamicInputModel extends DynamicInputModel {
} }
get hasLanguages(): boolean { get hasLanguages(): boolean {
if (this.languageCodes && this.languageCodes.length > 1) { return this.languageCodes && this.languageCodes.length > 1;
return true;
} else {
return false;
}
} }
get language(): string { get language(): string {

View File

@@ -1,5 +1,5 @@
import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core'; import { DynamicFormControlLayout, DynamicFormGroupModel, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model'; import { DsDynamicInputModel } from './ds-dynamic-input.model';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core/src/model/form-group/dynamic-form-group.model'; import { DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core/src/model/form-group/dynamic-form-group.model';
import { LanguageCode } from '../../models/form-field-language-value.model'; import { LanguageCode } from '../../models/form-field-language-value.model';
@@ -12,6 +12,7 @@ export interface DsDynamicQualdropModelConfig extends DynamicFormGroupModelConfi
languageCodes?: LanguageCode[]; languageCodes?: LanguageCode[];
language?: string; language?: string;
readOnly: boolean; readOnly: boolean;
hint?: string;
} }
export class DynamicQualdropModel extends DynamicFormGroupModel { export class DynamicQualdropModel extends DynamicFormGroupModel {
@@ -20,6 +21,7 @@ export class DynamicQualdropModel extends DynamicFormGroupModel {
@serializable() languageUpdates: Subject<string>; @serializable() languageUpdates: Subject<string>;
@serializable() hasLanguages = false; @serializable() hasLanguages = false;
@serializable() readOnly: boolean; @serializable() readOnly: boolean;
@serializable() hint: string;
isCustomGroup = true; isCustomGroup = true;
constructor(config: DsDynamicQualdropModelConfig, layout?: DynamicFormControlLayout) { constructor(config: DsDynamicQualdropModelConfig, layout?: DynamicFormControlLayout) {
@@ -33,6 +35,8 @@ export class DynamicQualdropModel extends DynamicFormGroupModel {
this.languageUpdates.subscribe((lang: string) => { this.languageUpdates.subscribe((lang: string) => {
this.language = lang; this.language = lang;
}); });
this.hint = config.hint;
} }
get value() { get value() {

View File

@@ -20,11 +20,10 @@
[disabled]="isInputDisabled()" [disabled]="isInputDisabled()"
[placeholder]="model.placeholder | translate" [placeholder]="model.placeholder | translate"
[readonly]="model.readOnly" [readonly]="model.readOnly"
(change)="$event.preventDefault()" (change)="onChange($event)"
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
(click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();" (click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();">
(input)="onInput($event)">
</div> </div>
<!--Lookup-name, second field--> <!--Lookup-name, second field-->
@@ -40,11 +39,10 @@
[disabled]="firstInputValue.length === 0 || isInputDisabled()" [disabled]="firstInputValue.length === 0 || isInputDisabled()"
[placeholder]="model.secondPlaceholder | translate" [placeholder]="model.secondPlaceholder | translate"
[readonly]="model.readOnly" [readonly]="model.readOnly"
(change)="$event.preventDefault()" (change)="onChange($event)"
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
(click)="$event.stopPropagation(); sdRef.close();" (click)="$event.stopPropagation(); sdRef.close();">
(input)="onInput($event)">
</div> </div>
<div class="col-auto text-center"> <div class="col-auto text-center">
<button ngbDropdownAnchor <button ngbDropdownAnchor

View File

@@ -237,6 +237,12 @@ describe('Dynamic Lookup component', () => {
it('should init component properly', () => { it('should init component properly', () => {
expect(lookupComp.firstInputValue).toBe(''); expect(lookupComp.firstInputValue).toBe('');
const de = lookupFixture.debugElement.queryAll(By.css('button'));
const searchBtnEl = de[0].nativeElement;
const editBtnEl = de[1].nativeElement;
expect(searchBtnEl.disabled).toBe(true);
expect(editBtnEl.disabled).toBe(true);
expect(editBtnEl.textContent.trim()).toBe('form.edit');
}); });
it('should return search results', fakeAsync(() => { it('should return search results', fakeAsync(() => {
@@ -283,7 +289,7 @@ describe('Dynamic Lookup component', () => {
lookupComp.firstInputValue = 'test'; lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges(); lookupFixture.detectChanges();
lookupComp.onInput(new Event('input')); lookupComp.onChange(new Event('change'));
expect(lookupComp.model.value).toEqual(new FormFieldMetadataValueObject('test')) expect(lookupComp.model.value).toEqual(new FormFieldMetadataValueObject('test'))
})); }));
@@ -293,10 +299,11 @@ describe('Dynamic Lookup component', () => {
lookupComp.firstInputValue = 'test'; lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges(); lookupFixture.detectChanges();
lookupComp.onInput(new Event('input')); lookupComp.onChange(new Event('change'));
expect(lookupComp.model.value).not.toBeDefined(); expect(lookupComp.model.value).not.toBeDefined();
}); });
}); });
describe('and init model value is not empty', () => { describe('and init model value is not empty', () => {
@@ -318,6 +325,19 @@ describe('Dynamic Lookup component', () => {
it('should init component properly', () => { it('should init component properly', () => {
expect(lookupComp.firstInputValue).toBe('test'); expect(lookupComp.firstInputValue).toBe('test');
}); });
it('should have search button disabled on edit mode', () => {
lookupComp.editMode = true;
lookupFixture.detectChanges();
const de = lookupFixture.debugElement.queryAll(By.css('button'));
const searchBtnEl = de[0].nativeElement;
const saveBtnEl = de[1].nativeElement;
expect(searchBtnEl.disabled).toBe(true);
expect(saveBtnEl.disabled).toBe(false);
expect(saveBtnEl.textContent.trim()).toBe('form.save');
});
}); });
}); });
@@ -340,7 +360,14 @@ describe('Dynamic Lookup component', () => {
}); });
it('should render two input element', () => { it('should render two input element', () => {
const de = lookupFixture.debugElement.queryAll(By.css('input.form-control')); const de = lookupFixture.debugElement.queryAll(By.css('input.form-control'));
const deBtn = lookupFixture.debugElement.queryAll(By.css('button'));
const searchBtnEl = deBtn[0].nativeElement;
const editBtnEl = deBtn[1].nativeElement;
expect(de.length).toBe(2); expect(de.length).toBe(2);
expect(searchBtnEl.disabled).toBe(true);
expect(editBtnEl.disabled).toBe(true);
expect(editBtnEl.textContent.trim()).toBe('form.edit');
}); });
}); });
@@ -418,6 +445,19 @@ describe('Dynamic Lookup component', () => {
expect(lookupComp.firstInputValue).toBe('Name'); expect(lookupComp.firstInputValue).toBe('Name');
expect(lookupComp.secondInputValue).toBe('Lastname'); expect(lookupComp.secondInputValue).toBe('Lastname');
}); });
it('should have search button disabled on edit mode', () => {
lookupComp.editMode = true;
lookupFixture.detectChanges();
const de = lookupFixture.debugElement.queryAll(By.css('button'));
const searchBtnEl = de[0].nativeElement;
const saveBtnEl = de[1].nativeElement;
expect(searchBtnEl.disabled).toBe(true);
expect(saveBtnEl.disabled).toBe(false);
expect(saveBtnEl.textContent.trim()).toBe('form.save');
});
}); });
}); });
}); });

View File

@@ -123,6 +123,15 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
} }
} }
protected updateModel(value) {
this.group.markAsDirty();
this.model.valueUpdates.next(value);
this.setInputsValue(value);
this.change.emit(value);
this.optionsList = null;
this.pageInfo = null;
}
public formatItemForInput(item: any, field: number): string { public formatItemForInput(item: any, field: number): string {
if (isUndefined(item) || isNull(item)) { if (isUndefined(item) || isNull(item)) {
return ''; return '';
@@ -159,7 +168,7 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
} }
public isSearchDisabled() { public isSearchDisabled() {
return isEmpty(this.firstInputValue); return isEmpty(this.firstInputValue) || this.editMode;
} }
public onBlurEvent(event: Event) { public onBlurEvent(event: Event) {
@@ -170,12 +179,13 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
this.focus.emit(event); this.focus.emit(event);
} }
public onInput(event) { public onChange(event) {
event.preventDefault();
if (!this.model.authorityOptions.closed) { if (!this.model.authorityOptions.closed) {
if (isNotEmpty(this.getCurrentValue())) { if (isNotEmpty(this.getCurrentValue())) {
const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue()); const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue());
if (!this.editMode) { if (!this.editMode) {
this.onSelect(currentValue); this.updateModel(currentValue);
} }
} else { } else {
this.remove(); this.remove();
@@ -191,12 +201,7 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
} }
public onSelect(event) { public onSelect(event) {
this.group.markAsDirty(); this.updateModel(event);
this.model.valueUpdates.next(event);
this.setInputsValue(event);
this.change.emit(event);
this.optionsList = null;
this.pageInfo = null;
} }
public openChange(isOpened: boolean) { public openChange(isOpened: boolean) {
@@ -219,7 +224,7 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
display: this.getCurrentValue(), display: this.getCurrentValue(),
value: this.getCurrentValue() value: this.getCurrentValue()
}); });
this.onSelect(newValue); this.updateModel(newValue);
} else { } else {
this.remove(); this.remove();
} }

View File

@@ -129,9 +129,11 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent
|| this.selectedChipItem.item[model.name].value === PLACEHOLDER_PARENT_METADATA) || this.selectedChipItem.item[model.name].value === PLACEHOLDER_PARENT_METADATA)
? null ? null
: this.selectedChipItem.item[model.name]; : this.selectedChipItem.item[model.name];
if (isNotNull(value)) {
model.valueUpdates.next(this.formBuilderService.isInputModel(model) ? value.value : value); const nextValue = (this.formBuilderService.isInputModel(model) && isNotNull(value) && (typeof value !== 'string')) ?
} value.value : value;
model.valueUpdates.next(nextValue);
}); });
}); });
@@ -229,7 +231,7 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent
flatMap((valueModel) => { flatMap((valueModel) => {
const returnList: Array<Observable<any>> = []; const returnList: Array<Observable<any>> = [];
valueModel.forEach((valueObj) => { valueModel.forEach((valueObj) => {
const returnObj = Object.keys(valueObj).map((fieldName) => { const returnObj = Object.keys(valueObj).map((fieldName) => {
let return$: Observable<any>; let return$: Observable<any>;
if (isObject(valueObj[fieldName]) && valueObj[fieldName].hasAuthority() && isNotEmpty(valueObj[fieldName].authority)) { if (isObject(valueObj[fieldName]) && valueObj[fieldName].hasAuthority() && isNotEmpty(valueObj[fieldName].authority)) {
const fieldId = fieldName.replace(/\./g, '_'); const fieldId = fieldName.replace(/\./g, '_');
@@ -253,7 +255,7 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent
} else { } else {
return$ = observableOf(valueObj[fieldName]); return$ = observableOf(valueObj[fieldName]);
} }
return return$.pipe(map((entry) => ({[fieldName]: entry}))); return return$.pipe(map((entry) => ({ [fieldName]: entry })));
}); });
returnList.push(combineLatest(returnObj)); returnList.push(combineLatest(returnObj));

View File

@@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } fro
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { catchError, first, tap } from 'rxjs/operators'; import { catchError, distinctUntilChanged, first, tap } from 'rxjs/operators';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
import { import {
DynamicFormControlComponent, DynamicFormControlComponent,
@@ -71,7 +71,13 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp
} }
this.pageInfo = object.pageInfo; this.pageInfo = object.pageInfo;
this.cdr.detectChanges(); this.cdr.detectChanges();
}) });
this.group.get(this.model.id).valueChanges.pipe(distinctUntilChanged())
.subscribe((value) => {
this.setCurrentValue(value);
});
} }
inputFormatter = (x: AuthorityValue): string => x.display || x.value; inputFormatter = (x: AuthorityValue): string => x.display || x.value;

View File

@@ -28,7 +28,8 @@
aria-hidden="true" aria-hidden="true"
[authorityValue]="currentValue" [authorityValue]="currentValue"
(whenClickOnConfidenceNotAccepted)="whenClickOnConfidenceNotAccepted($event)"></i> (whenClickOnConfidenceNotAccepted)="whenClickOnConfidenceNotAccepted($event)"></i>
<input class="form-control" <input #instance="ngbTypeahead"
class="form-control"
[attr.autoComplete]="model.autoComplete" [attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages" [class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id" [dynamicId]="bindId && model.id"

View File

@@ -156,7 +156,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => {
inputElement.value = 'test value'; inputElement.value = 'test value';
inputElement.dispatchEvent(new Event('input')); inputElement.dispatchEvent(new Event('input'));
expect((typeaheadComp.model as any).value).toEqual(new FormFieldMetadataValueObject('test value')) expect(typeaheadComp.inputValue).toEqual(new FormFieldMetadataValueObject('test value'))
}); });
@@ -173,19 +173,56 @@ describe('DsDynamicTypeaheadComponent test suite', () => {
}); });
it('should emit blur Event onBlur', () => { it('should emit blur Event onBlur when popup is closed', () => {
spyOn(typeaheadComp.blur, 'emit'); spyOn(typeaheadComp.blur, 'emit');
spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false);
typeaheadComp.onBlur(new Event('blur')); typeaheadComp.onBlur(new Event('blur'));
expect(typeaheadComp.blur.emit).toHaveBeenCalled(); expect(typeaheadComp.blur.emit).toHaveBeenCalled();
}); });
it('should emit change Event onBlur when AuthorityOptions.closed is false', () => { it('should not emit blur Event onBlur when popup is opened', () => {
spyOn(typeaheadComp.blur, 'emit');
spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(true);
const input = typeaheadFixture.debugElement.query(By.css('input'));
input.nativeElement.blur();
expect(typeaheadComp.blur.emit).not.toHaveBeenCalled();
});
it('should emit change Event onBlur when AuthorityOptions.closed is false and inputValue is changed', () => {
typeaheadComp.inputValue = 'test value'; typeaheadComp.inputValue = 'test value';
typeaheadFixture.detectChanges(); typeaheadFixture.detectChanges();
spyOn(typeaheadComp.blur, 'emit'); spyOn(typeaheadComp.blur, 'emit');
spyOn(typeaheadComp.change, 'emit'); spyOn(typeaheadComp.change, 'emit');
typeaheadComp.onBlur(new Event('blur')); spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false);
// expect(typeaheadComp.change.emit).toHaveBeenCalled(); typeaheadComp.onBlur(new Event('blur', ));
expect(typeaheadComp.change.emit).toHaveBeenCalled();
expect(typeaheadComp.blur.emit).toHaveBeenCalled();
});
it('should not emit change Event onBlur when AuthorityOptions.closed is false and inputValue is not changed', () => {
typeaheadComp.inputValue = 'test value';
typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
(typeaheadComp.model as any).value = 'test value';
typeaheadFixture.detectChanges();
spyOn(typeaheadComp.blur, 'emit');
spyOn(typeaheadComp.change, 'emit');
spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false);
typeaheadComp.onBlur(new Event('blur', ));
expect(typeaheadComp.change.emit).not.toHaveBeenCalled();
expect(typeaheadComp.blur.emit).toHaveBeenCalled();
});
it('should not emit change Event onBlur when AuthorityOptions.closed is false and inputValue is null', () => {
typeaheadComp.inputValue = null;
typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
(typeaheadComp.model as any).value = 'test value';
typeaheadFixture.detectChanges();
spyOn(typeaheadComp.blur, 'emit');
spyOn(typeaheadComp.change, 'emit');
spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false);
typeaheadComp.onBlur(new Event('blur', ));
expect(typeaheadComp.change.emit).not.toHaveBeenCalled();
expect(typeaheadComp.blur.emit).toHaveBeenCalled(); expect(typeaheadComp.blur.emit).toHaveBeenCalled();
}); });

View File

@@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { import {
@@ -8,14 +8,13 @@ import {
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { catchError, debounceTime, distinctUntilChanged, filter, map, merge, switchMap, tap } from 'rxjs/operators'; import { catchError, debounceTime, distinctUntilChanged, filter, map, merge, switchMap, tap } from 'rxjs/operators';
import { Observable, of as observableOf, Subject } from 'rxjs'; import { Observable, of as observableOf, Subject } from 'rxjs';
import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { DynamicTypeaheadModel } from './dynamic-typeahead.model'; import { DynamicTypeaheadModel } from './dynamic-typeahead.model';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
import { isEmpty, isNotEmpty } from '../../../../../empty.util'; import { isEmpty, isNotEmpty, isNotNull } from '../../../../../empty.util';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { ConfidenceType } from '../../../../../../core/integration/models/confidence-type'; import { ConfidenceType } from '../../../../../../core/integration/models/confidence-type';
@Component({ @Component({
@@ -32,6 +31,8 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp
@Output() change: EventEmitter<any> = new EventEmitter<any>(); @Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>(); @Output() focus: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('instance') instance: NgbTypeahead;
searching = false; searching = false;
searchOptions: IntegrationSearchOptions; searchOptions: IntegrationSearchOptions;
searchFailed = false; searchFailed = false;
@@ -105,16 +106,26 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp
onInput(event) { onInput(event) {
if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) { if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) {
this.inputValue = new FormFieldMetadataValueObject(event.target.value); this.inputValue = new FormFieldMetadataValueObject(event.target.value);
this.model.valueUpdates.next(this.inputValue);
} }
} }
onBlur(event: Event) { onBlur(event: Event) {
if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) { if (!this.instance.isPopupOpen()) {
this.change.emit(this.inputValue); if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) {
this.inputValue = null; if (isNotNull(this.inputValue) && this.model.value !== this.inputValue) {
this.model.valueUpdates.next(this.inputValue);
this.change.emit(this.inputValue);
}
this.inputValue = null;
}
this.blur.emit(event);
} else {
// prevent on blur propagation if typeahed suggestions are showed
event.preventDefault();
event.stopImmediatePropagation();
// set focus on input again, this is to avoid to lose changes when no suggestion is selected
(event.target as HTMLInputElement).focus();
} }
this.blur.emit(event);
} }
onChange(event: Event) { onChange(event: Event) {
@@ -141,4 +152,5 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp
this.click$.next(this.formatter(this.currentValue)); this.click$.next(this.formatter(this.currentValue));
} }
} }
} }

View File

@@ -47,6 +47,7 @@ export class ConcatFieldParser extends FieldParser {
const input1ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_FIRST_INPUT_SUFFIX, label, false, false); const input1ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_FIRST_INPUT_SUFFIX, label, false, false);
const input2ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_SECOND_INPUT_SUFFIX, label, true, false); const input2ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_SECOND_INPUT_SUFFIX, label, true, false);
input2ModelConfig.hint = '&nbsp;';
if (this.configData.mandatory) { if (this.configData.mandatory) {
input1ModelConfig.required = true; input1ModelConfig.required = true;

View File

@@ -190,6 +190,8 @@ export abstract class FieldParser {
controlModel.placeholder = this.configData.label; controlModel.placeholder = this.configData.label;
controlModel.hint = this.configData.hints;
if (this.configData.mandatory && setErrors) { if (this.configData.mandatory && setErrors) {
this.markAsRequired(controlModel); this.markAsRequired(controlModel);
} }

View File

@@ -24,6 +24,7 @@ export class OneboxFieldParser extends FieldParser {
const clsGroup = { const clsGroup = {
element: { element: {
control: 'form-row', control: 'form-row',
hint: 'ds-form-qualdrop-hint'
} }
}; };
@@ -54,8 +55,10 @@ export class OneboxFieldParser extends FieldParser {
inputSelectGroup.id = newId.replace(/\./g, '_') + QUALDROP_GROUP_SUFFIX; inputSelectGroup.id = newId.replace(/\./g, '_') + QUALDROP_GROUP_SUFFIX;
inputSelectGroup.group = []; inputSelectGroup.group = [];
inputSelectGroup.legend = this.configData.label; inputSelectGroup.legend = this.configData.label;
inputSelectGroup.hint = this.configData.hints;
const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label); const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label);
selectModelConfig.hint = null;
this.setOptions(selectModelConfig); this.setOptions(selectModelConfig);
if (isNotEmpty(fieldValue)) { if (isNotEmpty(fieldValue)) {
selectModelConfig.value = fieldValue.metadata; selectModelConfig.value = fieldValue.metadata;
@@ -63,6 +66,7 @@ export class OneboxFieldParser extends FieldParser {
inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect)); inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect));
const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, true); const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, true);
inputModelConfig.hint = null;
this.setValues(inputModelConfig, fieldValue); this.setValues(inputModelConfig, fieldValue);
inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly; inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly;

View File

@@ -42,3 +42,8 @@
.right-addon input { .right-addon input {
padding-right: $spacer * 2.25; padding-right: $spacer * 2.25;
} }
.ds-form-qualdrop-hint {
top: -$spacer;
position: relative;
}

View File

@@ -2,7 +2,7 @@
<a [class.disabled]="!(object.workflowitem | async)?.hasSucceeded" <a [class.disabled]="!(object.workflowitem | async)?.hasSucceeded"
class="btn btn-primary mt-1 mb-3" class="btn btn-primary mt-1 mb-3"
ngbTooltip="{{'submission.workflow.tasks.claimed.edit_help' | translate}}" ngbTooltip="{{'submission.workflow.tasks.claimed.edit_help' | translate}}"
[routerLink]="['/workflowitems/' + (object.workflowitem | async)?.payload.id + '/' + object.id + '/edit']" [routerLink]="['/workflowitems/' + (object.workflowitem | async)?.payload.id + '/edit']"
role="button"> role="button">
<i class="fa fa-edit"></i> {{'submission.workflow.tasks.claimed.edit' | translate}} <i class="fa fa-edit"></i> {{'submission.workflow.tasks.claimed.edit' | translate}}
</a> </a>

View File

@@ -15,11 +15,11 @@
<ng-container *ngVar="(bitstreams$ | async) as bitstreams"> <ng-container *ngVar="(bitstreams$ | async) as bitstreams">
<ds-metadata-field-wrapper [label]="('item.page.files' | translate)"> <ds-metadata-field-wrapper [label]="('item.page.files' | translate)">
<div *ngIf="bitstreams?.length > 0" class="file-section"> <div *ngIf="bitstreams?.length > 0" class="file-section">
<a *ngFor="let file of bitstreams; let last=last;" [href]="file?.content" target="_blank" [download]="file?.name"> <button class="btn btn-link" *ngFor="let file of bitstreams; let last=last;" (click)="downloadBitstreamFile(file?.uuid)">
<span>{{file?.name}}</span> <span>{{file?.name}}</span>
<span>({{(file?.sizeBytes) | dsFileSize }})</span> <span>({{(file?.sizeBytes) | dsFileSize }})</span>
<span *ngIf="!last" innerHTML="{{separator}}"></span> <span *ngIf="!last" innerHTML="{{separator}}"></span>
</a> </button>
</div> </div>
<ng-container *ngIf="bitstreams?.length === 0"> <ng-container *ngIf="bitstreams?.length === 0">
<span class="text-muted">{{('mydspace.results.no-files' | translate)}}</span> <span class="text-muted">{{('mydspace.results.no-files' | translate)}}</span>

View File

@@ -12,10 +12,20 @@ import { MockTranslateLoader } from '../../../mocks/mock-translate-loader';
import { ItemDetailPreviewFieldComponent } from './item-detail-preview-field/item-detail-preview-field.component'; import { ItemDetailPreviewFieldComponent } from './item-detail-preview-field/item-detail-preview-field.component';
import { FileSizePipe } from '../../../utils/file-size-pipe'; import { FileSizePipe } from '../../../utils/file-size-pipe';
import { VarDirective } from '../../../utils/var.directive'; import { VarDirective } from '../../../utils/var.directive';
import { FileService } from '../../../../core/shared/file.service';
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
import { HALEndpointServiceStub } from '../../../testing/hal-endpoint-service-stub';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { PaginatedList } from '../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../core/data/paginated-list';
import { PageInfo } from '../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../core/shared/page-info.model';
function getMockFileService(): FileService {
return jasmine.createSpyObj('FileService', {
downloadFile: jasmine.createSpy('downloadFile'),
getFileNameFromResponseContentDisposition: jasmine.createSpy('getFileNameFromResponseContentDisposition')
});
}
let component: ItemDetailPreviewComponent; let component: ItemDetailPreviewComponent;
let fixture: ComponentFixture<ItemDetailPreviewComponent>; let fixture: ComponentFixture<ItemDetailPreviewComponent>;
@@ -62,6 +72,10 @@ describe('ItemDetailPreviewComponent', () => {
}), }),
], ],
declarations: [ItemDetailPreviewComponent, ItemDetailPreviewFieldComponent, TruncatePipe, FileSizePipe, VarDirective], declarations: [ItemDetailPreviewComponent, ItemDetailPreviewFieldComponent, TruncatePipe, FileSizePipe, VarDirective],
providers: [
{ provide: FileService, useValue: getMockFileService() },
{ provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') }
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemDetailPreviewComponent, { }).overrideComponent(ItemDetailPreviewComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default } set: { changeDetection: ChangeDetectionStrategy.Default }

View File

@@ -1,12 +1,15 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
import { fadeInOut } from '../../../animations/fade'; import { fadeInOut } from '../../../animations/fade';
import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Bitstream } from '../../../../core/shared/bitstream.model';
import { MyDSpaceResult } from '../../../../+my-dspace-page/my-dspace-result.model'; import { MyDSpaceResult } from '../../../../+my-dspace-page/my-dspace-result.model';
import { FileService } from '../../../../core/shared/file.service';
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
/** /**
* This component show metadata for the given item object in the detail view. * This component show metadata for the given item object in the detail view.
@@ -54,6 +57,16 @@ export class ItemDetailPreviewComponent {
*/ */
public thumbnail$: Observable<Bitstream>; public thumbnail$: Observable<Bitstream>;
/**
* Initialize instance variables
*
* @param {FileService} fileService
* @param {HALEndpointService} halService
*/
constructor(private fileService: FileService,
private halService: HALEndpointService) {
}
/** /**
* Initialize all instance variables * Initialize all instance variables
*/ */
@@ -62,4 +75,15 @@ export class ItemDetailPreviewComponent {
this.bitstreams$ = this.item.getFiles(); this.bitstreams$ = this.item.getFiles();
} }
/**
* Perform bitstream download
*/
public downloadBitstreamFile(uuid: string) {
this.halService.getEndpoint('bitstreams').pipe(
first())
.subscribe((url) => {
const fileUrl = `${url}/${uuid}/content`;
this.fileService.downloadFile(fileUrl);
});
}
} }

View File

@@ -10,10 +10,10 @@
class="btn btn-outline-primary" class="btn btn-outline-primary"
(blur)="onClose()" (blur)="onClose()"
(click)="onClose()" (click)="onClose()"
[disabled]="(disabled$ | async)" [disabled]="(disabled$ | async) || (processingChange$ | async)"
ngbDropdownToggle> ngbDropdownToggle>
<span *ngIf="(disabled$ | async)"><i class='fas fa-circle-notch fa-spin'></i></span> <span *ngIf="(processingChange$ | async)"><i class='fas fa-circle-notch fa-spin'></i></span>
<span *ngIf="!(disabled$ | async)">{{ selectedCollectionName$ | async }}</span> <span *ngIf="!(processingChange$ | async)">{{ selectedCollectionName$ | async }}</span>
</button> </button>
<div ngbDropdownMenu <div ngbDropdownMenu

View File

@@ -1,10 +1,4 @@
import { import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement, SimpleChange } from '@angular/core';
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
DebugElement,
SimpleChange
} from '@angular/core';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -14,12 +8,10 @@ import { filter } from 'rxjs/operators';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { cold } from 'jasmine-marbles';
import { SubmissionServiceStub } from '../../../shared/testing/submission-service-stub'; import { SubmissionServiceStub } from '../../../shared/testing/submission-service-stub';
import { import { mockSubmissionId, mockSubmissionRestResponse } from '../../../shared/mocks/mock-submission';
mockSubmissionId,
mockSubmissionRestResponse
} from '../../../shared/mocks/mock-submission';
import { SubmissionService } from '../../submission.service'; import { SubmissionService } from '../../submission.service';
import { SubmissionFormCollectionComponent } from './submission-form-collection.component'; import { SubmissionFormCollectionComponent } from './submission-form-collection.component';
import { CommunityDataService } from '../../../core/data/community-data.service'; import { CommunityDataService } from '../../../core/data/community-data.service';
@@ -27,16 +19,35 @@ import { SubmissionJsonPatchOperationsService } from '../../../core/submission/s
import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service-stub'; import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service-stub';
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { Community } from '../../../core/shared/community.model';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { import { createTestComponent } from '../../../shared/testing/utils';
createTestComponent import { CollectionDataService } from '../../../core/data/collection-data.service';
} from '../../../shared/testing/utils';
import { cold } from 'jasmine-marbles'; const subcommunities = [Object.assign(new Community(), {
import { SearchResult } from '../../../+search-page/search-result.model'; name: 'SubCommunity 1',
import { SearchService } from '../../../+search-page/search-service/search.service'; id: '123456789-1',
import { RemoteData } from '../../../core/data/remote-data'; metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'SubCommunity 1'
}]
}),
Object.assign(new Community(), {
name: 'SubCommunity 1',
id: '123456789s-1',
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'SubCommunity 1'
}]
})
];
const mockCommunity1Collection1 = Object.assign(new Collection(), { const mockCommunity1Collection1 = Object.assign(new Collection(), {
name: 'Community 1-Collection 1', name: 'Community 1-Collection 1',
@@ -82,20 +93,54 @@ const mockCommunity2Collection2 = Object.assign(new Collection(), {
}] }]
}); });
const collectionResults = [mockCommunity1Collection1, mockCommunity1Collection2, mockCommunity2Collection1, mockCommunity2Collection2].map((collection: Collection) => Object.assign(new SearchResult<Collection>(), { indexableObject: collection })); const mockCommunity = Object.assign(new Community(), {
const searchService = { name: 'Community 1',
search: () => { id: '123456789-1',
return observableOf(new RemoteData(true, true, true, metadata: [
undefined, new PaginatedList(new PageInfo(), collectionResults))) {
} key: 'dc.title',
}; language: 'en_US',
value: 'Community 1'
}],
collections: observableOf(new RemoteData(true, true, true,
undefined, new PaginatedList(new PageInfo(), [mockCommunity1Collection1, mockCommunity1Collection2]))),
subcommunities: observableOf(new RemoteData(true, true, true,
undefined, new PaginatedList(new PageInfo(), subcommunities))),
});
const mockCommunity2 = Object.assign(new Community(), {
name: 'Community 2',
id: '123456789-2',
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Community 2'
}],
collections: observableOf(new RemoteData(true, true, true,
undefined, new PaginatedList(new PageInfo(), [mockCommunity2Collection1, mockCommunity2Collection2]))),
subcommunities: observableOf(new RemoteData(true, true, true,
undefined, new PaginatedList(new PageInfo(), []))),
});
const mockCommunity1Collection1Rd = observableOf(new RemoteData(true, true, true,
undefined, mockCommunity1Collection1));
const mockCommunityList = observableOf(new RemoteData(true, true, true,
undefined, new PaginatedList(new PageInfo(), [mockCommunity, mockCommunity2])));
const mockCommunityCollectionList = observableOf(new RemoteData(true, true, true,
undefined, new PaginatedList(new PageInfo(), [mockCommunity1Collection1, mockCommunity1Collection2])));
const mockCommunity2CollectionList = observableOf(new RemoteData(true, true, true,
undefined, new PaginatedList(new PageInfo(), [mockCommunity2Collection1, mockCommunity2Collection2])));
const mockCollectionList = [ const mockCollectionList = [
{ {
communities: [ communities: [
{ {
id: 'c0e4de93-f506-4990-a840-d406f6f2ada7', id: '123456789-1',
name: 'Submission test' name: 'Community 1'
} }
], ],
collection: { collection: {
@@ -106,8 +151,8 @@ const mockCollectionList = [
{ {
communities: [ communities: [
{ {
id: 'c0e4de93-f506-4990-a840-d406f6f2ada7', id: '123456789-1',
name: 'Submission test' name: 'Community 1'
} }
], ],
collection: { collection: {
@@ -118,8 +163,8 @@ const mockCollectionList = [
{ {
communities: [ communities: [
{ {
id: 'c0e4de93-f506-4990-a840-d406f6f2ada7', id: '123456789-2',
name: 'Submission test' name: 'Community 2'
} }
], ],
collection: { collection: {
@@ -130,8 +175,8 @@ const mockCollectionList = [
{ {
communities: [ communities: [
{ {
id: 'c0e4de93-f506-4990-a840-d406f6f2ada7', id: '123456789-2',
name: 'Submission test' name: 'Community 2'
} }
], ],
collection: { collection: {
@@ -158,6 +203,12 @@ describe('SubmissionFormCollectionComponent Component', () => {
const communityDataService: any = jasmine.createSpyObj('communityDataService', { const communityDataService: any = jasmine.createSpyObj('communityDataService', {
findAll: jasmine.createSpy('findAll') findAll: jasmine.createSpy('findAll')
}); });
const collectionDataService: any = jasmine.createSpyObj('collectionDataService', {
findById: jasmine.createSpy('findById'),
getAuthorizedCollectionByCommunity: jasmine.createSpy('getAuthorizedCollectionByCommunity')
});
const store: any = jasmine.createSpyObj('store', { const store: any = jasmine.createSpyObj('store', {
dispatch: jasmine.createSpy('dispatch'), dispatch: jasmine.createSpy('dispatch'),
select: jasmine.createSpy('select') select: jasmine.createSpy('select')
@@ -179,15 +230,12 @@ describe('SubmissionFormCollectionComponent Component', () => {
TestComponent TestComponent
], ],
providers: [ providers: [
{ { provide: CollectionDataService, useValue: collectionDataService },
provide: SubmissionJsonPatchOperationsService, { provide: SubmissionJsonPatchOperationsService, useClass: SubmissionJsonPatchOperationsServiceStub },
useClass: SubmissionJsonPatchOperationsServiceStub
},
{ provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub },
{ provide: CommunityDataService, useValue: communityDataService }, { provide: CommunityDataService, useValue: communityDataService },
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
{ provide: Store, useValue: store }, { provide: Store, useValue: store },
{ provide: SearchService, useValue: searchService },
ChangeDetectorRef, ChangeDetectorRef,
SubmissionFormCollectionComponent SubmissionFormCollectionComponent
], ],
@@ -252,17 +300,21 @@ describe('SubmissionFormCollectionComponent Component', () => {
}); });
it('should init collection list properly', () => { it('should init collection list properly', () => {
communityDataService.findAll.and.returnValue(mockCommunityList);
collectionDataService.findById.and.returnValue(mockCommunity1Collection1Rd);
collectionDataService.getAuthorizedCollectionByCommunity.and.returnValues(mockCommunityCollectionList, mockCommunity2CollectionList);
comp.ngOnChanges({ comp.ngOnChanges({
currentCollectionId: new SimpleChange(null, collectionId, true) currentCollectionId: new SimpleChange(null, collectionId, true)
}); });
expect(comp.searchListCollection$).toBeObservable(cold('(b)', { expect(comp.searchListCollection$).toBeObservable(cold('(ab)', {
a: [],
b: mockCollectionList b: mockCollectionList
})); }));
expect(comp.selectedCollectionName$).toBeObservable(cold('(ab|)', { expect(comp.selectedCollectionName$).toBeObservable(cold('(a|)', {
a: '', a: 'Community 1-Collection 1'
b: 'Community 1-Collection 1'
})); }));
}); });
@@ -394,8 +446,6 @@ class TestComponent {
definitionId = 'traditional'; definitionId = 'traditional';
submissionId = mockSubmissionId; submissionId = mockSubmissionId;
onCollectionChange = () => { onCollectionChange = () => { return; }
return;
}
} }

View File

@@ -17,13 +17,16 @@ import {
distinctUntilChanged, distinctUntilChanged,
filter, filter,
find, find,
flatMap,
map, map,
mergeMap, mergeMap,
reduce,
startWith startWith
} from 'rxjs/operators'; } from 'rxjs/operators';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { CommunityDataService } from '../../../core/data/community-data.service'; import { CommunityDataService } from '../../../core/data/community-data.service';
import { Community } from '../../../core/shared/community.model';
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
@@ -32,12 +35,8 @@ import { PaginatedList } from '../../../core/data/paginated-list';
import { SubmissionService } from '../../submission.service'; import { SubmissionService } from '../../submission.service';
import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; import { SubmissionObject } from '../../../core/submission/models/submission-object.model';
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
import { SearchService } from '../../../+search-page/search-service/search.service'; import { CollectionDataService } from '../../../core/data/collection-data.service';
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; import { FindAllOptions } from '../../../core/data/request.models';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { SearchResult } from '../../../+search-page/search-result.model';
/** /**
* An interface to represent a collection entry * An interface to represent a collection entry
@@ -95,6 +94,12 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
*/ */
public disabled$ = new BehaviorSubject<boolean>(true); public disabled$ = new BehaviorSubject<boolean>(true);
/**
* A boolean representing if a collection change operation is processing
* @type {BehaviorSubject<boolean>}
*/
public processingChange$ = new BehaviorSubject<boolean>(false);
/** /**
* The search form control * The search form control
* @type {FormControl} * @type {FormControl}
@@ -148,17 +153,17 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
* *
* @param {ChangeDetectorRef} cdr * @param {ChangeDetectorRef} cdr
* @param {CommunityDataService} communityDataService * @param {CommunityDataService} communityDataService
* @param {CollectionDataService} collectionDataService
* @param {JsonPatchOperationsBuilder} operationsBuilder * @param {JsonPatchOperationsBuilder} operationsBuilder
* @param {SubmissionJsonPatchOperationsService} operationsService * @param {SubmissionJsonPatchOperationsService} operationsService
* @param {SubmissionService} submissionService * @param {SubmissionService} submissionService
* @param {SearchService} searchService
*/ */
constructor(protected cdr: ChangeDetectorRef, constructor(protected cdr: ChangeDetectorRef,
private communityDataService: CommunityDataService, private communityDataService: CommunityDataService,
private collectionDataService: CollectionDataService,
private operationsBuilder: JsonPatchOperationsBuilder, private operationsBuilder: JsonPatchOperationsBuilder,
private operationsService: SubmissionJsonPatchOperationsService, private operationsService: SubmissionJsonPatchOperationsService,
private submissionService: SubmissionService, private submissionService: SubmissionService) {
private searchService: SearchService) {
} }
/** /**
@@ -195,57 +200,55 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
&& hasValue(changes.currentCollectionId.currentValue)) { && hasValue(changes.currentCollectionId.currentValue)) {
this.selectedCollectionId = this.currentCollectionId; this.selectedCollectionId = this.currentCollectionId;
// // @TODO replace with search/top browse endpoint this.selectedCollectionName$ = this.collectionDataService.findById(this.currentCollectionId).pipe(
// // @TODO implement community/subcommunity hierarchy find((collectionRD: RemoteData<Collection>) => isNotEmpty(collectionRD.payload)),
// const communities$ = this.communityDataService.findAll().pipe( map((collectionRD: RemoteData<Collection>) => collectionRD.payload.name)
// find((communities: RemoteData<PaginatedList<Community>>) => isNotEmpty(communities.payload)), );
// mergeMap((communities: RemoteData<PaginatedList<Community>>) => communities.payload.page));
const listCollection$: Observable<CollectionListEntry[]> = this.searchService.search( const findOptions: FindAllOptions = {
new PaginatedSearchOptions({ elementsPerPage: 1000
dsoType: DSpaceObjectType.COLLECTION, };
pagination: new PaginationComponentOptions(),
scope: 'c0e4de93-f506-4990-a840-d406f6f2ada7' // Retrieve collection list only when is the first change
}) if (changes.currentCollectionId.isFirstChange()) {
).pipe( // @TODO replace with search/top browse endpoint
getSucceededRemoteData(), // @TODO implement community/subcommunity hierarchy
map((collections: RemoteData<PaginatedList<SearchResult<Collection>>>) => collections.payload.page), const communities$ = this.communityDataService.findAll(findOptions).pipe(
filter((collectionData: Array<SearchResult<Collection>>) => isNotEmpty(collectionData)), find((communities: RemoteData<PaginatedList<Community>>) => isNotEmpty(communities.payload)),
map((collectionData: Array<SearchResult<Collection>>) => { mergeMap((communities: RemoteData<PaginatedList<Community>>) => communities.payload.page));
return collectionData.map((collection: SearchResult<Collection>) => {
return { const listCollection$ = communities$.pipe(
communities: [{ flatMap((communityData: Community) => {
id: 'c0e4de93-f506-4990-a840-d406f6f2ada7', return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid, findOptions).pipe(
name: 'Submission test' find((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending && collections.hasSucceeded),
}], mergeMap((collections: RemoteData<PaginatedList<Collection>>) => collections.payload.page),
collection: { id: collection.indexableObject.id, name: collection.indexableObject.name } filter((collectionData: Collection) => isNotEmpty(collectionData)),
map((collectionData: Collection) => ({
communities: [{ id: communityData.id, name: communityData.name }],
collection: { id: collectionData.id, name: collectionData.name }
}))
);
}),
reduce((acc: any, value: any) => [...acc, ...value], []),
startWith([])
);
const searchTerm$ = this.searchField.valueChanges.pipe(
debounceTime(200),
distinctUntilChanged(),
startWith('')
);
this.searchListCollection$ = combineLatest(searchTerm$, listCollection$).pipe(
map(([searchTerm, listCollection]) => {
this.disabled$.next(isEmpty(listCollection));
if (isEmpty(searchTerm)) {
return listCollection;
} else {
return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5);
} }
}) }));
}) }
);
this.selectedCollectionName$ = listCollection$.pipe(
map((collectionData: CollectionListEntry[]) => collectionData.find((entry: CollectionListEntry) => entry.collection.id === this.selectedCollectionId)),
filter((entry: CollectionListEntry) => hasValue(entry.collection)),
map((entry: CollectionListEntry) => entry.collection.name),
startWith('')
);
const searchTerm$ = this.searchField.valueChanges.pipe(
debounceTime(200),
distinctUntilChanged(),
startWith('')
);
this.searchListCollection$ = combineLatest(searchTerm$, listCollection$).pipe(
map(([searchTerm, listCollection]) => {
this.disabled$.next(isEmpty(listCollection));
if (isEmpty(searchTerm)) {
return listCollection;
} else {
return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5);
}
}));
} }
} }
@@ -271,7 +274,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
*/ */
onSelect(event) { onSelect(event) {
this.searchField.reset(); this.searchField.reset();
this.disabled$.next(true); this.processingChange$.next(true);
this.operationsBuilder.replace(this.pathCombiner.getPath(), event.collection.id, true); this.operationsBuilder.replace(this.pathCombiner.getPath(), event.collection.id, true);
this.subs.push(this.operationsService.jsonPatchByResourceID( this.subs.push(this.operationsService.jsonPatchByResourceID(
this.submissionService.getSubmissionObjectLinkName(), this.submissionService.getSubmissionObjectLinkName(),
@@ -283,7 +286,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
this.selectedCollectionName$ = observableOf(event.collection.name); this.selectedCollectionName$ = observableOf(event.collection.name);
this.collectionChange.emit(submissionObject[0]); this.collectionChange.emit(submissionObject[0]);
this.submissionService.changeSubmissionCollection(this.submissionId, event.collection.id); this.submissionService.changeSubmissionCollection(this.submissionId, event.collection.id);
this.disabled$.next(false); this.processingChange$.next(false);
this.cdr.detectChanges(); this.cdr.detectChanges();
}) })
); );

View File

@@ -361,7 +361,7 @@ const addError = (state: SubmissionObjectState, action: InertSectionErrorsAction
* @param state * @param state
* the current state * the current state
* @param action * @param action
* an RemoveSectionErrorsAction * a RemoveSectionErrorsAction
* @return SubmissionObjectState * @return SubmissionObjectState
* the new state, with the section's errors updated. * the new state, with the section's errors updated.
*/ */
@@ -416,7 +416,7 @@ function initSubmission(state: SubmissionObjectState, action: InitSubmissionForm
* @param state * @param state
* the current state * the current state
* @param action * @param action
* an ResetSubmissionFormAction * a ResetSubmissionFormAction
* @return SubmissionObjectState * @return SubmissionObjectState
* the new state, with the section removed. * the new state, with the section removed.
*/ */
@@ -439,7 +439,7 @@ function resetSubmission(state: SubmissionObjectState, action: ResetSubmissionFo
* @param state * @param state
* the current state * the current state
* @param action * @param action
* an CompleteInitSubmissionFormAction * a CompleteInitSubmissionFormAction
* @return SubmissionObjectState * @return SubmissionObjectState
* the new state, with the section removed. * the new state, with the section removed.
*/ */
@@ -461,7 +461,7 @@ function completeInit(state: SubmissionObjectState, action: CompleteInitSubmissi
* @param state * @param state
* the current state * the current state
* @param action * @param action
* an SaveSubmissionFormAction | SaveSubmissionSectionFormAction * a SaveSubmissionFormAction | SaveSubmissionSectionFormAction
* | SaveForLaterSubmissionFormAction | SaveAndDepositSubmissionAction * | SaveForLaterSubmissionFormAction | SaveAndDepositSubmissionAction
* @return SubmissionObjectState * @return SubmissionObjectState
* the new state, with the flag set to true. * the new state, with the flag set to true.
@@ -491,7 +491,7 @@ function saveSubmission(state: SubmissionObjectState,
* @param state * @param state
* the current state * the current state
* @param action * @param action
* an SaveSubmissionFormSuccessAction | SaveForLaterSubmissionFormSuccessAction * a SaveSubmissionFormSuccessAction | SaveForLaterSubmissionFormSuccessAction
* | SaveSubmissionSectionFormSuccessAction | SaveSubmissionFormErrorAction * | SaveSubmissionSectionFormSuccessAction | SaveSubmissionFormErrorAction
* | SaveForLaterSubmissionFormErrorAction | SaveSubmissionSectionFormErrorAction * | SaveForLaterSubmissionFormErrorAction | SaveSubmissionSectionFormErrorAction
* @return SubmissionObjectState * @return SubmissionObjectState
@@ -521,7 +521,7 @@ function completeSave(state: SubmissionObjectState,
* @param state * @param state
* the current state * the current state
* @param action * @param action
* an DepositSubmissionAction * a DepositSubmissionAction
* @return SubmissionObjectState * @return SubmissionObjectState
* the new state, with the deposit flag changed. * the new state, with the deposit flag changed.
*/ */
@@ -544,7 +544,7 @@ function startDeposit(state: SubmissionObjectState, action: DepositSubmissionAct
* @param state * @param state
* the current state * the current state
* @param action * @param action
* an DepositSubmissionSuccessAction or DepositSubmissionErrorAction * a DepositSubmissionSuccessAction or a DepositSubmissionErrorAction
* @return SubmissionObjectState * @return SubmissionObjectState
* the new state, with the deposit flag changed. * the new state, with the deposit flag changed.
*/ */
@@ -586,7 +586,7 @@ function changeCollection(state: SubmissionObjectState, action: ChangeSubmission
* @param state * @param state
* the current state * the current state
* @param action * @param action
* an SetActiveSectionAction * a SetActiveSectionAction
* @return SubmissionObjectState * @return SubmissionObjectState
* the new state, with the active section. * the new state, with the active section.
*/ */
@@ -676,7 +676,7 @@ function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDa
* @param state * @param state
* the current state * the current state
* @param action * @param action
* an DisableSectionAction * a DisableSectionAction
* @param enabled * @param enabled
* enabled or disabled section. * enabled or disabled section.
* @return SubmissionObjectState * @return SubmissionObjectState
@@ -705,7 +705,7 @@ function changeSectionState(state: SubmissionObjectState, action: EnableSectionA
* @param state * @param state
* the current state * the current state
* @param action * @param action
* an SectionStatusChangeAction * a SectionStatusChangeAction
* @return SubmissionObjectState * @return SubmissionObjectState
* the new state, with the section new validity status. * the new state, with the section new validity status.
*/ */
@@ -769,7 +769,7 @@ function newFile(state: SubmissionObjectState, action: NewUploadedFileAction): S
* @param state * @param state
* the current state * the current state
* @param action * @param action
* a EditFileDataAction action * an EditFileDataAction action
* @return SubmissionObjectState * @return SubmissionObjectState
* the new state, with the edited file. * the new state, with the edited file.
*/ */

View File

@@ -64,6 +64,12 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
*/ */
public isLoading = true; public isLoading = true;
/**
* A map representing all field on their way to be removed
* @type {Map}
*/
protected fieldsOnTheirWayToBeRemoved: Map<string, number[]> = new Map();
/** /**
* The form config * The form config
* @type {SubmissionFormsModel} * @type {SubmissionFormsModel}
@@ -295,6 +301,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
}), }),
distinctUntilChanged()) distinctUntilChanged())
.subscribe((sectionState: SubmissionSectionObject) => { .subscribe((sectionState: SubmissionSectionObject) => {
this.fieldsOnTheirWayToBeRemoved = new Map();
this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors); this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors);
}) })
) )
@@ -348,11 +355,24 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
* the [[DynamicFormControlEvent]] emitted * the [[DynamicFormControlEvent]] emitted
*/ */
onRemove(event: DynamicFormControlEvent): void { onRemove(event: DynamicFormControlEvent): void {
const fieldId = this.formBuilderService.getId(event.model);
const fieldIndex = this.formOperationsService.getArrayIndexFromEvent(event);
// Keep track that this field will be removed
if (this.fieldsOnTheirWayToBeRemoved.has(fieldId)) {
const indexes = this.fieldsOnTheirWayToBeRemoved.get(fieldId);
indexes.push(fieldIndex);
this.fieldsOnTheirWayToBeRemoved.set(fieldId, indexes);
} else {
this.fieldsOnTheirWayToBeRemoved.set(fieldId, [fieldIndex]);
}
this.formOperationsService.dispatchOperationsFromEvent( this.formOperationsService.dispatchOperationsFromEvent(
this.pathCombiner, this.pathCombiner,
event, event,
this.previousValue, this.previousValue,
this.hasStoredValue(this.formBuilderService.getId(event.model), this.formOperationsService.getArrayIndexFromEvent(event))); this.hasStoredValue(fieldId, fieldIndex));
} }
/** /**
@@ -365,9 +385,23 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
*/ */
hasStoredValue(fieldId, index): boolean { hasStoredValue(fieldId, index): boolean {
if (isNotEmpty(this.sectionData.data)) { if (isNotEmpty(this.sectionData.data)) {
return this.sectionData.data.hasOwnProperty(fieldId) && isNotEmpty(this.sectionData.data[fieldId][index]); return this.sectionData.data.hasOwnProperty(fieldId) &&
isNotEmpty(this.sectionData.data[fieldId][index]) &&
!this.isFieldToRemove(fieldId, index);
} else { } else {
return false; return false;
} }
} }
/**
* Check if the specified field is on the way to be removed
*
* @param fieldId
* the section data retrieved from the serverù
* @param index
* the section data retrieved from the server
*/
isFieldToRemove(fieldId, index) {
return this.fieldsOnTheirWayToBeRemoved.has(fieldId) && this.fieldsOnTheirWayToBeRemoved.get(fieldId).includes(index);
}
} }

View File

@@ -155,14 +155,14 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
filter((submissionObject: SubmissionObjectEntry) => isUndefined(this.collectionId) || this.collectionId !== submissionObject.collection), filter((submissionObject: SubmissionObjectEntry) => isUndefined(this.collectionId) || this.collectionId !== submissionObject.collection),
tap((submissionObject: SubmissionObjectEntry) => this.collectionId = submissionObject.collection), tap((submissionObject: SubmissionObjectEntry) => this.collectionId = submissionObject.collection),
flatMap((submissionObject: SubmissionObjectEntry) => this.collectionDataService.findById(submissionObject.collection)), flatMap((submissionObject: SubmissionObjectEntry) => this.collectionDataService.findById(submissionObject.collection)),
find((rd: RemoteData<Collection>) => isNotUndefined((rd.payload))), filter((rd: RemoteData<Collection>) => isNotUndefined((rd.payload))),
tap((collectionRemoteData: RemoteData<Collection>) => this.collectionName = collectionRemoteData.payload.name), tap((collectionRemoteData: RemoteData<Collection>) => this.collectionName = collectionRemoteData.payload.name),
flatMap((collectionRemoteData: RemoteData<Collection>) => { flatMap((collectionRemoteData: RemoteData<Collection>) => {
return this.resourcePolicyService.findByHref( return this.resourcePolicyService.findByHref(
(collectionRemoteData.payload as any)._links.defaultAccessConditions (collectionRemoteData.payload as any)._links.defaultAccessConditions
); );
}), }),
find((defaultAccessConditionsRemoteData: RemoteData<ResourcePolicy>) => filter((defaultAccessConditionsRemoteData: RemoteData<ResourcePolicy>) =>
defaultAccessConditionsRemoteData.hasSucceeded), defaultAccessConditionsRemoteData.hasSucceeded),
tap((defaultAccessConditionsRemoteData: RemoteData<ResourcePolicy>) => { tap((defaultAccessConditionsRemoteData: RemoteData<ResourcePolicy>) => {
if (isNotEmpty(defaultAccessConditionsRemoteData.payload)) { if (isNotEmpty(defaultAccessConditionsRemoteData.payload)) {
@@ -171,7 +171,6 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
} }
}), }),
flatMap(() => config$), flatMap(() => config$),
take(1),
flatMap((config: SubmissionUploadsModel) => { flatMap((config: SubmissionUploadsModel) => {
this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : []; this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : [];

View File

@@ -197,7 +197,11 @@ export class SubmissionService {
* The submission id * The submission id
*/ */
dispatchSave(submissionId) { dispatchSave(submissionId) {
this.store.dispatch(new SaveSubmissionFormAction(submissionId)); this.getSubmissionSaveProcessingStatus(submissionId).pipe(
find((isPending: boolean) => !isPending)
).subscribe(() => {
this.store.dispatch(new SaveSubmissionFormAction(submissionId));
})
} }
/** /**

View File

@@ -67,7 +67,7 @@ export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
function onHandleError(parentZoneDelegate, currentZone, targetZone, error) { function onHandleError(parentZoneDelegate, currentZone, targetZone, error) {
if (!res._headerSent) { if (!res._headerSent) {
console.warn('Error in SSR, serving for direct CSR'); console.warn('Error in SSR, serving for direct CSR. Error details : ', error);
res.sendFile('index.csr.html', { root: './src' }); res.sendFile('index.csr.html', { root: './src' });
} }
} }

View File

@@ -1,16 +1,16 @@
@import '_themed_bootstrap_variables.scss'; @import '_themed_bootstrap_variables.scss';
/** Help Variables **/ /** Help Variables **/
$fa-fixed-width: 1.25rem; $fa-fixed-width: 1.25rem !default;
$icon-padding: 1rem; $icon-padding: 1rem !default;
$collapsed-sidebar-width: calculatePx($fa-fixed-width + (2 * $icon-padding)); $collapsed-sidebar-width: calculatePx($fa-fixed-width + (2 * $icon-padding)) !default;
$sidebar-items-width: 250px; $sidebar-items-width: 250px !default;
$total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width; $total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width !default;
/* Fonts */ /* Fonts */
$fa-font-path: "../assets/fonts"; $fa-font-path: "../assets/fonts" !default;
/* Images */ /* Images */
$image-path: "../assets/images"; $image-path: "../assets/images" !default;
/** Bootstrap Variables **/ /** Bootstrap Variables **/
/* Colors */ /* Colors */
@@ -44,8 +44,8 @@ $link-color: map-get($theme-colors, info) !default;
$navbar-dark-color: rgba(white, .5) !default; $navbar-dark-color: rgba(white, .5) !default;
$navbar-light-color: rgba(black, .5) !default; $navbar-light-color: rgba(black, .5) !default;
$navbar-dark-toggler-icon-bg: url("data%3Aimage%2Fsvg+xml%3Bcharset%3Dutf8%2C%3Csvg+viewBox%3D%270+0+30+30%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath+stroke%3D%27#{$navbar-dark-color}%27+stroke-width%3D%272%27+stroke-linecap%3D%27round%27+stroke-miterlimit%3D%2710%27+d%3D%27M4+7h22M4+15h22M4+23h22%27%2F%3E%3C%2Fsvg%3E"); $navbar-dark-toggler-icon-bg: url("data%3Aimage%2Fsvg+xml%3Bcharset%3Dutf8%2C%3Csvg+viewBox%3D%270+0+30+30%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath+stroke%3D%27#{$navbar-dark-color}%27+stroke-width%3D%272%27+stroke-linecap%3D%27round%27+stroke-miterlimit%3D%2710%27+d%3D%27M4+7h22M4+15h22M4+23h22%27%2F%3E%3C%2Fsvg%3E") !default;
$navbar-light-toggler-icon-bg: url("data%3Aimage%2Fsvg+xml%3Bcharset%3Dutf8%2C%3Csvg+viewBox%3D%270+0+30+30%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath+stroke%3D%27#{$navbar-light-color}%27+stroke-width%3D%272%27+stroke-linecap%3D%27round%27+stroke-miterlimit%3D%2710%27+d%3D%27M4+7h22M4+15h22M4+23h22%27%2F%3E%3C%2Fsvg%3E"); $navbar-light-toggler-icon-bg: url("data%3Aimage%2Fsvg+xml%3Bcharset%3Dutf8%2C%3Csvg+viewBox%3D%270+0+30+30%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath+stroke%3D%27#{$navbar-light-color}%27+stroke-width%3D%272%27+stroke-linecap%3D%27round%27+stroke-miterlimit%3D%2710%27+d%3D%27M4+7h22M4+15h22M4+23h22%27%2F%3E%3C%2Fsvg%3E") !default;
$enable-shadows: true !default; $enable-shadows: true !default;

View File

@@ -1,38 +1,39 @@
@import '_themed_custom_variables.scss'; @import '_themed_custom_variables.scss';
$content-spacing: $spacer * 1.5; $content-spacing: $spacer * 1.5 !default;
$button-height: $input-btn-padding-y * 2 + $input-btn-line-height + calculateRem($input-btn-border-width*2); $button-height: $input-btn-padding-y * 2 + $input-btn-line-height + calculateRem($input-btn-border-width*2) !default;
$card-height-percentage:98%; $card-height-percentage:98% !default;
$card-thumbnail-height:240px; $card-thumbnail-height:240px !default;
$dropdown-menu-max-height: 200px; $dropdown-menu-max-height: 200px !default;
$drop-zone-area-height: 44px; $drop-zone-area-height: 44px !default;
$drop-zone-area-z-index: 1025; $drop-zone-area-z-index: 1025 !default;
$drop-zone-area-inner-z-index: 1021; $drop-zone-area-inner-z-index: 1021 !default;
$login-logo-height:72px; $login-logo-height:72px !default;
$login-logo-width:72px; $login-logo-width:72px !default;
$submission-header-z-index: 1001; $submission-header-z-index: 1001 !default;
$submission-footer-z-index: 1000; $submission-footer-z-index: 999 !default;
$main-z-index: 0; $main-z-index: 0 !default;
$nav-z-index: 10; $nav-z-index: 10 !default;
$sidebar-z-index: 20; $sidebar-z-index: 20 !default;
$header-logo-height: 80px; $header-logo-height: 80px !default;
$header-logo-height-xs: 50px; $header-logo-height-xs: 50px !default;
$header-icon-color: $link-color !default;
$admin-sidebar-bg: darken(#2B4E72, 17%); $admin-sidebar-bg: darken(#2B4E72, 17%) !default;
$admin-sidebar-active-bg: darken($admin-sidebar-bg, 3%); $admin-sidebar-active-bg: darken($admin-sidebar-bg, 3%) !default;
$admin-sidebar-header-bg: darken($admin-sidebar-bg, 7%); $admin-sidebar-header-bg: darken($admin-sidebar-bg, 7%) !default;
$dark-scrollbar-background: $admin-sidebar-active-bg; $dark-scrollbar-background: $admin-sidebar-active-bg !default;
$dark-scrollbar-foreground: #47495d; $dark-scrollbar-foreground: #47495d !default;
$submission-sections-margin-bottom: .5rem !default; $submission-sections-margin-bottom: .5rem !default;
$edit-item-button-min-width: 100px; $edit-item-button-min-width: 100px !default;
$edit-item-metadata-field-width: 190px; $edit-item-metadata-field-width: 190px !default;
$edit-item-language-field-width: 43px; $edit-item-language-field-width: 43px !default;
$thumbnail-max-width: 175px; $thumbnail-max-width: 175px !default;