mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-12 04:23:04 +00:00
59334: added validation
This commit is contained in:
@@ -233,6 +233,9 @@
|
|||||||
"language": "Lang",
|
"language": "Lang",
|
||||||
"edit": "Edit"
|
"edit": "Edit"
|
||||||
},
|
},
|
||||||
|
"metadatafield": {
|
||||||
|
"invalid": "Please choose a valid metadata field"
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"outdated": {
|
"outdated": {
|
||||||
"title": "Changed outdated",
|
"title": "Changed outdated",
|
||||||
@@ -241,6 +244,10 @@
|
|||||||
"discarded": {
|
"discarded": {
|
||||||
"title": "Changed discarded",
|
"title": "Changed discarded",
|
||||||
"content": "Your changes were discarded. To reinstate your changes click the 'Undo' button"
|
"content": "Your changes were discarded. To reinstate your changes click the 'Undo' button"
|
||||||
|
},
|
||||||
|
"invalid": {
|
||||||
|
"title": "Metadata invalid",
|
||||||
|
"content": "Please make sure all fields are valid"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,10 +13,14 @@
|
|||||||
[(ngModel)]="metadata.key"
|
[(ngModel)]="metadata.key"
|
||||||
(submitSuggestion)="update()"
|
(submitSuggestion)="update()"
|
||||||
(clickSuggestion)="update()"
|
(clickSuggestion)="update()"
|
||||||
|
(typeSuggestion)="update()"
|
||||||
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
||||||
|
[formControl]="formControl"
|
||||||
ngDefaultControl
|
ngDefaultControl
|
||||||
></ds-input-suggestions>
|
></ds-input-suggestions>
|
||||||
</div>
|
</div>
|
||||||
|
<small class="text-danger"
|
||||||
|
*ngIf="!(valid | async)">{{"item.edit.metadata.metadatafield.invalid" | translate}}</small>
|
||||||
</td>
|
</td>
|
||||||
<td class="col-7">
|
<td class="col-7">
|
||||||
<div *ngIf="!(editable | async)">
|
<div *ngIf="!(editable | async)">
|
||||||
@@ -38,10 +42,14 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="col-1 text-center">
|
<td class="col-1 text-center">
|
||||||
<div>
|
<div>
|
||||||
<i *ngIf="canSetEditable() | async" class="fas fa-edit fa-fw text-primary" (click)="setEditable(true)"></i>
|
<i *ngIf="canSetEditable() | async" class="fas fa-edit fa-fw text-primary"
|
||||||
<i *ngIf="canSetUneditable() | async" class="fas fa-check fa-fw text-success" (click)="setEditable(false)"></i>
|
(click)="setEditable(true)"></i>
|
||||||
<i *ngIf="canRemove() | async" class="fas fa-trash-alt fa-fw text-danger" (click)="remove()"></i>
|
<i *ngIf="canSetUneditable() | async" class="fas fa-check fa-fw text-success"
|
||||||
<i *ngIf="canUndo() | async" class="fas fa-undo-alt fa-fw text-warning" (click)="removeChangesFromField()"></i>
|
(click)="setEditable(false)"></i>
|
||||||
|
<i *ngIf="canRemove() | async" class="fas fa-trash-alt fa-fw text-danger"
|
||||||
|
(click)="remove()"></i>
|
||||||
|
<i *ngIf="canUndo() | async" class="fas fa-undo-alt fa-fw text-warning"
|
||||||
|
(click)="removeChangesFromField()"></i>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</div>
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
||||||
|
@@ -11,6 +11,9 @@ import { FieldChangeType } from '../../../../core/data/object-updates/object-upd
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { inListValidator } from '../../../../shared/utils/validator.functions';
|
||||||
|
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { FormControl, ValidationErrors, ValidatorFn } from '@angular/forms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-in-place-field',
|
selector: 'ds-edit-in-place-field',
|
||||||
@@ -21,7 +24,6 @@ import { ObjectUpdatesService } from '../../../../core/data/object-updates/objec
|
|||||||
* Component that displays a single metadatum of an item on the edit page
|
* Component that displays a single metadatum of an item on the edit page
|
||||||
*/
|
*/
|
||||||
export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current field, value and state of the metadatum
|
* The current field, value and state of the metadatum
|
||||||
*/
|
*/
|
||||||
@@ -39,22 +41,43 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
*/
|
*/
|
||||||
editable: Observable<boolean>;
|
editable: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits whether or not this field is currently valid
|
||||||
|
*/
|
||||||
|
valid: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current suggestions for the metadatafield when editing
|
* The current suggestions for the metadatafield when editing
|
||||||
*/
|
*/
|
||||||
metadataFieldSuggestions: BehaviorSubject<InputSuggestion[]> = new BehaviorSubject([]);
|
metadataFieldSuggestions: BehaviorSubject<InputSuggestion[]> = new BehaviorSubject([]);
|
||||||
|
|
||||||
|
formControl: FormControl;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private metadataFieldService: RegistryService,
|
private metadataFieldService: RegistryService,
|
||||||
private objectUpdatesService: ObjectUpdatesService,
|
private objectUpdatesService: ObjectUpdatesService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up an observable that keeps track of the current editable and valid state of this field
|
||||||
|
* Also creates a form control object for the input suggestions
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.editable = this.objectUpdatesService.isEditable(this.route, this.metadata.uuid);
|
||||||
|
this.valid = this.objectUpdatesService.isValid(this.route, this.metadata.uuid);
|
||||||
|
this.findMetadataFields().pipe(take(1)).subscribe((metadataFields: string[]) => {
|
||||||
|
const validator = inListValidator(metadataFields);
|
||||||
|
this.formControl = new FormControl('', validator);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a new change update for this field to the object updates service
|
* Sends a new change update for this field to the object updates service
|
||||||
*/
|
*/
|
||||||
update() {
|
update() {
|
||||||
this.objectUpdatesService.saveChangeFieldUpdate(this.route, this.metadata);
|
this.objectUpdatesService.saveChangeFieldUpdate(this.route, this.metadata);
|
||||||
|
this.objectUpdatesService.setValidFieldUpdate(this.route, this.metadata.uuid, this.formControl.valid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,13 +102,6 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
this.objectUpdatesService.removeSingleFieldUpdate(this.route, this.metadata.uuid);
|
this.objectUpdatesService.removeSingleFieldUpdate(this.route, this.metadata.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up an observable that keeps track of the current editable state of this field
|
|
||||||
*/
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.editable = this.objectUpdatesService.isEditable(this.route, this.metadata.uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the current metadatafield based on the fieldUpdate input field
|
* Sets the current metadatafield based on the fieldUpdate input field
|
||||||
*/
|
*/
|
||||||
@@ -115,6 +131,13 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findMetadataFields(): Observable<string[]> {
|
||||||
|
return this.metadataFieldService.getAllMetadataFields().pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
take(1),
|
||||||
|
map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString())));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a user should be allowed to edit this field
|
* Check if a user should be allowed to edit this field
|
||||||
* @return an observable that emits true when the user should be able to edit this field and false when they should not
|
* @return an observable that emits true when the user should be able to edit this field and false when they should not
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
.button-row .btn {
|
||||||
|
min-width: $button-min-width;
|
||||||
|
}
|
@@ -11,7 +11,7 @@ import {
|
|||||||
Identifiable
|
Identifiable
|
||||||
} from '../../../core/data/object-updates/object-updates.reducer';
|
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||||
import { first, switchMap, tap } from 'rxjs/operators';
|
import { first, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
@@ -20,6 +20,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-metadata',
|
selector: 'ds-item-metadata',
|
||||||
|
styleUrls: ['./item-metadata.component.scss'],
|
||||||
templateUrl: './item-metadata.component.html',
|
templateUrl: './item-metadata.component.html',
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
@@ -114,6 +115,8 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
* Makes sure the new version of the item is rendered on the page
|
* Makes sure the new version of the item is rendered on the page
|
||||||
*/
|
*/
|
||||||
submit() {
|
submit() {
|
||||||
|
this.isValid().pipe(first()).subscribe((isValid) => {
|
||||||
|
if (isValid) {
|
||||||
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.route, this.item.metadata) as Observable<Metadatum[]>;
|
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.route, this.item.metadata) as Observable<Metadatum[]>;
|
||||||
metadata$.pipe(
|
metadata$.pipe(
|
||||||
first(),
|
first(),
|
||||||
@@ -130,6 +133,12 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata);
|
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
const title = this.translateService.instant('item.edit.metadata.notifications.invalid.title');
|
||||||
|
const content = this.translateService.instant('item.edit.metadata.notifications.invalid.content');
|
||||||
|
this.notificationsService.error(title, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,4 +172,8 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isValid() {
|
||||||
|
return this.objectUpdatesService.isValidPage(this.route);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -38,7 +38,7 @@ const ITEM_EDIT_PATH = ':id/edit';
|
|||||||
{
|
{
|
||||||
path: ITEM_EDIT_PATH,
|
path: ITEM_EDIT_PATH,
|
||||||
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
||||||
canActivate: [AuthenticatedGuard]
|
// canActivate: [AuthenticatedGuard]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ActionReducerMap,
|
ActionReducerMap,
|
||||||
createFeatureSelector,
|
createFeatureSelector,
|
||||||
createSelector,
|
|
||||||
MemoizedSelector
|
|
||||||
} from '@ngrx/store';
|
} from '@ngrx/store';
|
||||||
|
|
||||||
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
|
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
|
||||||
@@ -14,8 +12,6 @@ import {
|
|||||||
objectUpdatesReducer,
|
objectUpdatesReducer,
|
||||||
ObjectUpdatesState
|
ObjectUpdatesState
|
||||||
} from './data/object-updates/object-updates.reducer';
|
} from './data/object-updates/object-updates.reducer';
|
||||||
import { hasValue } from '../shared/empty.util';
|
|
||||||
import { AppState } from '../app.reducer';
|
|
||||||
|
|
||||||
export interface CoreState {
|
export interface CoreState {
|
||||||
'cache/object': ObjectCacheState,
|
'cache/object': ObjectCacheState,
|
||||||
|
@@ -9,6 +9,7 @@ import { INotification } from '../../../shared/notifications/models/notification
|
|||||||
export const ObjectUpdatesActionTypes = {
|
export const ObjectUpdatesActionTypes = {
|
||||||
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
||||||
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
||||||
|
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
|
||||||
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
||||||
DISCARD: type('dspace/core/cache/object-updates/DISCARD'),
|
DISCARD: type('dspace/core/cache/object-updates/DISCARD'),
|
||||||
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
||||||
@@ -109,6 +110,33 @@ export class SetEditableFieldUpdateAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to set the isValid state of an existing field in the ObjectUpdates state for a certain page url
|
||||||
|
*/
|
||||||
|
export class SetValidFieldUpdateAction implements Action {
|
||||||
|
type = ObjectUpdatesActionTypes.SET_VALID_FIELD;
|
||||||
|
payload: {
|
||||||
|
url: string,
|
||||||
|
uuid: string,
|
||||||
|
isValid: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new SetEditableFieldUpdateAction
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the unique url of the page
|
||||||
|
* @param fieldUUID The UUID of the field of which
|
||||||
|
* @param isValid The new isValid value for the field
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
url: string,
|
||||||
|
fieldUUID: string,
|
||||||
|
isValid: boolean) {
|
||||||
|
this.payload = { url, uuid: fieldUUID, isValid };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An ngrx action to discard all existing updates in the ObjectUpdates state for a certain page url
|
* An ngrx action to discard all existing updates in the ObjectUpdates state for a certain page url
|
||||||
*/
|
*/
|
||||||
|
@@ -7,7 +7,7 @@ import {
|
|||||||
ObjectUpdatesActionTypes,
|
ObjectUpdatesActionTypes,
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
RemoveFieldUpdateAction,
|
RemoveFieldUpdateAction,
|
||||||
RemoveObjectUpdatesAction, SetEditableFieldUpdateAction
|
RemoveObjectUpdatesAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
|
|
||||||
@@ -15,7 +15,8 @@ export const OBJECT_UPDATES_TRASH_PATH = '/trash';
|
|||||||
|
|
||||||
export interface FieldState {
|
export interface FieldState {
|
||||||
editable: boolean,
|
editable: boolean,
|
||||||
isNew: boolean
|
isNew: boolean,
|
||||||
|
isValid: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FieldStates {
|
export interface FieldStates {
|
||||||
@@ -46,8 +47,8 @@ export interface ObjectUpdatesState {
|
|||||||
[url: string]: ObjectUpdatesEntry;
|
[url: string]: ObjectUpdatesEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialFieldState = { editable: false, isNew: false };
|
const initialFieldState = { editable: false, isNew: false, isValid: true };
|
||||||
const initialNewFieldState = { editable: true, isNew: true };
|
const initialNewFieldState = { editable: true, isNew: true, isValid: true };
|
||||||
|
|
||||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||||
const initialState = Object.create(null);
|
const initialState = Object.create(null);
|
||||||
@@ -80,6 +81,9 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
|||||||
case ObjectUpdatesActionTypes.SET_EDITABLE_FIELD: {
|
case ObjectUpdatesActionTypes.SET_EDITABLE_FIELD: {
|
||||||
return setEditableFieldUpdate(state, action as SetEditableFieldUpdateAction);
|
return setEditableFieldUpdate(state, action as SetEditableFieldUpdateAction);
|
||||||
}
|
}
|
||||||
|
case ObjectUpdatesActionTypes.SET_VALID_FIELD: {
|
||||||
|
return setValidFieldUpdate(state, action as SetValidFieldUpdateAction);
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -147,8 +151,8 @@ function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) {
|
|||||||
Object.keys(pageState.fieldStates).forEach((uuid: string) => {
|
Object.keys(pageState.fieldStates).forEach((uuid: string) => {
|
||||||
const fieldState: FieldState = pageState.fieldStates[uuid];
|
const fieldState: FieldState = pageState.fieldStates[uuid];
|
||||||
if (!fieldState.isNew) {
|
if (!fieldState.isNew) {
|
||||||
/* After discarding we don't want the reset fields to stay editable */
|
/* After discarding we don't want the reset fields to stay editable or invalid */
|
||||||
newFieldStates[uuid] = Object.assign({}, fieldState, { editable: false });
|
newFieldStates[uuid] = Object.assign({}, fieldState, { editable: false, isValid: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,7 +219,7 @@ function removeFieldUpdate(state: any, action: RemoveFieldUpdateAction) {
|
|||||||
/* If this field was added, just throw it away */
|
/* If this field was added, just throw it away */
|
||||||
delete newFieldStates[uuid];
|
delete newFieldStates[uuid];
|
||||||
} else {
|
} else {
|
||||||
newFieldStates[uuid] = Object.assign({}, newFieldStates[uuid], { editable: false });
|
newFieldStates[uuid] = Object.assign({}, newFieldStates[uuid], { editable: false, isValid: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newPageState = Object.assign({}, state[url], {
|
newPageState = Object.assign({}, state[url], {
|
||||||
@@ -243,7 +247,7 @@ function determineChangeType(oldType: FieldChangeType, newType: FieldChangeType)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the state of a specific action's url and uuid to false or true
|
* Set the editable state of a specific action's url and uuid to false or true
|
||||||
* @param state The current state
|
* @param state The current state
|
||||||
* @param action The action to perform on the current state
|
* @param action The action to perform on the current state
|
||||||
*/
|
*/
|
||||||
@@ -264,6 +268,28 @@ function setEditableFieldUpdate(state: any, action: SetEditableFieldUpdateAction
|
|||||||
return Object.assign({}, state, { [url]: newPageState });
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the isValid state of a specific action's url and uuid to false or true
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
|
function setValidFieldUpdate(state: any, action: SetValidFieldUpdateAction) {
|
||||||
|
const url: string = action.payload.url;
|
||||||
|
const uuid: string = action.payload.uuid;
|
||||||
|
const isValid: boolean = action.payload.isValid;
|
||||||
|
|
||||||
|
const pageState: ObjectUpdatesEntry = state[url];
|
||||||
|
|
||||||
|
const fieldState = pageState.fieldStates[uuid];
|
||||||
|
const newFieldState = Object.assign({}, fieldState, { isValid });
|
||||||
|
|
||||||
|
const newFieldStates = Object.assign({}, pageState.fieldStates, { [uuid]: newFieldState });
|
||||||
|
|
||||||
|
const newPageState = Object.assign({}, pageState, { fieldStates: newFieldStates });
|
||||||
|
|
||||||
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to create an initial FieldStates object based on a list of Identifiable objects
|
* Method to create an initial FieldStates object based on a list of Identifiable objects
|
||||||
* @param fields Identifiable objects
|
* @param fields Identifiable objects
|
||||||
|
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import { coreSelector, CoreState } from '../../core.reducers';
|
import { coreSelector, CoreState } from '../../core.reducers';
|
||||||
import {
|
import {
|
||||||
|
FieldState,
|
||||||
FieldUpdates,
|
FieldUpdates,
|
||||||
Identifiable, OBJECT_UPDATES_TRASH_PATH,
|
Identifiable, OBJECT_UPDATES_TRASH_PATH,
|
||||||
ObjectUpdatesEntry,
|
ObjectUpdatesEntry,
|
||||||
@@ -15,7 +16,7 @@ import {
|
|||||||
InitializeFieldsAction,
|
InitializeFieldsAction,
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
RemoveFieldUpdateAction,
|
RemoveFieldUpdateAction,
|
||||||
SetEditableFieldUpdateAction
|
SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { filter, map } from 'rxjs/operators';
|
import { filter, map } from 'rxjs/operators';
|
||||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
@@ -102,6 +103,33 @@ export class ObjectUpdatesService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to check if a specific field is currently valid in the store
|
||||||
|
* @param url The URL of the page on which the field resides
|
||||||
|
* @param uuid The UUID of the field
|
||||||
|
*/
|
||||||
|
isValid(url: string, uuid: string): Observable<boolean> {
|
||||||
|
const objectUpdates = this.getObjectEntry(url);
|
||||||
|
return objectUpdates.pipe(
|
||||||
|
filter((objectEntry) => hasValue(objectEntry.fieldStates[uuid])),
|
||||||
|
map((objectEntry) => objectEntry.fieldStates[uuid].isValid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to check if a specific page is currently valid in the store
|
||||||
|
* @param url The URL of the page
|
||||||
|
*/
|
||||||
|
isValidPage(url: string): Observable<boolean> {
|
||||||
|
const objectUpdates = this.getObjectEntry(url);
|
||||||
|
return objectUpdates.pipe(
|
||||||
|
map((entry: ObjectUpdatesEntry) => {
|
||||||
|
return Object.values(entry.fieldStates).findIndex((state: FieldState) => !state.isValid) < 0
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the saveFieldUpdate method with FieldChangeType.ADD
|
* Calls the saveFieldUpdate method with FieldChangeType.ADD
|
||||||
* @param url The page's URL for which the changes are saved
|
* @param url The page's URL for which the changes are saved
|
||||||
@@ -139,6 +167,16 @@ export class ObjectUpdatesService {
|
|||||||
this.store.dispatch(new SetEditableFieldUpdateAction(url, uuid, editable));
|
this.store.dispatch(new SetEditableFieldUpdateAction(url, uuid, editable));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a SetValidFieldUpdateAction to the store to set a field's isValid state
|
||||||
|
* @param url The URL of the page on which the field resides
|
||||||
|
* @param uuid The UUID of the field that should be set
|
||||||
|
* @param valid The new value of isValid in the store for this field
|
||||||
|
*/
|
||||||
|
setValidFieldUpdate(url: string, uuid: string, valid: boolean) {
|
||||||
|
this.store.dispatch(new SetValidFieldUpdateAction(url, uuid, valid));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to dispatch an DiscardObjectUpdatesAction to the store
|
* Method to dispatch an DiscardObjectUpdatesAction to the store
|
||||||
* @param url The page's URL for which the changes should be discarded
|
* @param url The page's URL for which the changes should be discarded
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<ds-form *ngIf="formModel"
|
<ds-form *ngIf="formModel"
|
||||||
[formId]="'comcol-form-id'"
|
[formId]="'comcol-form-id'"
|
||||||
[formModel]="formModel" (submitForm)="onSubmit()"></ds-form>
|
[formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>
|
||||||
|
@@ -50,10 +50,7 @@ describe('ComColFormComponent', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
const locationStub = {
|
const locationStub = jasmine.createSpyObj('location', ['back']);
|
||||||
back: () => {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/* tslint:enable:no-empty */
|
/* tslint:enable:no-empty */
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
@@ -112,4 +109,11 @@ describe('ComColFormComponent', () => {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onCancel', () => {
|
||||||
|
it('should call the back method on the Location service', () => {
|
||||||
|
comp.onCancel();
|
||||||
|
expect(locationStub.back).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -112,4 +112,8 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
ElementRef, EventEmitter, forwardRef,
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
forwardRef,
|
||||||
Input,
|
Input,
|
||||||
|
OnChanges,
|
||||||
Output,
|
Output,
|
||||||
QueryList, SimpleChanges,
|
QueryList,
|
||||||
|
SimpleChanges,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ViewChildren
|
ViewChildren
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
@@ -19,6 +23,8 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: NG_VALUE_ACCESSOR,
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
// Usage of forwardRef necessary https://github.com/angular/angular.io/issues/1151
|
||||||
|
// tslint:disable-next-line:no-forward-ref
|
||||||
useExisting: forwardRef(() => InputSuggestionsComponent),
|
useExisting: forwardRef(() => InputSuggestionsComponent),
|
||||||
multi: true
|
multi: true
|
||||||
}
|
}
|
||||||
@@ -28,7 +34,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|||||||
/**
|
/**
|
||||||
* Component representing a form with a autocomplete functionality
|
* Component representing a form with a autocomplete functionality
|
||||||
*/
|
*/
|
||||||
export class InputSuggestionsComponent implements ControlValueAccessor {
|
export class InputSuggestionsComponent implements ControlValueAccessor, OnChanges {
|
||||||
/**
|
/**
|
||||||
* The suggestions that should be shown
|
* The suggestions that should be shown
|
||||||
*/
|
*/
|
||||||
@@ -64,6 +70,11 @@ export class InputSuggestionsComponent implements ControlValueAccessor {
|
|||||||
*/
|
*/
|
||||||
@Output() clickSuggestion = new EventEmitter();
|
@Output() clickSuggestion = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output for when something is typed in the input field
|
||||||
|
*/
|
||||||
|
@Output() typeSuggestion = new EventEmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Output for when new suggestions should be requested
|
* Output for when new suggestions should be requested
|
||||||
*/
|
*/
|
||||||
@@ -195,6 +206,7 @@ export class InputSuggestionsComponent implements ControlValueAccessor {
|
|||||||
this.findSuggestions.emit(data);
|
this.findSuggestions.emit(data);
|
||||||
}
|
}
|
||||||
this.blockReopen = false;
|
this.blockReopen = false;
|
||||||
|
this.typeSuggestion.emit(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(data) {
|
onSubmit(data) {
|
||||||
|
7
src/app/shared/utils/validator.functions.ts
Normal file
7
src/app/shared/utils/validator.functions.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { AbstractControl, ValidatorFn } from '@angular/forms';
|
||||||
|
|
||||||
|
export function inListValidator(list: string[]): ValidatorFn {
|
||||||
|
return (control: AbstractControl): {[key: string]: any} | null => {
|
||||||
|
const contains = list.indexOf(control.value) > 0;
|
||||||
|
return contains ? null : {inList: {value: control.value}} };
|
||||||
|
}
|
@@ -1,6 +1,8 @@
|
|||||||
$content-spacing: $spacer * 1.5;
|
$content-spacing: $spacer * 1.5;
|
||||||
|
|
||||||
$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);
|
||||||
|
$button-min-width: 100px;
|
||||||
|
|
||||||
$card-height-percentage:98%;
|
$card-height-percentage:98%;
|
||||||
$card-thumbnail-height:240px;
|
$card-thumbnail-height:240px;
|
||||||
$dropdown-menu-max-height: 200px;
|
$dropdown-menu-max-height: 200px;
|
||||||
|
Reference in New Issue
Block a user