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,
SearchFixedFilterService,
ConfigurationSearchPageGuard,
SearchFilterService,
SearchConfigurationService
],
entryComponents: [

View File

@@ -19,6 +19,7 @@ import { Observable } from 'rxjs/internal/Observable';
import { FindAllOptions } from './request.models';
import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list';
import { SearchParam } from '../cache/models/search-param.model';
@Injectable()
export class CollectionDataService extends ComColDataService<Collection> {
@@ -40,6 +41,36 @@ export class CollectionDataService extends ComColDataService<Collection> {
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
*

View File

@@ -3,7 +3,7 @@ import { HttpHeaders } from '@angular/common/http';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
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 { AppState } from '../../app.reducer';
@@ -262,12 +262,13 @@ export class RequestService {
*/
private clearRequestsOnTheirWayToTheStore(request: GetRequest) {
this.getByHref(request.href).pipe(
find((re: RequestEntry) => hasValue(re)))
.subscribe((re: RequestEntry) => {
if (!re.responsePending) {
remove(this.requestsOnTheirWayToTheStore, (item) => item === request.href);
}
});
filter((re: RequestEntry) => hasValue(re)),
take(1)
).subscribe((re: RequestEntry) => {
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
Object.keys(item.sections)
.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({});
// Iterate over all sections property
Object.keys(item.sections[sectionId])

View File

@@ -8,3 +8,14 @@
background-image: none !important;
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 { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
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 {
metadata: string;
@@ -62,7 +63,7 @@ export class ChipsItem {
if (this._item.hasOwnProperty(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).hasPlaceholder()) {
&& !this.hasPlaceholder(this._item[icon.metadata])) {
if ((icon.visibleWhenAuthorityEmpty
|| (this._item[icon.metadata] as FormFieldMetadataValueObject).confidence !== ConfidenceType.CF_UNSET)
&& isNotEmpty(icon.style)) {
@@ -109,4 +110,9 @@ export class ChipsItem {
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>
<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')]">
<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 { Subject } from 'rxjs';
import { isNotEmpty } from '../../../../empty.util';
import { DsDynamicInputModel } from './ds-dynamic-input.model';
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
@@ -16,12 +19,16 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
@serializable() separator: string;
@serializable() hasLanguages = false;
isCustomGroup = true;
valueUpdates: Subject<string>;
constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.separator = config.separator + ' ';
this.valueUpdates = new Subject<string>();
this.valueUpdates.subscribe((value: string) => this.value = value);
}
get value() {

View File

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

View File

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

View File

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

View File

@@ -237,6 +237,12 @@ describe('Dynamic Lookup component', () => {
it('should init component properly', () => {
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(() => {
@@ -283,7 +289,7 @@ describe('Dynamic Lookup component', () => {
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
lookupComp.onInput(new Event('input'));
lookupComp.onChange(new Event('change'));
expect(lookupComp.model.value).toEqual(new FormFieldMetadataValueObject('test'))
}));
@@ -293,10 +299,11 @@ describe('Dynamic Lookup component', () => {
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
lookupComp.onInput(new Event('input'));
lookupComp.onChange(new Event('change'));
expect(lookupComp.model.value).not.toBeDefined();
});
});
describe('and init model value is not empty', () => {
@@ -318,6 +325,19 @@ describe('Dynamic Lookup component', () => {
it('should init component properly', () => {
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', () => {
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(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.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 {
if (isUndefined(item) || isNull(item)) {
return '';
@@ -159,7 +168,7 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
}
public isSearchDisabled() {
return isEmpty(this.firstInputValue);
return isEmpty(this.firstInputValue) || this.editMode;
}
public onBlurEvent(event: Event) {
@@ -170,12 +179,13 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
this.focus.emit(event);
}
public onInput(event) {
public onChange(event) {
event.preventDefault();
if (!this.model.authorityOptions.closed) {
if (isNotEmpty(this.getCurrentValue())) {
const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue());
if (!this.editMode) {
this.onSelect(currentValue);
this.updateModel(currentValue);
}
} else {
this.remove();
@@ -191,12 +201,7 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
}
public onSelect(event) {
this.group.markAsDirty();
this.model.valueUpdates.next(event);
this.setInputsValue(event);
this.change.emit(event);
this.optionsList = null;
this.pageInfo = null;
this.updateModel(event);
}
public openChange(isOpened: boolean) {
@@ -219,7 +224,7 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
display: this.getCurrentValue(),
value: this.getCurrentValue()
});
this.onSelect(newValue);
this.updateModel(newValue);
} else {
this.remove();
}

View File

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

View File

@@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } fro
import { FormGroup } from '@angular/forms';
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 {
DynamicFormControlComponent,
@@ -71,7 +71,13 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp
}
this.pageInfo = object.pageInfo;
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;

View File

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

View File

@@ -156,7 +156,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => {
inputElement.value = 'test value';
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.instance, 'isPopupOpen').and.returnValue(false);
typeaheadComp.onBlur(new Event('blur'));
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';
typeaheadFixture.detectChanges();
spyOn(typeaheadComp.blur, 'emit');
spyOn(typeaheadComp.change, 'emit');
typeaheadComp.onBlur(new Event('blur'));
// expect(typeaheadComp.change.emit).toHaveBeenCalled();
spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false);
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();
});

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 {
@@ -8,14 +8,13 @@ import {
} from '@ng-dynamic-forms/core';
import { catchError, debounceTime, distinctUntilChanged, filter, map, merge, switchMap, tap } from 'rxjs/operators';
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 { DynamicTypeaheadModel } from './dynamic-typeahead.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 { ConfidenceType } from '../../../../../../core/integration/models/confidence-type';
@Component({
@@ -32,6 +31,8 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('instance') instance: NgbTypeahead;
searching = false;
searchOptions: IntegrationSearchOptions;
searchFailed = false;
@@ -105,16 +106,26 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp
onInput(event) {
if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) {
this.inputValue = new FormFieldMetadataValueObject(event.target.value);
this.model.valueUpdates.next(this.inputValue);
}
}
onBlur(event: Event) {
if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) {
this.change.emit(this.inputValue);
this.inputValue = null;
if (!this.instance.isPopupOpen()) {
if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) {
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) {
@@ -141,4 +152,5 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp
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 input2ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_SECOND_INPUT_SUFFIX, label, true, false);
input2ModelConfig.hint = '&nbsp;';
if (this.configData.mandatory) {
input1ModelConfig.required = true;

View File

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

View File

@@ -24,6 +24,7 @@ export class OneboxFieldParser extends FieldParser {
const clsGroup = {
element: {
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.group = [];
inputSelectGroup.legend = this.configData.label;
inputSelectGroup.hint = this.configData.hints;
const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label);
selectModelConfig.hint = null;
this.setOptions(selectModelConfig);
if (isNotEmpty(fieldValue)) {
selectModelConfig.value = fieldValue.metadata;
@@ -63,6 +66,7 @@ export class OneboxFieldParser extends FieldParser {
inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect));
const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, true);
inputModelConfig.hint = null;
this.setValues(inputModelConfig, fieldValue);
inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly;

View File

@@ -42,3 +42,8 @@
.right-addon input {
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"
class="btn btn-primary mt-1 mb-3"
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">
<i class="fa fa-edit"></i> {{'submission.workflow.tasks.claimed.edit' | translate}}
</a>

View File

@@ -15,11 +15,11 @@
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
<ds-metadata-field-wrapper [label]="('item.page.files' | translate)">
<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?.sizeBytes) | dsFileSize }})</span>
<span *ngIf="!last" innerHTML="{{separator}}"></span>
</a>
</button>
</div>
<ng-container *ngIf="bitstreams?.length === 0">
<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 { FileSizePipe } from '../../../utils/file-size-pipe';
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 { PaginatedList } from '../../../../core/data/paginated-list';
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 fixture: ComponentFixture<ItemDetailPreviewComponent>;
@@ -62,6 +72,10 @@ describe('ItemDetailPreviewComponent', () => {
}),
],
declarations: [ItemDetailPreviewComponent, ItemDetailPreviewFieldComponent, TruncatePipe, FileSizePipe, VarDirective],
providers: [
{ provide: FileService, useValue: getMockFileService() },
{ provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemDetailPreviewComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }

View File

@@ -1,12 +1,15 @@
import { Component, Input } from '@angular/core';
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { Item } from '../../../../core/shared/item.model';
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
import { fadeInOut } from '../../../animations/fade';
import { Bitstream } from '../../../../core/shared/bitstream.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.
@@ -54,6 +57,16 @@ export class ItemDetailPreviewComponent {
*/
public thumbnail$: Observable<Bitstream>;
/**
* Initialize instance variables
*
* @param {FileService} fileService
* @param {HALEndpointService} halService
*/
constructor(private fileService: FileService,
private halService: HALEndpointService) {
}
/**
* Initialize all instance variables
*/
@@ -62,4 +75,15 @@ export class ItemDetailPreviewComponent {
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"
(blur)="onClose()"
(click)="onClose()"
[disabled]="(disabled$ | async)"
[disabled]="(disabled$ | async) || (processingChange$ | async)"
ngbDropdownToggle>
<span *ngIf="(disabled$ | async)"><i class='fas fa-circle-notch fa-spin'></i></span>
<span *ngIf="!(disabled$ | async)">{{ selectedCollectionName$ | async }}</span>
<span *ngIf="(processingChange$ | async)"><i class='fas fa-circle-notch fa-spin'></i></span>
<span *ngIf="!(processingChange$ | async)">{{ selectedCollectionName$ | async }}</span>
</button>
<div ngbDropdownMenu

View File

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

View File

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

View File

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

View File

@@ -64,6 +64,12 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
*/
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
* @type {SubmissionFormsModel}
@@ -295,6 +301,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
}),
distinctUntilChanged())
.subscribe((sectionState: SubmissionSectionObject) => {
this.fieldsOnTheirWayToBeRemoved = new Map();
this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors);
})
)
@@ -348,11 +355,24 @@ export class SubmissionSectionformComponent extends SectionModelComponent {
* the [[DynamicFormControlEvent]] emitted
*/
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.pathCombiner,
event,
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 {
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 {
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),
tap((submissionObject: SubmissionObjectEntry) => this.collectionId = 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),
flatMap((collectionRemoteData: RemoteData<Collection>) => {
return this.resourcePolicyService.findByHref(
(collectionRemoteData.payload as any)._links.defaultAccessConditions
);
}),
find((defaultAccessConditionsRemoteData: RemoteData<ResourcePolicy>) =>
filter((defaultAccessConditionsRemoteData: RemoteData<ResourcePolicy>) =>
defaultAccessConditionsRemoteData.hasSucceeded),
tap((defaultAccessConditionsRemoteData: RemoteData<ResourcePolicy>) => {
if (isNotEmpty(defaultAccessConditionsRemoteData.payload)) {
@@ -171,7 +171,6 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
}
}),
flatMap(() => config$),
take(1),
flatMap((config: SubmissionUploadsModel) => {
this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : [];

View File

@@ -197,7 +197,11 @@ export class SubmissionService {
* The submission id
*/
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) {
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' });
}
}

View File

@@ -1,16 +1,16 @@
@import '_themed_bootstrap_variables.scss';
/** Help Variables **/
$fa-fixed-width: 1.25rem;
$icon-padding: 1rem;
$collapsed-sidebar-width: calculatePx($fa-fixed-width + (2 * $icon-padding));
$sidebar-items-width: 250px;
$total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width;
$fa-fixed-width: 1.25rem !default;
$icon-padding: 1rem !default;
$collapsed-sidebar-width: calculatePx($fa-fixed-width + (2 * $icon-padding)) !default;
$sidebar-items-width: 250px !default;
$total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width !default;
/* Fonts */
$fa-font-path: "../assets/fonts";
$fa-font-path: "../assets/fonts" !default;
/* Images */
$image-path: "../assets/images";
$image-path: "../assets/images" !default;
/** Bootstrap Variables **/
/* Colors */
@@ -44,8 +44,8 @@ $link-color: map-get($theme-colors, info) !default;
$navbar-dark-color: rgba(white, .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-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-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") !default;
$enable-shadows: true !default;

View File

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