diff --git a/resources/i18n/en.json b/resources/i18n/en.json index e8db951518..b951431d8f 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -84,6 +84,13 @@ "auth.messages.expired": "Your session has expired. Please log in again.", "bitstream.edit.title": "Edit 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. This date cannot be modified on this form. To set an embargo date for a bitstream, go to the Item Status tab, click Authorizations..., create or edit the bitstream's READ policy, and set the Start Date 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.dateissued": "By Issue Date", "browse.comcol.by.subject": "By Subject", @@ -733,4 +740,4 @@ "uploader.or": ", or", "uploader.processing": "Processing", "uploader.queue-lenght": "Queue length" -} \ No newline at end of file +} diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html index 518398f690..4b854cb584 100644 --- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html @@ -1,13 +1,22 @@
-
-

{{'bitstream.edit.title' | translate}}

- -
- {{'bitstream.edit.bitstream' | translate}} - {{bitstream.name}} +
+ +
+
+
+
+
+

{{bitstream?.name}} ({{bitstream?.sizeBytes | dsFileSize}})

+
- +
+
diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss new file mode 100644 index 0000000000..d212b5347c --- /dev/null +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss @@ -0,0 +1,8 @@ +:host { + ::ng-deep { + .switch { + position: absolute; + top: $spacer*2.5; + } + } +} diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index e78030d7ff..c74c0005d1 100644 --- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -1,30 +1,248 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs/internal/Observable'; -import { RemoteData } from '../../core/data/remote-data'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; import { ActivatedRoute } from '@angular/router'; 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({ selector: 'ds-edit-bitstream-page', + styleUrls: ['./edit-bitstream-page.component.scss'], templateUrl: './edit-bitstream-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) /** * Page component for editing a bitstream */ -export class EditBitstreamPageComponent implements OnInit { +export class EditBitstreamPageComponent implements OnInit, OnDestroy { /** * The bitstream to edit */ - bitstreamRD$: Observable>; + 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 { - 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(); + } } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 455c1075ef..145ffd8282 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -68,6 +68,8 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a 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 { 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 | null { switch (model.type) { @@ -125,6 +127,9 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type< case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME: return DsDynamicLookupComponent; + case DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH: + return CustomSwitchComponent; + default: return null; } @@ -176,6 +181,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo 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) { if (changes) { super.ngOnChanges(changes); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html new file mode 100644 index 0000000000..9d059b4bee --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html @@ -0,0 +1,20 @@ +
+ + +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts new file mode 100644 index 0000000000..5bab244587 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts @@ -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(); + @Output() remove = new EventEmitter(); + @Output() blur = new EventEmitter(); + @Output() change = new EventEmitter(); + @Output() focus = new EventEmitter(); +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model.ts new file mode 100644 index 0000000000..73360caa3e --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model.ts @@ -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); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 164c0ccce9..835e33e3cf 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -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 { 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 { CustomSwitchComponent } from './form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -265,7 +266,8 @@ const COMPONENTS = [ BrowseByComponent, ItemTypeBadgeComponent, BrowseByComponent, - AbstractTrackableComponent + AbstractTrackableComponent, + CustomSwitchComponent ]; const ENTRY_COMPONENTS = [ @@ -309,7 +311,8 @@ const ENTRY_COMPONENTS = [ StartsWithTextComponent, PlainTextMetadataListElementComponent, ItemMetadataListElementComponent, - MetadataRepresentationListElementComponent + MetadataRepresentationListElementComponent, + CustomSwitchComponent ]; const SHARED_ITEM_PAGE_COMPONENTS = [