Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,7 @@ languages:
- code: uk
label: Yкраї́нська
active: false

markdown:
enabled: true
mathjax: false
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<ds-dso-edit-metadata-value *ngFor="let mdValue of form.fields[mdField]; let idx = index" role="presentation"
[dso]="dso"
[mdValue]="mdValue"
[mdField]="mdField"
[markdownEnabledForForm]="isLocalDescriptionUseMarkdownEnabled()"
[dsoType]="dsoType"
[saving$]="saving$"
[isOnlyValue]="form.fields[mdField].length === 1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,114 @@ describe('DsoEditMetadataFieldValuesComponent', () => {
});
});

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();
});

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', () => {
beforeEach(() => {
component.drop(Object.assign({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,6 +59,47 @@ 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;
return this.isUseMarkdownValueEnabled(value);
});
}

private isUseMarkdownValueEnabled(value: any): boolean {
if (value === null || value === undefined) {
return false;
}

if (typeof value === 'boolean') {
return value;
}

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;
}

/**
* Drop a value into a new position
* Update the form's value array for the current field to match the dropped position
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
<div class="d-flex flex-row ds-value-row" *ngVar="mdValue.newValue.isVirtual as isVirtual" role="row"
cdkDrag (cdkDragStarted)="dragging.emit(true)" (cdkDragEnded)="dragging.emit(false)"
[ngClass]="{ 'ds-warning': mdValue.reordered || mdValue.change === DsoEditMetadataChangeTypeEnum.UPDATE, 'ds-danger': mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE, 'ds-success': mdValue.change === DsoEditMetadataChangeTypeEnum.ADD, 'h-100': isOnlyValue }">
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex align-items-center" *ngVar="(mdRepresentation$ | async) as mdRepresentation" role="cell">
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex" *ngVar="(mdRepresentation$ | async) as mdRepresentation" role="cell"
[ngClass]="mdValue.editing && !mdRepresentation ? 'flex-column align-items-stretch' : 'align-items-center'">
<div class="dont-break-out preserve-line-breaks" *ngIf="!mdValue.editing && !mdRepresentation">{{ mdValue.newValue.value }}</div>
<textarea class="form-control" rows="5" *ngIf="mdValue.editing && !mdRepresentation" [(ngModel)]="mdValue.newValue.value"
<div *ngIf="canShowMarkdownPreviewToggle()" class="btn-group btn-group-sm ds-markdown-mode-toggle" role="group"
[attr.aria-label]="'submission.form.markdown.toggle.preview' | translate">
<button type="button" class="btn" [ngClass]="isMarkdownPreviewModeEnabled() ? 'btn-outline-secondary' : 'btn-primary'"
(click)="setMarkdownPreviewMode(false)">
{{ 'submission.form.markdown.toggle.edit' | translate }}
</button>
<button type="button" class="btn" [ngClass]="isMarkdownPreviewModeEnabled() ? 'btn-primary' : 'btn-outline-secondary'"
(click)="setMarkdownPreviewMode(true)">
{{ 'submission.form.markdown.toggle.preview' | translate }}
</button>
</div>
<textarea class="form-control" rows="5" *ngIf="mdValue.editing && !mdRepresentation && !isMarkdownPreviewModeEnabled()" [(ngModel)]="mdValue.newValue.value"
[attr.aria-label]="(dsoType + '.edit.metadata.edit.value') | translate"
[dsDebounce]="300" (onDebounce)="confirm.emit(false)"></textarea>
<div *ngIf="mdValue.editing && !mdRepresentation && isMarkdownPreviewModeEnabled()" class="form-control ds-markdown-preview-container">
<div [innerHTML]="getMarkdownPreviewValue() | dsMarkdown | async"></div>
</div>
<div class="d-flex" *ngIf="mdRepresentation">
<a class="mr-2" target="_blank" [routerLink]="mdRepresentationItemRoute$ | async">{{ mdRepresentationName$ | async }}</a>
<ds-themed-type-badge [object]="mdRepresentation"></ds-themed-type-badge>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -64,10 +67,64 @@ 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', () => {
let appConfig: any;
let originalMarkdownEnabled: boolean;

beforeEach(() => {
editMetadataValue.editing = true;
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();
});

it('should hide toggle when markdown metadata gate is disabled', () => {
component.markdownEnabledForForm = false;

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);

expect(component.isMarkdownPreviewModeEnabled()).toBeTrue();
expect(component.getMarkdownPreviewValue()).toBe('Regular Name');
});

it('should hide toggle for non-description metadata fields', () => {
component.mdField = 'dc.title';

expect(component.canShowMarkdownPreviewToggle()).toBeFalse();
});
});

it('should not show a badge', () => {
expect(fixture.debugElement.query(By.css('ds-themed-type-badge'))).toBeNull();
});
Expand Down
Loading