Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
Loading