64961: Edit bitstream page foundations

This commit is contained in:
Kristof De Langhe
2019-09-12 17:49:45 +02:00
parent a1ac80e53a
commit 91c6b76230
10 changed files with 327 additions and 17 deletions

View File

@@ -84,6 +84,13 @@
"auth.messages.expired": "Your session has expired. Please log in again.", "auth.messages.expired": "Your session has expired. Please log in again.",
"bitstream.edit.title": "Edit bitstream", "bitstream.edit.title": "Edit bitstream",
"bitstream.edit.bitstream": "Bitstream: ", "bitstream.edit.bitstream": "Bitstream: ",
"bitstream.edit.form.description.label": "Description",
"bitstream.edit.form.description.hint": "Optionally, provide a brief description of the file, for example \"Main article\" or \"Experiment data readings\".",
"bitstream.edit.form.embargo.label": "Embargo until specific date",
"bitstream.edit.form.embargo.hint": "The first day from which access is allowed. <b>This date cannot be modified on this form.</b> To set an embargo date for a bitstream, go to the <i>Item Status</i> tab, click <i>Authorizations...<i/>, create or edit the bitstream's <i>READ</i> policy, and set the <i>Start Date</i> as desired.",
"bitstream.edit.form.fileName.label": "Filename",
"bitstream.edit.form.fileName.hint": "Change the filename for the bitstream. Note that this will change the display bitstream URL, but old links will still resolve as long as the sequence ID does not change.",
"bitstream.edit.form.primaryBitstream.label": "Primary bitstream",
"browse.comcol.by.author": "By Author", "browse.comcol.by.author": "By Author",
"browse.comcol.by.dateissued": "By Issue Date", "browse.comcol.by.dateissued": "By Issue Date",
"browse.comcol.by.subject": "By Subject", "browse.comcol.by.subject": "By Subject",

View File

@@ -1,13 +1,22 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 mb-4"> <div class="col-md-2">
<h2>{{'bitstream.edit.title' | translate}}</h2> <ds-thumbnail [thumbnail]="bitstream"></ds-thumbnail>
<ng-container *ngVar="(bitstreamRD$ | async)?.payload as bitstream">
<div *ngIf="bitstream">
<span class="font-weight-bold">{{'bitstream.edit.bitstream' | translate}}</span>
<span>{{bitstream.name}}</span>
</div> </div>
</ng-container> <div class="col-md-10">
<div class="container">
<div class="row">
<div class="col-12">
<h3>{{bitstream?.name}} <span class="text-muted">({{bitstream?.sizeBytes | dsFileSize}})</span></h3>
</div>
</div>
</div>
<ds-form *ngIf="bitstream"
[formId]="'edit-bitstream-form-id'"
[formGroup]="formGroup"
[formModel]="formModel"
[formLayout]="formLayout"
(submitForm)="onSubmit()"></ds-form>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,8 @@
:host {
::ng-deep {
.switch {
position: absolute;
top: $spacer*2.5;
}
}
}

View File

@@ -1,30 +1,248 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../core/data/remote-data';
import { Bitstream } from '../../core/shared/bitstream.model'; import { Bitstream } from '../../core/shared/bitstream.model';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { Subscription } from 'rxjs/internal/Subscription';
import {
DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService,
DynamicInputModel,
DynamicTextAreaModel
} from '@ng-dynamic-forms/core';
import { FormGroup } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
@Component({ @Component({
selector: 'ds-edit-bitstream-page', selector: 'ds-edit-bitstream-page',
styleUrls: ['./edit-bitstream-page.component.scss'],
templateUrl: './edit-bitstream-page.component.html', templateUrl: './edit-bitstream-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
/** /**
* Page component for editing a bitstream * Page component for editing a bitstream
*/ */
export class EditBitstreamPageComponent implements OnInit { export class EditBitstreamPageComponent implements OnInit, OnDestroy {
/** /**
* The bitstream to edit * The bitstream to edit
*/ */
bitstreamRD$: Observable<RemoteData<Bitstream>>; bitstream: Bitstream;
constructor(private route: ActivatedRoute) { /**
* @type {string} Key prefix used to generate form messages
*/
KEY_PREFIX = 'bitstream.edit.form.';
/**
* @type {string} Key suffix used to generate form labels
*/
LABEL_KEY_SUFFIX = '.label';
/**
* @type {string} Key suffix used to generate form labels
*/
HINT_KEY_SUFFIX = '.hint';
/**
* The Dynamic Input Model for the file's name
*/
fileNameModel = new DynamicInputModel({
id: 'fileName',
name: 'fileName',
required: true,
validators: {
required: null
},
errorMessages: {
required: 'You must provide a file name for the bitstream'
}
});
/**
* The Dynamic Switch Model for the file's name
*/
primaryBitstreamModel = new DynamicCustomSwitchModel({
id: 'primaryBitstream',
name: 'primaryBitstream'
});
/**
* The Dynamic TextArea Model for the file's description
*/
descriptionModel = new DynamicTextAreaModel({
id: 'description',
name: 'description'
});
/**
* The Dynamic Input Model for the file's embargo (disabled on this page)
*/
embargoModel = new DynamicInputModel({
id: 'embargo',
name: 'embargo',
disabled: true
});
/**
* All input models in a simple array for easier iterations
*/
inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.embargoModel];
/**
* The dynamic form fields used for editing the information of a bitstream
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
*/
formModel: DynamicFormControlModel[] = [
new DynamicFormGroupModel({
id: 'fileNamePrimaryContainer',
group: [
this.fileNameModel,
this.primaryBitstreamModel
]
}),
new DynamicFormGroupModel({
id: 'descriptionContainer',
group: [
this.descriptionModel
]
}),
new DynamicFormGroupModel({
id: 'embargoContainer',
group: [
this.embargoModel
]
})
];
/**
* Layout used for structuring the form inputs
*/
formLayout: DynamicFormLayout = {
fileName: {
grid: {
host: 'col col-sm-8 d-inline-block'
}
},
primaryBitstream: {
grid: {
host: 'col col-sm-4 d-inline-block switch'
}
},
description: {
grid: {
host: 'col-12 d-inline-block'
}
},
embargo: {
grid: {
host: 'col-12 d-inline-block'
}
},
fileNamePrimaryContainer: {
grid: {
host: 'row position-relative'
}
},
descriptionContainer: {
grid: {
host: 'row'
}
},
embargoContainer: {
grid: {
host: 'row'
}
}
};
/**
* The form group of this form
*/
formGroup: FormGroup;
/**
* The subscription on the bitstream
*/
sub: Subscription;
constructor(private route: ActivatedRoute,
private formService: DynamicFormService,
private translate: TranslateService) {
} }
/**
* Initialize the component
* - Create a FormGroup using the FormModel defined earlier
* - Subscribe on the route data to fetch the bitstream to edit and update the form values
* - Translate the form labels and hints
*/
ngOnInit(): void { ngOnInit(): void {
this.bitstreamRD$ = this.route.data.pipe(map((data) => data.bitstream)); this.formGroup = this.formService.createFormGroup(this.formModel);
this.sub = this.route.data.pipe(map((data) => data.bitstream)).subscribe((bitstreamRD) => {
this.bitstream = bitstreamRD.payload;
this.updateForm(this.bitstream);
});
this.updateFieldTranslations();
this.translate.onLangChange
.subscribe(() => {
this.updateFieldTranslations();
});
}
/**
* Update the current form values with bitstream properties
* @param bitstream
*/
updateForm(bitstream: Bitstream) {
this.formGroup.patchValue({
fileNamePrimaryContainer: {
fileName: bitstream.name,
primaryBitstream: false
},
descriptionContainer: {
description: bitstream.description
}
});
}
/**
* Used to update translations of labels and hints on init and on language change
*/
private updateFieldTranslations() {
this.inputModels.forEach(
(fieldModel: DynamicFormControlModel) => {
this.updateFieldTranslation(fieldModel);
}
);
}
/**
* Update the translations of a DynamicFormControlModel
* @param fieldModel
*/
private updateFieldTranslation(fieldModel) {
fieldModel.label = this.translate.instant(this.KEY_PREFIX + fieldModel.id + this.LABEL_KEY_SUFFIX);
if (fieldModel.id !== this.primaryBitstreamModel.id) {
fieldModel.hint = this.translate.instant(this.KEY_PREFIX + fieldModel.id + this.HINT_KEY_SUFFIX);
}
}
/**
* Check for changes against the bitstream and send update requests to the REST API
*/
onSubmit() {
// TODO: Check for changes against the bitstream and send requests to the REST API accordingly
console.log(this.formGroup.getRawValue());
}
/**
* Unsubscribe from open subscriptions
*/
ngOnDestroy(): void {
if (this.sub) {
this.sub.unsubscribe();
}
} }
} }

View File

@@ -68,6 +68,8 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a
import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model';
import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component';
import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model';
import { CustomSwitchComponent } from './models/custom-switch/custom-switch.component';
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null { export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
switch (model.type) { switch (model.type) {
@@ -125,6 +127,9 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME: case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME:
return DsDynamicLookupComponent; return DsDynamicLookupComponent;
case DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH:
return CustomSwitchComponent;
default: default:
return null; return null;
} }
@@ -176,6 +181,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
super(componentFactoryResolver, layoutService, validationService); super(componentFactoryResolver, layoutService, validationService);
} }
get isCheckbox(): boolean {
return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX || this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH;
}
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes) { if (changes) {
super.ngOnChanges(changes); super.ngOnChanges(changes);

View File

@@ -0,0 +1,20 @@
<div [formGroup]="group" class="form-check custom-control custom-switch" [class.disabled]="model.disabled">
<input type="checkbox" class="form-check-input custom-control-input"
[checked]="model.checked"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[indeterminate]="model.indeterminate"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[required]="model.required"
[tabindex]="model.tabIndex"
[value]="model.value"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"/>
<label class="form-check-label custom-control-label" [for]="bindId && model.id">
<span [innerHTML]="model.label"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></span>
</label>
</div>

View File

@@ -0,0 +1,20 @@
import { DynamicNGBootstrapCheckboxComponent } from '@ng-dynamic-forms/ui-ng-bootstrap';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DynamicCustomSwitchModel } from './custom-switch.model';
@Component({
selector: 'ds-custom-switch',
styleUrls: ['./custom-switch.component.scss'],
templateUrl: './custom-switch.component.html',
})
export class CustomSwitchComponent extends DynamicNGBootstrapCheckboxComponent {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicCustomSwitchModel;
@Output() selected = new EventEmitter<number>();
@Output() remove = new EventEmitter<number>();
@Output() blur = new EventEmitter<any>();
@Output() change = new EventEmitter<any>();
@Output() focus = new EventEmitter<any>();
}

View File

@@ -0,0 +1,16 @@
import {
DynamicCheckboxModel,
DynamicCheckboxModelConfig,
DynamicFormControlLayout,
serializable
} from '@ng-dynamic-forms/core';
export const DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH = 'CUSTOM_SWITCH';
export class DynamicCustomSwitchModel extends DynamicCheckboxModel {
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH;
constructor(config: DynamicCheckboxModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
}
}

View File

@@ -142,6 +142,7 @@ import { AbstractTrackableComponent } from './trackable/abstract-trackable.compo
import { TypedItemSearchResultGridElementComponent } from './object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; import { TypedItemSearchResultGridElementComponent } from './object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component'; import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component';
import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component'; import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component';
import { CustomSwitchComponent } from './form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component';
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -265,7 +266,8 @@ const COMPONENTS = [
BrowseByComponent, BrowseByComponent,
ItemTypeBadgeComponent, ItemTypeBadgeComponent,
BrowseByComponent, BrowseByComponent,
AbstractTrackableComponent AbstractTrackableComponent,
CustomSwitchComponent
]; ];
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
@@ -309,7 +311,8 @@ const ENTRY_COMPONENTS = [
StartsWithTextComponent, StartsWithTextComponent,
PlainTextMetadataListElementComponent, PlainTextMetadataListElementComponent,
ItemMetadataListElementComponent, ItemMetadataListElementComponent,
MetadataRepresentationListElementComponent MetadataRepresentationListElementComponent,
CustomSwitchComponent
]; ];
const SHARED_ITEM_PAGE_COMPONENTS = [ const SHARED_ITEM_PAGE_COMPONENTS = [