From 13e0ded689dc79d7bea9a4c6a023d752b381194d Mon Sep 17 00:00:00 2001 From: Milan Kuchtiak Date: Mon, 13 Oct 2025 16:26:54 +0200 Subject: [PATCH 01/14] Issue 1125: render description field using MardownConfig --- .../clarin-description-item-field.component.html | 3 ++- .../clarin-description-item-field.component.ts | 14 ++++++++++++-- src/config/default-app-config.ts | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.html b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.html index fc0946b71e0..c581ddc2ab6 100644 --- a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.html +++ b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.html @@ -1 +1,2 @@ -
+
+
diff --git a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts index da8c8d4a98b..02866453831 100644 --- a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts +++ b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts @@ -1,6 +1,7 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Inject, Input, OnInit } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; import { makeLinks } from '../../../../shared/clarin-shared-util'; +import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface'; @Component({ selector: 'ds-clarin-description-item-field', @@ -9,6 +10,8 @@ import { makeLinks } from '../../../../shared/clarin-shared-util'; }) export class ClarinDescriptionItemFieldComponent implements OnInit { + constructor(@Inject(APP_CONFIG) private appConfig: AppConfig) {} + /** * The item to display metadata for */ @@ -24,11 +27,18 @@ export class ClarinDescriptionItemFieldComponent implements OnInit { */ validTextMetadata: string; + /** + * This variable will be true if {@link environment.markdown.enabled} is true. + */ + renderMarkdown; + ngOnInit(): void { + this.renderMarkdown = !!this.appConfig.markdown.enabled; + // Store all description metadata values let updatedMVs = []; this.item.allMetadataValues(this.fields).forEach((value) => { - updatedMVs.push(makeLinks(value)); + updatedMVs.push(this.renderMarkdown ? value : makeLinks(value)); }); // Join the metadata values with a line break diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 0dd72b617e4..5717fb27f87 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -407,7 +407,7 @@ export class DefaultAppConfig implements AppConfig { // display in supported metadata fields. By default, only dc.description.abstract is supported. markdown: MarkdownConfig = { enabled: false, - mathjax: false, + mathjax: false }; // Which vocabularies should be used for which search filters From 5e615586b236c7559aef38bfc0e58af4b2ed1732 Mon Sep 17 00:00:00 2001 From: Milan Kuchtiak Date: Tue, 14 Oct 2025 12:34:38 +0200 Subject: [PATCH 02/14] fixed unit test --- .../clarin-description-item-field.component.spec.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts index 9eb04c1b3ae..9960e8b9a7c 100644 --- a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts @@ -3,6 +3,11 @@ import { ClarinDescriptionItemFieldComponent } from './clarin-description-item-f import { Item } from '../../../../core/shared/item.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createPaginatedList } from '../../../../shared/testing/utils.test'; +import {TruncatableService} from "../../../../shared/truncatable/truncatable.service"; +import {DSONameService} from "../../../../core/breadcrumbs/dso-name.service"; +import {DSONameServiceMock} from "../../../../shared/mocks/dso-name.service.mock"; +import {APP_CONFIG} from "../../../../../config/app-config.interface"; +import {environment} from "../../../../../environments/environment"; describe('ClarinDescriptionItemFieldComponent', () => { let component: ClarinDescriptionItemFieldComponent; @@ -22,7 +27,10 @@ describe('ClarinDescriptionItemFieldComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ ClarinDescriptionItemFieldComponent ] + declarations: [ ClarinDescriptionItemFieldComponent ], + providers: [ + { provide: APP_CONFIG, useValue: environment } + ] }) .compileComponents(); From 5140a2f9f35091447f0d30f5b647199f728c0595 Mon Sep 17 00:00:00 2001 From: Milan Kuchtiak Date: Tue, 14 Oct 2025 14:08:51 +0200 Subject: [PATCH 03/14] resolve Copilot comments --- .../clarin-description-item-field.component.spec.ts | 7 ++----- .../clarin-description-item-field.component.ts | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts index 9960e8b9a7c..9b712132898 100644 --- a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts @@ -3,11 +3,8 @@ import { ClarinDescriptionItemFieldComponent } from './clarin-description-item-f import { Item } from '../../../../core/shared/item.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createPaginatedList } from '../../../../shared/testing/utils.test'; -import {TruncatableService} from "../../../../shared/truncatable/truncatable.service"; -import {DSONameService} from "../../../../core/breadcrumbs/dso-name.service"; -import {DSONameServiceMock} from "../../../../shared/mocks/dso-name.service.mock"; -import {APP_CONFIG} from "../../../../../config/app-config.interface"; -import {environment} from "../../../../../environments/environment"; +import {APP_CONFIG} from '../../../../../config/app-config.interface'; +import {environment} from '../../../../../environments/environment'; describe('ClarinDescriptionItemFieldComponent', () => { let component: ClarinDescriptionItemFieldComponent; diff --git a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts index 02866453831..9d78c545b70 100644 --- a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts +++ b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts @@ -28,7 +28,7 @@ export class ClarinDescriptionItemFieldComponent implements OnInit { validTextMetadata: string; /** - * This variable will be true if {@link environment.markdown.enabled} is true. + * This variable will be true if {@link appConfig.markdown.enabled} is true. */ renderMarkdown; From 0fa034b9374795e50902f74b64613f15898095aa Mon Sep 17 00:00:00 2001 From: Milan Kuchtiak Date: Wed, 15 Oct 2025 07:38:44 +0200 Subject: [PATCH 04/14] formatting --- .../clarin-description-item-field.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts index 9b712132898..d4f958c0cb7 100644 --- a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts @@ -3,8 +3,8 @@ import { ClarinDescriptionItemFieldComponent } from './clarin-description-item-f import { Item } from '../../../../core/shared/item.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createPaginatedList } from '../../../../shared/testing/utils.test'; -import {APP_CONFIG} from '../../../../../config/app-config.interface'; -import {environment} from '../../../../../environments/environment'; +import { APP_CONFIG } from '../../../../../config/app-config.interface'; +import { environment } from '../../../../../environments/environment'; describe('ClarinDescriptionItemFieldComponent', () => { let component: ClarinDescriptionItemFieldComponent; From 2b0f2f15eea90f5f8625df3d1bf7a8324ca3e6b2 Mon Sep 17 00:00:00 2001 From: Milan Kuchtiak Date: Wed, 15 Oct 2025 10:57:04 +0200 Subject: [PATCH 05/14] fix randomly failing test --- .../clarin-description-item-field.component.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts index d4f958c0cb7..cec72e9f52c 100644 --- a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.spec.ts @@ -5,6 +5,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-dat import { createPaginatedList } from '../../../../shared/testing/utils.test'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { environment } from '../../../../../environments/environment'; +import { MarkdownPipe } from '../../../../shared/utils/markdown.pipe'; describe('ClarinDescriptionItemFieldComponent', () => { let component: ClarinDescriptionItemFieldComponent; @@ -24,7 +25,7 @@ describe('ClarinDescriptionItemFieldComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ ClarinDescriptionItemFieldComponent ], + declarations: [ ClarinDescriptionItemFieldComponent, MarkdownPipe ], providers: [ { provide: APP_CONFIG, useValue: environment } ] From 544f1a8fe800f864cd1087a327255df340bd391f Mon Sep 17 00:00:00 2001 From: Milan Kuchtiak Date: Thu, 16 Oct 2025 16:52:02 +0200 Subject: [PATCH 06/14] UI components use the local.description.usemarkdown property to handle markdown text, in the Home page - Add toggle button to render markdown formatted text --- ...clarin-description-item-field.component.ts | 12 +++++++-- .../clarin-item-box-view.component.html | 17 ++++++++++++- .../clarin-item-box-view.component.spec.ts | 6 ++++- .../clarin-item-box-view.component.ts | 25 +++++++++++++++++-- src/assets/i18n/cs.json5 | 2 ++ src/assets/i18n/en.json5 | 3 ++- 6 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts index 9d78c545b70..95281e7600d 100644 --- a/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts +++ b/src/app/item-page/simple/field-components/clarin-description-item-field/clarin-description-item-field.component.ts @@ -30,10 +30,10 @@ export class ClarinDescriptionItemFieldComponent implements OnInit { /** * This variable will be true if {@link appConfig.markdown.enabled} is true. */ - renderMarkdown; + renderMarkdown: boolean; ngOnInit(): void { - this.renderMarkdown = !!this.appConfig.markdown.enabled; + this.renderMarkdown = !!this.appConfig.markdown.enabled && this.markdownEnabled(); // Store all description metadata values let updatedMVs = []; @@ -45,4 +45,12 @@ export class ClarinDescriptionItemFieldComponent implements OnInit { this.validTextMetadata = updatedMVs.join('
'); } + /** + * Check if the item uses Markdown to render description text. + * */ + private markdownEnabled() { + const useMarkdown = this.item.metadata?.['local.description.usemarkdown']?.[0]?.value; + return useMarkdown !== undefined && useMarkdown.toLowerCase() === 'yes'; + } + } diff --git a/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.html b/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.html index 50066833e28..baf8981df69 100644 --- a/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.html +++ b/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.html @@ -28,7 +28,22 @@
{{'item.view.box.description.message' | translate}}
-
{{itemDescription}}
+ +
{{itemDescription}}
+
+
+
+
+
+
+
diff --git a/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.spec.ts b/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.spec.ts index f813dc7e99b..f6a346541e5 100644 --- a/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.spec.ts +++ b/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.spec.ts @@ -14,6 +14,9 @@ import { ClarinLicenseDataService } from 'src/app/core/data/clarin/clarin-licens import { ClarinDateService } from '../clarin-date.service'; import { DomSanitizer } from '@angular/platform-browser'; import { DSONameServiceMock } from '../mocks/dso-name.service.mock'; +import { MarkdownPipe } from '../utils/markdown.pipe'; +import { APP_CONFIG } from '../../../config/app-config.interface'; +import { environment } from '../../../environments/environment'; describe('ClarinItemBoxViewComponent', () => { let component: ClarinItemBoxViewComponent; @@ -60,7 +63,7 @@ describe('ClarinItemBoxViewComponent', () => { }), StoreModule.forRoot(), ], - declarations: [ClarinItemBoxViewComponent], + declarations: [ClarinItemBoxViewComponent, MarkdownPipe], providers: [ { provide: CollectionDataService, useValue: collectionDataServiceMock }, { provide: BundleDataService, useValue: bundleDataServiceMock }, @@ -75,6 +78,7 @@ describe('ClarinItemBoxViewComponent', () => { { provide: ClarinDateService, useValue: clarinDateServiceMock }, { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: DomSanitizer, useValue: sanitizerStub }, + { provide: APP_CONFIG, useValue: environment }, provideMockStore({ initialState }), ], }).compileComponents(); diff --git a/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.ts b/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.ts index bfb268217f3..a4ed1009c46 100644 --- a/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.ts +++ b/src/app/shared/clarin-item-box-view/clarin-item-box-view.component.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line max-classes-per-file -import { Component, Input, OnInit } from '@angular/core'; +import {Component, Inject, Input, OnInit} from '@angular/core'; import { Item } from '../../core/shared/item.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { @@ -31,6 +31,7 @@ import { getItemPageRoute } from '../../item-page/item-page-routing-paths'; import { FindListOptions } from '../../core/data/find-list-options.model'; import { ClarinDateService } from '../clarin-date.service'; import { AUTHOR_METADATA_FIELDS } from '../../core/shared/clarin/constants'; +import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; /** * Show item on the Home/Search page in the customized box with Item's information. @@ -123,7 +124,13 @@ export class ClarinItemBoxViewComponent implements OnInit { */ licenseLabelIcons: BehaviorSubject = new BehaviorSubject([]); - constructor(protected collectionService: CollectionDataService, + /** + * This variable will be true if {@link appConfig.markdown.enabled} is true. + */ + renderMarkdown: boolean; + + constructor(@Inject(APP_CONFIG) private appConfig: AppConfig, + protected collectionService: CollectionDataService, protected bundleService: BundleDataService, protected dsoNameService: DSONameService, protected configurationService: ConfigurationDataService, @@ -132,6 +139,7 @@ export class ClarinItemBoxViewComponent implements OnInit { private clarinDateService: ClarinDateService) { } async ngOnInit(): Promise { + if (this.object instanceof Item) { this.item = this.object; } else if (this.object instanceof ItemSearchResult) { @@ -140,6 +148,8 @@ export class ClarinItemBoxViewComponent implements OnInit { return; } + this.renderMarkdown = !!this.appConfig.markdown.enabled && this.markdownEnabled(); + // Load Items metadata this.itemType = this.item?.firstMetadataValue('dc.type'); this.itemName = this.item?.firstMetadataValue('dc.title'); @@ -262,6 +272,17 @@ export class ClarinItemBoxViewComponent implements OnInit { return this.itemCountOfFiles.value > 1; } + /** + * Check if the item uses Markdown to render description text. + * */ + private markdownEnabled() { + if (isNull(this.item)) { + return false; + } + const useMarkdown = this.item.metadata?.['local.description.usemarkdown']?.[0]?.value; + return useMarkdown !== undefined && useMarkdown.toLowerCase() === 'yes'; + } + handleImageError(event) { const imgElement = event.target as HTMLImageElement; imgElement.src = diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 8df0dbcdb61..e1e4d3d7d01 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -3418,6 +3418,8 @@ "item.view.box.one.file.message": "Tento záznam obsahuje 1 soubor", // "item.view.box.no-files.message": "This item contains no files.", "item.view.box.no-files.message": "Tento záznam neobsahuje soubory.", + // "item.view.box.markdown.formatted.text": "Markdown formatted text", + "item.view.box.markdown.formatted.text": "Text ve formátu Markdown", // "item.view.box.description.message": "Description:", "item.view.box.description.message": "Popis:", // "item.view.box.author.preview.and": "and:", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2de8c441413..edc09281bb4 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2869,8 +2869,9 @@ "item.view.box.no-files.message": "This item contains no files.", - "item.view.box.description.message": "Description:", + "item.view.box.markdown.formatted.text": "Markdown formatted text", + "item.view.box.description.message": "Description:", "item.view.box.author.preview.and": "and", From 7484ba2c2f2c74679d771bfae03966b876a77594 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Mon, 2 Mar 2026 14:51:20 +0100 Subject: [PATCH 07/14] Add delay to cc-license spec assertion Update the SubmissionSectionCcLicensesComponent spec to wait before asserting the getCcLicenseLink call. The test now uses a setTimeout(350) and the Jasmine done callback, calling fixture.detectChanges inside the timeout before verifying submissionCcLicenseUrlDataService.getCcLicenseLink was invoked with the expected Map. This change prevents timing-related flakiness by allowing async updates to complete before the assertion. --- ...sion-section-cc-licenses.component.spec.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts index dc9bf3c028e..904e181af76 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts @@ -242,14 +242,18 @@ describe('SubmissionSectionCcLicensesComponent', () => { fixture.detectChanges(); }); - it('should call the submission cc licenses data service getCcLicenseLink method', () => { - expect(submissionCcLicenseUrlDataService.getCcLicenseLink).toHaveBeenCalledWith( - ccLicence, - new Map([ - [ccLicence.fields[0], ccLicence.fields[0].enums[1]], - [ccLicence.fields[1], ccLicence.fields[1].enums[0]], - ]) - ); + it('should call the submission cc licenses data service getCcLicenseLink method', (done) => { + setTimeout(() => { + fixture.detectChanges(); + expect(submissionCcLicenseUrlDataService.getCcLicenseLink).toHaveBeenCalledWith( + ccLicence, + new Map([ + [ccLicence.fields[0], ccLicence.fields[0].enums[1]], + [ccLicence.fields[1], ccLicence.fields[1].enums[0]], + ]) + ); + done(); + }, 350); }); it('should display a cc license link', (done) => { From 635499bfe87d6ea7c43ed983f59ef28fe80ece16 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Wed, 4 Mar 2026 09:30:13 +0100 Subject: [PATCH 08/14] Add markdown preview toggle for description fields Add markdown preview toggle for description fields --- ...-edit-metadata-field-values.component.html | 2 + ...it-metadata-field-values.component.spec.ts | 22 ++++ ...so-edit-metadata-field-values.component.ts | 28 ++++ .../dso-edit-metadata-value.component.html | 19 ++- .../dso-edit-metadata-value.component.scss | 10 ++ .../dso-edit-metadata-value.component.spec.ts | 31 +++++ .../dso-edit-metadata-value.component.ts | 70 +++++++++- .../dso-edit-metadata.component.html | 2 + .../dso-edit-metadata.component.ts | 30 ++++- ...amic-form-control-container.component.html | 19 ++- ...amic-form-control-container.component.scss | 14 ++ ...c-form-control-container.component.spec.ts | 117 +++++++++++++++++ ...ynamic-form-control-container.component.ts | 120 ++++++++++++++++++ .../models/ds-dynamic-textarea.model.ts | 3 + .../parsers/textarea-field-parser.spec.ts | 25 ++++ .../builder/parsers/textarea-field-parser.ts | 13 ++ src/assets/i18n/cs.json5 | 6 + src/assets/i18n/en.json5 | 4 + 18 files changed, 529 insertions(+), 6 deletions(-) diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.html index 9f74216d54f..46cf12e6d44 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.html @@ -3,6 +3,8 @@ { }); }); + describe('isLocalDescriptionUseMarkdownEnabled', () => { + it('should return false when local.description.usemarkdown is missing', () => { + expect(component.isLocalDescriptionUseMarkdownEnabled()).toBeFalse(); + }); + + it('should return true when local.description.usemarkdown is yes', () => { + form = new DsoEditMetadataForm({ + ...dso.metadata, + 'local.description.usemarkdown': [ + Object.assign(new MetadataValue(), { + value: 'yes', + language: 'en', + place: 0, + }), + ], + }); + component.form = form; + + expect(component.isLocalDescriptionUseMarkdownEnabled()).toBeTrue(); + }); + }); + describe('dropping a value on a different index', () => { beforeEach(() => { component.drop(Object.assign({ diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts index 2702e0ba1f8..054e0095ecb 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts @@ -14,6 +14,8 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; * Component displaying table rows for each value for a certain metadata field within a form */ export class DsoEditMetadataFieldValuesComponent { + protected readonly localDescriptionUseMarkdownMetadataKey = 'local.description.usemarkdown'; + /** * The parent {@link DSpaceObject} to display a metadata form for * Also used to determine metadata-representations in case of virtual metadata @@ -57,6 +59,32 @@ export class DsoEditMetadataFieldValuesComponent { */ public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType; + /** + * Returns whether local.description.usemarkdown exists in form metadata and explicitly enables markdown. + */ + isLocalDescriptionUseMarkdownEnabled(): boolean { + const useMarkdownValues = this.form?.fields?.[this.localDescriptionUseMarkdownMetadataKey]; + + if (!Array.isArray(useMarkdownValues) || useMarkdownValues.length === 0) { + return false; + } + + return useMarkdownValues.some((metadataValue: DsoEditMetadataValue) => { + const value = metadataValue?.newValue?.value; + + if (typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + const normalizedValue = value.toLowerCase(); + return normalizedValue === 'yes' || normalizedValue === 'true'; + } + + return value === true; + }); + } + /** * Drop a value into a new position * Update the form's value array for the current field to match the dropped position diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html index f96759a75ef..f02dd0d39c5 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html @@ -1,11 +1,26 @@
-
+
{{ mdValue.newValue.value }}
- +
+
+
{{ mdRepresentationName$ | async }} diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss index e5551913005..47defd6cb6d 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss @@ -14,3 +14,13 @@ .cdk-drag-placeholder { opacity: 0; } + +.ds-markdown-mode-toggle { + margin-bottom: var(--ds-spacer-1); +} + +.ds-markdown-preview-container { + min-height: 9rem; + overflow-y: auto; + white-space: normal; +} diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts index e90e7391f87..3224e9a1ee8 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts @@ -12,6 +12,8 @@ import { MetadataValue, VIRTUAL_METADATA_PREFIX } from '../../../core/shared/met import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form'; import { By } from '@angular/platform-browser'; import {BtnDisabledDirective} from '../../../shared/btn-disabled.directive'; +import { APP_CONFIG } from '../../../../config/app-config.interface'; +import { environment } from '../../../../environments/environment'; const EDIT_BTN = 'edit'; const CONFIRM_BTN = 'confirm'; @@ -55,6 +57,7 @@ describe('DsoEditMetadataValueComponent', () => { providers: [ { provide: RelationshipDataService, useValue: relationshipService }, { provide: DSONameService, useValue: dsoNameService }, + { provide: APP_CONFIG, useValue: environment }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -64,10 +67,38 @@ describe('DsoEditMetadataValueComponent', () => { fixture = TestBed.createComponent(DsoEditMetadataValueComponent); component = fixture.componentInstance; component.mdValue = editMetadataValue; + component.mdField = 'dc.description'; + component.markdownEnabledForForm = true; component.saving$ = of(false); fixture.detectChanges(); }); + describe('markdown preview toggle', () => { + beforeEach(() => { + editMetadataValue.editing = true; + const appConfig = TestBed.inject(APP_CONFIG) as any; + appConfig.markdown.enabled = true; + fixture.detectChanges(); + }); + + it('should show toggle for description fields when metadata markdown is enabled', () => { + expect(component.canShowMarkdownPreviewToggle()).toBeTrue(); + }); + + it('should hide toggle when markdown metadata gate is disabled', () => { + component.markdownEnabledForForm = false; + + expect(component.canShowMarkdownPreviewToggle()).toBeFalse(); + }); + + it('should enable preview mode and return current value', () => { + component.setMarkdownPreviewMode(true); + + expect(component.isMarkdownPreviewModeEnabled()).toBeTrue(); + expect(component.getMarkdownPreviewValue()).toBe('Regular Name'); + }); + }); + it('should not show a badge', () => { expect(fixture.debugElement.query(By.css('ds-themed-type-badge'))).toBeNull(); }); diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts index 3fdcd381abc..e79581f9664 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form'; import { Observable } from 'rxjs/internal/Observable'; import { @@ -12,6 +12,7 @@ import { map } from 'rxjs/operators'; import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { EMPTY } from 'rxjs/internal/observable/empty'; +import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface'; @Component({ selector: 'ds-dso-edit-metadata-value', @@ -22,6 +23,12 @@ import { EMPTY } from 'rxjs/internal/observable/empty'; * Component displaying a single editable row for a metadata value */ export class DsoEditMetadataValueComponent implements OnInit { + protected readonly markdownDescriptionMetadataAllowList: string[] = [ + 'description', + 'dc.description', + 'dc.description.abstract' + ]; + /** * The parent {@link DSpaceObject} to display a metadata form for * Also used to determine metadata-representations in case of virtual metadata @@ -33,6 +40,16 @@ export class DsoEditMetadataValueComponent implements OnInit { */ @Input() mdValue: DsoEditMetadataValue; + /** + * Metadata field this value belongs to + */ + @Input() mdField: string; + + /** + * Whether local.description.usemarkdown is available in the form and enabled + */ + @Input() markdownEnabledForForm = false; + /** * Type of DSO we're displaying values for * Determines i18n messages @@ -97,8 +114,14 @@ export class DsoEditMetadataValueComponent implements OnInit { */ mdRepresentationName$: Observable; + /** + * Whether markdown preview mode is enabled while editing this value + */ + isMarkdownPreviewMode = false; + constructor(protected relationshipService: RelationshipDataService, - protected dsoNameService: DSONameService) { + protected dsoNameService: DSONameService, + @Inject(APP_CONFIG) protected appConfig: AppConfig) { } ngOnInit(): void { @@ -123,4 +146,47 @@ export class DsoEditMetadataValueComponent implements OnInit { map((mdRepresentation: ItemMetadataRepresentation) => mdRepresentation ? this.dsoNameService.getName(mdRepresentation) : null), ); } + + /** + * Returns whether markdown toggle should be shown for this value row. + */ + canShowMarkdownPreviewToggle(): boolean { + return this.mdValue?.editing + && this.appConfig?.markdown?.enabled + && this.markdownEnabledForForm + && this.isDescriptionField(); + } + + /** + * Returns whether preview mode is currently active. + */ + isMarkdownPreviewModeEnabled(): boolean { + return this.canShowMarkdownPreviewToggle() && this.isMarkdownPreviewMode; + } + + /** + * Enable or disable markdown preview mode. + */ + setMarkdownPreviewMode(enabled: boolean): void { + this.isMarkdownPreviewMode = enabled; + } + + /** + * Returns current editable metadata value as markdown preview text. + */ + getMarkdownPreviewValue(): string { + const value = this.mdValue?.newValue?.value; + return typeof value === 'string' ? value : ''; + } + + /** + * Returns true when this row belongs to a supported description field. + */ + protected isDescriptionField(): boolean { + if (typeof this.mdField !== 'string') { + return false; + } + + return this.markdownDescriptionMetadataAllowList.includes(this.mdField); + } } diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html index 1598558e513..d0755447e29 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html @@ -41,6 +41,8 @@ { + const value = metadataValue?.newValue?.value; + + if (typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + const normalizedValue = value.toLowerCase(); + return normalizedValue === 'yes' || normalizedValue === 'true'; + } + + return value === true; + }); + } + /** * Submit the current changes to the form by retrieving json PATCH operations from the form and sending it to the * DSpaceObject's data-service diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 606db29db37..925c6d01d0c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -12,10 +12,27 @@
-
+
+ + +
+ +
+
+ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.scss index 4e58759f4e7..adbb2171e75 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.scss @@ -14,3 +14,17 @@ -moz-appearance: none; appearance: none; } + +.markdown-preview-toggle { + .btn.active { + pointer-events: none; + } +} + +.markdown-preview-box { + max-height: 24rem; + min-height: 3rem; + overflow: auto; + padding: 0.75rem; + white-space: pre-wrap; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index 355e10b9a09..fe3d8061e2c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -377,4 +377,121 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { expect(testFn(formModel[25])).toEqual(DsDynamicFormGroupComponent); }); + it('should show markdown preview toggle for eligible textarea when markdown is enabled', () => { + const appConfig = TestBed.inject(APP_CONFIG) as any; + const previousMarkdownEnabled = appConfig.markdown.enabled; + appConfig.markdown.enabled = true; + + const textareaModel: any = formModel[14]; + textareaModel.supportsMarkdownPreview = true; + textareaModel.metadataFields = ['dc.description']; + + formGroup.addControl('local_description_usemarkdown', new UntypedFormControl('yes')); + + component.model = textareaModel; + component.group = formGroup; + component.ngOnChanges({ + group: new SimpleChange(null, component.group, false), + model: new SimpleChange(null, component.model, false) + }); + + expect(component.canShowMarkdownPreviewToggle()).toBe(true); + + appConfig.markdown.enabled = previousMarkdownEnabled; + }); + + it('should hide markdown preview toggle when markdown is disabled', () => { + const appConfig = TestBed.inject(APP_CONFIG) as any; + const previousMarkdownEnabled = appConfig.markdown.enabled; + appConfig.markdown.enabled = false; + + const textareaModel: any = formModel[14]; + textareaModel.supportsMarkdownPreview = true; + textareaModel.metadataFields = ['dc.description']; + + formGroup.addControl('local_description_usemarkdown', new UntypedFormControl('yes')); + + component.model = textareaModel; + component.group = formGroup; + component.ngOnChanges({ + group: new SimpleChange(null, component.group, false), + model: new SimpleChange(null, component.model, false) + }); + + expect(component.canShowMarkdownPreviewToggle()).toBe(false); + + appConfig.markdown.enabled = previousMarkdownEnabled; + }); + + it('should switch to markdown preview mode and return current textarea value', () => { + const appConfig = TestBed.inject(APP_CONFIG) as any; + const previousMarkdownEnabled = appConfig.markdown.enabled; + appConfig.markdown.enabled = true; + + const textareaModel: any = formModel[14]; + textareaModel.supportsMarkdownPreview = true; + textareaModel.metadataFields = ['dc.description']; + + formGroup.addControl('local_description_usemarkdown', new UntypedFormControl('yes')); + + component.model = textareaModel; + component.group = formGroup; + component.ngOnChanges({ + group: new SimpleChange(null, component.group, false), + model: new SimpleChange(null, component.model, false) + }); + + component.control.setValue('# Title'); + component.setMarkdownPreviewMode(true); + + expect(component.isMarkdownPreviewModeEnabled()).toBe(true); + expect(component.getMarkdownPreviewValue()).toBe('# Title'); + + appConfig.markdown.enabled = previousMarkdownEnabled; + }); + + it('should hide markdown preview toggle when local.description.usemarkdown is set to no', () => { + const appConfig = TestBed.inject(APP_CONFIG) as any; + const previousMarkdownEnabled = appConfig.markdown.enabled; + appConfig.markdown.enabled = true; + + const textareaModel: any = formModel[14]; + textareaModel.supportsMarkdownPreview = true; + textareaModel.metadataFields = ['dc.description']; + + formGroup.addControl('local_description_usemarkdown', new UntypedFormControl('no')); + + component.model = textareaModel; + component.group = formGroup; + component.ngOnChanges({ + group: new SimpleChange(null, component.group, false), + model: new SimpleChange(null, component.model, false) + }); + + expect(component.canShowMarkdownPreviewToggle()).toBe(false); + + appConfig.markdown.enabled = previousMarkdownEnabled; + }); + + it('should hide markdown preview toggle when local.description.usemarkdown control is missing', () => { + const appConfig = TestBed.inject(APP_CONFIG) as any; + const previousMarkdownEnabled = appConfig.markdown.enabled; + appConfig.markdown.enabled = true; + + const textareaModel: any = formModel[14]; + textareaModel.supportsMarkdownPreview = true; + textareaModel.metadataFields = ['dc.description']; + + component.model = textareaModel; + component.group = formGroup; + component.ngOnChanges({ + group: new SimpleChange(null, component.group, false), + model: new SimpleChange(null, component.model, false) + }); + + expect(component.canShowMarkdownPreviewToggle()).toBe(false); + + appConfig.markdown.enabled = previousMarkdownEnabled; + }); + }); 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 f2ea9c3baf2..f9b92926f2d 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 @@ -125,6 +125,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_AUTOCOMPLETE } from './models/autocomplete/ds import { DsDynamicSponsorAutocompleteComponent } from './models/sponsor-autocomplete/ds-dynamic-sponsor-autocomplete.component'; import { SPONSOR_METADATA_NAME } from './models/ds-dynamic-complex.model'; import { DsDynamicSponsorScrollableDropdownComponent } from './models/sponsor-scrollable-dropdown/dynamic-sponsor-scrollable-dropdown.component'; +import { DsDynamicTextAreaModel } from './models/ds-dynamic-textarea.model'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { @@ -210,6 +211,12 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type< changeDetection: ChangeDetectionStrategy.Default }) export class DsDynamicFormControlContainerComponent extends DynamicFormControlContainerComponent implements OnInit, OnChanges, OnDestroy { + protected readonly markdownDescriptionMetadataAllowList: string[] = [ + 'description', + 'dc.description', + 'dc.description.abstract' + ]; + @ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList; // eslint-disable-next-line @angular-eslint/no-input-rename @Input('templates') inputTemplateList: QueryList; @@ -254,6 +261,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo */ fetchThumbnail: boolean; + /** + * Whether markdown preview mode is enabled for the current control. + */ + isMarkdownPreviewMode = false; + get componentType(): Type | null { return dsDynamicFormControlMapFn(this.model); } @@ -511,6 +523,114 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo return isNotEmpty(this.model.hint) && this.model.hint !== ' '; } + /** + * Checks if markdown preview toggle can be shown for this control. + */ + canShowMarkdownPreviewToggle(): boolean { + return this.isMarkdownPreviewSupported() && !this.model.readOnly; + } + + /** + * Checks if markdown preview should be displayed. + */ + isMarkdownPreviewModeEnabled(): boolean { + return this.canShowMarkdownPreviewToggle() && this.isMarkdownPreviewMode; + } + + /** + * Enable/disable markdown preview mode. + */ + setMarkdownPreviewMode(enabled: boolean): void { + this.isMarkdownPreviewMode = enabled; + } + + /** + * Returns current control value as a string for markdown rendering. + */ + getMarkdownPreviewValue(): string { + const value = this.control?.value ?? this.model?.value; + if (typeof value === 'string') { + return value; + } + if (hasValue(value?.value) && typeof value.value === 'string') { + return value.value; + } + if (Array.isArray(value) && value.length > 0) { + return value + .map((entry) => { + if (typeof entry === 'string') { + return entry; + } + if (hasValue(entry?.value) && typeof entry.value === 'string') { + return entry.value; + } + return ''; + }) + .filter((entry: string) => isNotEmpty(entry)) + .join('\n'); + } + return ''; + } + + /** + * Checks if the current control is an eligible textarea for markdown preview. + */ + protected isMarkdownPreviewSupported(): boolean { + if (this.model?.type !== DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA) { + return false; + } + if (!this.appConfig?.markdown?.enabled) { + return false; + } + if (!this.isDescriptionTextareaField()) { + return false; + } + + return this.isLocalDescriptionUseMarkdownEnabled(); + } + + /** + * Check whether this textarea is configured for one of the known description metadata fields. + */ + protected isDescriptionTextareaField(): boolean { + const textareaModel = this.model as DsDynamicTextAreaModel; + if (textareaModel?.supportsMarkdownPreview === true) { + return true; + } + + const metadataFields = this.model?.metadataFields || []; + return metadataFields.some((metadataField: string) => this.markdownDescriptionMetadataAllowList.includes(metadataField)); + } + + /** + * Check if local.description.usemarkdown exists in form state and enables markdown rendering. + */ + protected isLocalDescriptionUseMarkdownEnabled(): boolean { + const useMarkdownControl = this.group?.root?.get('local_description_usemarkdown') + || this.formGroup?.root?.get('local_description_usemarkdown') + || this.group?.get('local_description_usemarkdown'); + + if (!hasValue(useMarkdownControl)) { + return false; + } + + const value = useMarkdownControl.value?.value ?? useMarkdownControl.value; + if (!hasValue(value)) { + return false; + } + + if (typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + const normalizedValue = value.toLowerCase(); + return normalizedValue === 'yes' || normalizedValue === 'true'; + } + + return false; + } + /** * Initialize this.item$ based on this.model.submissionId */ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model.ts index 00d385edefe..a36addec876 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model.ts @@ -5,12 +5,14 @@ export interface DsDynamicTextAreaModelConfig extends DsDynamicInputModelConfig cols?: number; rows?: number; wrap?: string; + supportsMarkdownPreview?: boolean; } export class DsDynamicTextAreaModel extends DsDynamicInputModel { @serializable() cols: number; @serializable() rows: number; @serializable() wrap: string; + @serializable() supportsMarkdownPreview: boolean; @serializable() type = DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA; constructor(config: DsDynamicTextAreaModelConfig, layout?: DynamicFormControlLayout) { @@ -19,6 +21,7 @@ export class DsDynamicTextAreaModel extends DsDynamicInputModel { this.cols = config.cols; this.rows = config.rows; this.wrap = config.wrap; + this.supportsMarkdownPreview = config.supportsMarkdownPreview; } } diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts index 259f8a60e14..643cbdf8c62 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts @@ -66,4 +66,29 @@ describe('TextareaFieldParser test suite', () => { expect(fieldModel.value).toEqual(expectedValue); }); + it('should enable markdown preview support for description metadata fields', () => { + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions, translateService); + + const fieldModel = parser.parse(); + + expect(fieldModel.supportsMarkdownPreview).toBe(true); + }); + + it('should disable markdown preview support for non-description metadata fields', () => { + field.selectableMetadata = [ + { + metadata: 'dc.title', + label: 'Title', + controlledVocabulary: null, + closed: false + } + ]; + + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions, translateService); + + const fieldModel = parser.parse(); + + expect(fieldModel.supportsMarkdownPreview).toBe(false); + }); + }); diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.ts index 548ce567c3e..62c73cd1fc7 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.ts @@ -8,6 +8,11 @@ import { import { environment } from '../../../../../environments/environment'; export class TextareaFieldParser extends FieldParser { + protected readonly markdownDescriptionMetadataAllowList: string[] = [ + 'description', + 'dc.description', + 'dc.description.abstract' + ]; public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { const textAreaModelConfig: DsDynamicTextAreaModelConfig = this.initModel(null, label); @@ -22,9 +27,17 @@ export class TextareaFieldParser extends FieldParser { textAreaModelConfig.rows = 10; textAreaModelConfig.spellCheck = environment.form.spellCheck; + textAreaModelConfig.supportsMarkdownPreview = this.isDescriptionMetadataField(textAreaModelConfig.metadataFields); this.setValues(textAreaModelConfig, fieldValue); const textAreaModel = new DsDynamicTextAreaModel(textAreaModelConfig, layout); return textAreaModel; } + + /** + * Check if any metadata field used by this textarea represents a description field. + */ + protected isDescriptionMetadataField(metadataFields: string[] = []): boolean { + return metadataFields.some((metadataField: string) => this.markdownDescriptionMetadataAllowList.includes(metadataField)); + } } diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index a3e821524d2..3d42ec8852c 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -2657,6 +2657,12 @@ // "form.edit": "Edit", "form.edit": "Upravit", + // "submission.form.markdown.toggle.edit": "Edit", + "submission.form.markdown.toggle.edit": "Upravit", + + // "submission.form.markdown.toggle.preview": "Preview", + "submission.form.markdown.toggle.preview": "Náhled", + // "form.edit-help": "Click here to edit the selected value", "form.edit-help": "Klikněte zde pro úpravu vybrané hodnoty", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index a64bac54ca2..5bbe4a459a9 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1771,6 +1771,10 @@ "form.edit": "Edit", + "submission.form.markdown.toggle.edit": "Edit", + + "submission.form.markdown.toggle.preview": "Preview", + "form.edit-help": "Click here to edit the selected value", "form.first-name": "First name", From 07169b76aafde6275db6ef90369adbf6978d1062 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Wed, 4 Mar 2026 12:54:38 +0100 Subject: [PATCH 09/14] Detect nested markdown toggle and add test Detect nested markdown toggle and add test --- config/config.yml | 4 ++ ...c-form-control-container.component.spec.ts | 33 +++++++++++ ...ynamic-form-control-container.component.ts | 59 +++++++++++++++++-- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/config/config.yml b/config/config.yml index 9257bd2b09d..35bac0acb45 100644 --- a/config/config.yml +++ b/config/config.yml @@ -171,3 +171,7 @@ languages: - code: uk label: Yкраї́нська active: false + +markdown: + enabled: true + mathjax: true diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index fe3d8061e2c..034918ba5ed 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -494,4 +494,37 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { appConfig.markdown.enabled = previousMarkdownEnabled; }); + it('should show markdown preview toggle when usemarkdown is in a sibling row-group with object value', () => { + const appConfig = TestBed.inject(APP_CONFIG) as any; + const previousMarkdownEnabled = appConfig.markdown.enabled; + appConfig.markdown.enabled = true; + + const textareaModel: any = new DynamicTextAreaModel({ id: 'dc_description' }); + textareaModel.supportsMarkdownPreview = true; + textareaModel.metadataFields = ['dc.description']; + + const descriptionGroup = new UntypedFormGroup({ + dc_description: new UntypedFormControl('# something') + }); + const markdownGroup = new UntypedFormGroup({ + local_description_usemarkdown: new UntypedFormControl({ local_description_usemarkdown_yes: true }) + }); + const rootFormGroup = new UntypedFormGroup({ + 'df-row-group-config-22': descriptionGroup, + 'df-row-group-config-23': markdownGroup + }); + + component.model = textareaModel; + component.group = descriptionGroup; + component.formGroup = rootFormGroup; + component.ngOnChanges({ + group: new SimpleChange(null, component.group, false), + model: new SimpleChange(null, component.model, false) + }); + + expect(component.canShowMarkdownPreviewToggle()).toBe(true); + + appConfig.markdown.enabled = previousMarkdownEnabled; + }); + }); 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 f9b92926f2d..cb7b0f46a46 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 @@ -17,7 +17,7 @@ import { ViewChild, ViewContainerRef } from '@angular/core'; -import { UntypedFormArray, UntypedFormGroup } from '@angular/forms'; +import { AbstractControl, UntypedFormArray, UntypedFormGroup } from '@angular/forms'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, @@ -585,7 +585,6 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo if (!this.isDescriptionTextareaField()) { return false; } - return this.isLocalDescriptionUseMarkdownEnabled(); } @@ -608,13 +607,52 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo protected isLocalDescriptionUseMarkdownEnabled(): boolean { const useMarkdownControl = this.group?.root?.get('local_description_usemarkdown') || this.formGroup?.root?.get('local_description_usemarkdown') - || this.group?.get('local_description_usemarkdown'); + || this.group?.get('local_description_usemarkdown') + || this.findNestedControlByKey(this.group?.root, 'local_description_usemarkdown') + || this.findNestedControlByKey(this.formGroup?.root, 'local_description_usemarkdown') + || this.findNestedControlByKey(this.group, 'local_description_usemarkdown'); if (!hasValue(useMarkdownControl)) { return false; } - const value = useMarkdownControl.value?.value ?? useMarkdownControl.value; + const rawValue = useMarkdownControl.value?.value ?? useMarkdownControl.value; + return this.isUseMarkdownValueEnabled(rawValue); + } + + private findNestedControlByKey(control: AbstractControl, key: string): AbstractControl { + if (!hasValue(control)) { + return null; + } + + if (control instanceof UntypedFormGroup) { + if (hasValue(control.controls[key])) { + return control.controls[key]; + } + + for (const childControl of Object.values(control.controls)) { + const matchingControl = this.findNestedControlByKey(childControl, key); + if (hasValue(matchingControl)) { + return matchingControl; + } + } + + return null; + } + + if (control instanceof UntypedFormArray) { + for (const childControl of control.controls) { + const matchingControl = this.findNestedControlByKey(childControl, key); + if (hasValue(matchingControl)) { + return matchingControl; + } + } + } + + return null; + } + + private isUseMarkdownValueEnabled(value: any): boolean { if (!hasValue(value)) { return false; } @@ -628,6 +666,19 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo return normalizedValue === 'yes' || normalizedValue === 'true'; } + if (Array.isArray(value)) { + return value.some((entry) => this.isUseMarkdownValueEnabled(entry)); + } + + if (typeof value === 'object') { + const yesOption = value.local_description_usemarkdown_yes; + if (hasValue(yesOption)) { + return this.isUseMarkdownValueEnabled(yesOption); + } + + return Object.values(value).some((entry) => this.isUseMarkdownValueEnabled(entry)); + } + return false; } From 9a1dcb77ba18ca33cd4520cbf042f0756f25a0b6 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Wed, 4 Mar 2026 13:54:26 +0100 Subject: [PATCH 10/14] cache toggle visibility and normalize usemarkdown truthy value handling fix(markdown-preview): cache toggle visibility and normalize usemarkdown truthy value handling --- ...it-metadata-field-values.component.spec.ts | 86 +++++++++++++ ...so-edit-metadata-field-values.component.ts | 33 +++-- .../dso-edit-metadata-value.component.spec.ts | 15 +++ ...amic-form-control-container.component.html | 2 +- ...c-form-control-container.component.spec.ts | 121 ++++++++++++++++++ ...ynamic-form-control-container.component.ts | 79 ++++++++++-- 6 files changed, 312 insertions(+), 24 deletions(-) diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.spec.ts index ea8264feb12..5d1b98028fb 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.spec.ts @@ -126,6 +126,92 @@ describe('DsoEditMetadataFieldValuesComponent', () => { expect(component.isLocalDescriptionUseMarkdownEnabled()).toBeTrue(); }); + + it('should return true when local.description.usemarkdown is YES', () => { + form = new DsoEditMetadataForm({ + ...dso.metadata, + 'local.description.usemarkdown': [ + Object.assign(new MetadataValue(), { + value: 'YES', + language: 'en', + place: 0, + }), + ], + }); + component.form = form; + + expect(component.isLocalDescriptionUseMarkdownEnabled()).toBeTrue(); + }); + + it('should return true when local.description.usemarkdown is True', () => { + form = new DsoEditMetadataForm({ + ...dso.metadata, + 'local.description.usemarkdown': [ + Object.assign(new MetadataValue(), { + value: 'True', + language: 'en', + place: 0, + }), + ], + }); + component.form = form; + + expect(component.isLocalDescriptionUseMarkdownEnabled()).toBeTrue(); + }); + + it('should return true when local.description.usemarkdown is boolean true', () => { + form = new DsoEditMetadataForm({ + ...dso.metadata, + 'local.description.usemarkdown': [ + Object.assign(new MetadataValue(), { + value: true, + language: 'en', + place: 0, + }), + ], + }); + component.form = form; + + expect(component.isLocalDescriptionUseMarkdownEnabled()).toBeTrue(); + }); + + it('should return true when local.description.usemarkdown is object map with truthy entry', () => { + form = new DsoEditMetadataForm({ + ...dso.metadata, + 'local.description.usemarkdown': [ + Object.assign(new MetadataValue(), { + value: { + local_description_usemarkdown_yes: false, + another_option: true, + }, + language: 'en', + place: 0, + }), + ], + }); + component.form = form; + + expect(component.isLocalDescriptionUseMarkdownEnabled()).toBeTrue(); + }); + + it('should return false when local.description.usemarkdown object map is fully falsy', () => { + form = new DsoEditMetadataForm({ + ...dso.metadata, + 'local.description.usemarkdown': [ + Object.assign(new MetadataValue(), { + value: { + local_description_usemarkdown_yes: false, + another_option: false, + }, + language: 'en', + place: 0, + }), + ], + }); + component.form = form; + + expect(component.isLocalDescriptionUseMarkdownEnabled()).toBeFalse(); + }); }); describe('dropping a value on a different index', () => { diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts index 054e0095ecb..7320e52dfd8 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts @@ -71,18 +71,33 @@ export class DsoEditMetadataFieldValuesComponent { return useMarkdownValues.some((metadataValue: DsoEditMetadataValue) => { const value = metadataValue?.newValue?.value; + return this.isUseMarkdownValueEnabled(value); + }); + } - if (typeof value === 'boolean') { - return value; - } + private isUseMarkdownValueEnabled(value: any): boolean { + if (value === null || value === undefined) { + return false; + } - if (typeof value === 'string') { - const normalizedValue = value.toLowerCase(); - return normalizedValue === 'yes' || normalizedValue === 'true'; - } + if (typeof value === 'boolean') { + return value; + } - return value === true; - }); + if (typeof value === 'string') { + const normalizedValue = value.toLowerCase(); + return normalizedValue === 'yes' || normalizedValue === 'true'; + } + + if (Array.isArray(value)) { + return value.some((entry) => this.isUseMarkdownValueEnabled(entry)); + } + + if (typeof value === 'object') { + return Object.values(value).some((entry) => this.isUseMarkdownValueEnabled(entry)); + } + + return false; } /** diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts index 3224e9a1ee8..0dcc364b085 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts @@ -97,6 +97,21 @@ describe('DsoEditMetadataValueComponent', () => { expect(component.isMarkdownPreviewModeEnabled()).toBeTrue(); expect(component.getMarkdownPreviewValue()).toBe('Regular Name'); }); + + it('should keep toggle visible for uppercase and boolean/object value shapes when markdown form gate is enabled', () => { + const valueShapes = ['YES', 'True', true, { local_description_usemarkdown_yes: true }]; + + valueShapes.forEach((valueShape) => { + component.mdValue.newValue.value = valueShape as any; + expect(component.canShowMarkdownPreviewToggle()).toBeTrue(); + }); + }); + + it('should hide toggle for non-description metadata fields', () => { + component.mdField = 'dc.title'; + + expect(component.canShowMarkdownPreviewToggle()).toBeFalse(); + }); }); it('should not show a badge', () => { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 925c6d01d0c..0cf018dc62e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -12,7 +12,7 @@
-
+
{{itemDescription}}
-
+
From d9fc08ca63fb5d81f83d5e3f3bead149554b3c3e Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Wed, 4 Mar 2026 14:36:27 +0100 Subject: [PATCH 12/14] Disable MathJax for markdown Disable MathJax for markdown --- config/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.yml b/config/config.yml index 35bac0acb45..fd5f8200d98 100644 --- a/config/config.yml +++ b/config/config.yml @@ -174,4 +174,4 @@ languages: markdown: enabled: true - mathjax: true + mathjax: false From 0164d08e6ebf4b672ce8ec72860886fa40b991df Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Wed, 4 Mar 2026 14:45:55 +0100 Subject: [PATCH 13/14] Clarify default allowlisted markdown fields Clarify default allowlisted markdown fields --- config/config.example.yml | 3 ++- src/config/default-app-config.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 8b56711c7d2..ff94087cf44 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -418,7 +418,8 @@ info: enablePrivacyStatement: true # Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/) -# display in supported metadata fields. By default, only dc.description.abstract is supported. +# display in supported metadata fields. By default, allowlisted description fields +# (description, dc.description, dc.description.abstract) are supported. markdown: enabled: false mathjax: false diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index f301cd73d41..3f6f2b49461 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -409,7 +409,8 @@ export class DefaultAppConfig implements AppConfig { }; // Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/) - // display in supported metadata fields. By default, only dc.description.abstract is supported. + // display in supported metadata fields. By default, allowlisted description fields + // (description, dc.description, dc.description.abstract) are supported. markdown: MarkdownConfig = { enabled: false, mathjax: false From 8132fbf5651e0ed686189ecad90cb6b8a0fb41db Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Wed, 4 Mar 2026 15:50:11 +0100 Subject: [PATCH 14/14] Extract markdown allow list and fix toggle Move markdown description metadata allow list into a shared constant and replace inline copies across components. Improve markdown preview toggle behavior and accessibility: add aria-label/aria-pressed attributes in the template, add shouldBindMarkdownToggleVisibility() and clearMarkdownToggleVisibilityBinding() to avoid binding/ subscriptions when markdown is disabled, field is not a textarea, or field is readOnly, and ensure proper cleanup. Update unit tests to handle global appConfig markdown state and add cases for hiding the toggle when global markdown is disabled or when not editing. --- .../dso-edit-metadata-value.component.spec.ts | 31 ++++++++++----- .../dso-edit-metadata-value.component.ts | 7 +--- ...arkdown-description-metadata-allow-list.ts | 5 +++ ...amic-form-control-container.component.html | 7 +++- ...ynamic-form-control-container.component.ts | 38 ++++++++++++++++--- .../builder/parsers/textarea-field-parser.ts | 7 +--- 6 files changed, 69 insertions(+), 26 deletions(-) create mode 100644 src/app/shared/form/builder/constants/markdown-description-metadata-allow-list.ts diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts index 0dcc364b085..8a7bd6c89c2 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts @@ -74,13 +74,21 @@ describe('DsoEditMetadataValueComponent', () => { }); describe('markdown preview toggle', () => { + let appConfig: any; + let originalMarkdownEnabled: boolean; + beforeEach(() => { editMetadataValue.editing = true; - const appConfig = TestBed.inject(APP_CONFIG) as any; + appConfig = TestBed.inject(APP_CONFIG) as any; + originalMarkdownEnabled = appConfig.markdown.enabled; appConfig.markdown.enabled = true; fixture.detectChanges(); }); + afterEach(() => { + appConfig.markdown.enabled = originalMarkdownEnabled; + }); + it('should show toggle for description fields when metadata markdown is enabled', () => { expect(component.canShowMarkdownPreviewToggle()).toBeTrue(); }); @@ -91,6 +99,18 @@ describe('DsoEditMetadataValueComponent', () => { expect(component.canShowMarkdownPreviewToggle()).toBeFalse(); }); + it('should hide toggle when global markdown is disabled', () => { + appConfig.markdown.enabled = false; + + expect(component.canShowMarkdownPreviewToggle()).toBeFalse(); + }); + + it('should hide toggle when value is not in editing mode', () => { + component.mdValue.editing = false; + + expect(component.canShowMarkdownPreviewToggle()).toBeFalse(); + }); + it('should enable preview mode and return current value', () => { component.setMarkdownPreviewMode(true); @@ -98,15 +118,6 @@ describe('DsoEditMetadataValueComponent', () => { expect(component.getMarkdownPreviewValue()).toBe('Regular Name'); }); - it('should keep toggle visible for uppercase and boolean/object value shapes when markdown form gate is enabled', () => { - const valueShapes = ['YES', 'True', true, { local_description_usemarkdown_yes: true }]; - - valueShapes.forEach((valueShape) => { - component.mdValue.newValue.value = valueShape as any; - expect(component.canShowMarkdownPreviewToggle()).toBeTrue(); - }); - }); - it('should hide toggle for non-description metadata fields', () => { component.mdField = 'dc.title'; diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts index e79581f9664..a7b38955545 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts @@ -13,6 +13,7 @@ import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { EMPTY } from 'rxjs/internal/observable/empty'; import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface'; +import { MARKDOWN_DESCRIPTION_METADATA_ALLOW_LIST } from '../../../shared/form/builder/constants/markdown-description-metadata-allow-list'; @Component({ selector: 'ds-dso-edit-metadata-value', @@ -23,11 +24,7 @@ import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface'; * Component displaying a single editable row for a metadata value */ export class DsoEditMetadataValueComponent implements OnInit { - protected readonly markdownDescriptionMetadataAllowList: string[] = [ - 'description', - 'dc.description', - 'dc.description.abstract' - ]; + protected readonly markdownDescriptionMetadataAllowList: string[] = MARKDOWN_DESCRIPTION_METADATA_ALLOW_LIST; /** * The parent {@link DSpaceObject} to display a metadata form for diff --git a/src/app/shared/form/builder/constants/markdown-description-metadata-allow-list.ts b/src/app/shared/form/builder/constants/markdown-description-metadata-allow-list.ts new file mode 100644 index 00000000000..a6440d27558 --- /dev/null +++ b/src/app/shared/form/builder/constants/markdown-description-metadata-allow-list.ts @@ -0,0 +1,5 @@ +export const MARKDOWN_DESCRIPTION_METADATA_ALLOW_LIST: string[] = [ + 'description', + 'dc.description', + 'dc.description.abstract' +]; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 0cf018dc62e..3f4e7997a21 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -12,16 +12,21 @@
-
+
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 fe5456df573..068eb3f2ad7 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 @@ -126,6 +126,7 @@ import { DsDynamicSponsorAutocompleteComponent } from './models/sponsor-autocomp import { SPONSOR_METADATA_NAME } from './models/ds-dynamic-complex.model'; import { DsDynamicSponsorScrollableDropdownComponent } from './models/sponsor-scrollable-dropdown/dynamic-sponsor-scrollable-dropdown.component'; import { DsDynamicTextAreaModel } from './models/ds-dynamic-textarea.model'; +import { MARKDOWN_DESCRIPTION_METADATA_ALLOW_LIST } from '../constants/markdown-description-metadata-allow-list'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { @@ -211,11 +212,7 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type< changeDetection: ChangeDetectionStrategy.Default }) export class DsDynamicFormControlContainerComponent extends DynamicFormControlContainerComponent implements OnInit, OnChanges, OnDestroy { - protected readonly markdownDescriptionMetadataAllowList: string[] = [ - 'description', - 'dc.description', - 'dc.description.abstract' - ]; + protected readonly markdownDescriptionMetadataAllowList: string[] = MARKDOWN_DESCRIPTION_METADATA_ALLOW_LIST; @ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList; // eslint-disable-next-line @angular-eslint/no-input-rename @@ -639,6 +636,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } private setupMarkdownToggleVisibilityBinding(): void { + if (!this.shouldBindMarkdownToggleVisibility()) { + this.clearMarkdownToggleVisibilityBinding(); + return; + } + const useMarkdownControl = this.resolveLocalDescriptionUseMarkdownControl(); const hasSameUseMarkdownControl = useMarkdownControl === this.localDescriptionUseMarkdownControl; @@ -663,6 +665,32 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo .subscribe(() => this.refreshMarkdownToggleVisibility()); } + private shouldBindMarkdownToggleVisibility(): boolean { + if (!this.appConfig?.markdown?.enabled) { + return false; + } + + if (this.model?.type !== DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA) { + return false; + } + + if (this.model?.readOnly) { + return false; + } + + return this.isDescriptionTextareaField(); + } + + private clearMarkdownToggleVisibilityBinding(): void { + this.markdownToggleVisible = false; + this.localDescriptionUseMarkdownControl = null; + + if (hasValue(this.localDescriptionUseMarkdownSubscription)) { + this.localDescriptionUseMarkdownSubscription.unsubscribe(); + this.localDescriptionUseMarkdownSubscription = null; + } + } + private resolveLocalDescriptionUseMarkdownControl(): AbstractControl { return this.group?.root?.get('local_description_usemarkdown') || this.formGroup?.root?.get('local_description_usemarkdown') diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.ts index 62c73cd1fc7..91eb6682ceb 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.ts @@ -6,13 +6,10 @@ import { DsDynamicTextAreaModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; import { environment } from '../../../../../environments/environment'; +import { MARKDOWN_DESCRIPTION_METADATA_ALLOW_LIST } from '../constants/markdown-description-metadata-allow-list'; export class TextareaFieldParser extends FieldParser { - protected readonly markdownDescriptionMetadataAllowList: string[] = [ - 'description', - 'dc.description', - 'dc.description.abstract' - ]; + protected readonly markdownDescriptionMetadataAllowList: string[] = MARKDOWN_DESCRIPTION_METADATA_ALLOW_LIST; public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { const textAreaModelConfig: DsDynamicTextAreaModelConfig = this.initModel(null, label);