From b154b783449d4460f903b0fb4eaf2769c67d1db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Fri, 20 Sep 2024 15:07:27 +0200 Subject: [PATCH 01/18] Add ItemPageCcLicenseFieldComponent to render rights information --- .../item-page-license-field.component.html | 29 ++ .../item-page-license-field.component.spec.ts | 280 ++++++++++++++++++ .../item-page-license-field.component.ts | 85 ++++++ .../license/item-page-license-field.scss | 4 + src/assets/i18n/en.json5 | 2 + 5 files changed, 400 insertions(+) create mode 100644 src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html create mode 100644 src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts create mode 100644 src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts create mode 100644 src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html new file mode 100644 index 00000000000..2f07cda100b --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html @@ -0,0 +1,29 @@ +
+ + + + + + + + + + + + {{ license }} + {{ separator }} + + + + + {{ license }} + {{ separator }} + + + {{ uri }} + {{ separator }} + + + + +
diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts new file mode 100644 index 00000000000..2dbb6f09822 --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts @@ -0,0 +1,280 @@ +import { + ChangeDetectionStrategy, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; +import { Item } from 'src/app/core/shared/item.model'; +import { + MetadataMap, + MetadataValue, +} from 'src/app/core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils'; +import { createPaginatedList } from 'src/app/shared/testing/utils.test'; + +import { APP_CONFIG } from '../../../../../../config/app-config.interface'; +import { environment } from '../../../../../../environments/environment'; +import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; +import { ItemPageLicenseFieldComponent } from './item-page-license-field.component'; + + +interface TestInstance { + metadata: { + }; +} + + +interface TestCase { + testInstance: TestInstance; + expected: { + render: boolean, + textElements: string[], + linkElements: string[], + }; +} + + +const licenseNameMock = 'LICENSE NAME'; +const exampleUriMock = 'http://example.com'; +const ccUriMock = 'https://creativecommons.org/licenses/by/4.0'; + + +const testCases: TestCase[] = [ + { + testInstance: { + metadata: { 'dc.rights': undefined, 'dc.rights.uri': undefined, }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': [ undefined, undefined ], 'dc.rights.uri': undefined, }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights.license': undefined, 'dc.rights.uri': undefined, }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': undefined, 'dc.rights.license': undefined, 'dc.rights.uri': [ undefined, undefined ], }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': null, }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': [ null, null ], }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': exampleUriMock, }, + }, + expected: { + render: true, + textElements: [exampleUriMock], + linkElements: [exampleUriMock], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': exampleUriMock, }, + }, + expected: { + render: true, + textElements: [exampleUriMock], + linkElements: [exampleUriMock], + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': ccUriMock, }, + }, + expected: { + render: true, + textElements: [ccUriMock], + linkElements: [ccUriMock], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': null, 'dc.rights.license': licenseNameMock, 'dc.rights.uri': null }, + }, + expected: { + render: true, + textElements: [licenseNameMock], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': licenseNameMock, 'dc.rights.uri': ccUriMock, }, + }, + expected: { + render: true, + // This test case is delegated to ItemPageCcLicenseFieldComponent + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': licenseNameMock, 'dc.rights.license': licenseNameMock, 'dc.rights.uri': ccUriMock }, + }, + expected: { + render: true, + // This test case meets the CC criteria too (since it has 'dc.rights', and 'dc.rights.uri' + // points to a CC license). Thus, it is delegated to ItemPageCcLicenseFieldComponent. + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': licenseNameMock, 'dc.rights.license': licenseNameMock, 'dc.rights.uri': exampleUriMock }, + }, + expected: { + render: true, + textElements: [licenseNameMock, licenseNameMock, exampleUriMock], + linkElements: [exampleUriMock], + }, + }, +]; + + +// Updates the component fixture with parameters from the test instance +function configureFixture( + fixture: ComponentFixture, + testInstance: TestInstance, +) { + const item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: new MetadataMap(), + }); + + for (const [key, values] of Object.entries(testInstance.metadata)) { + for (const value of values instanceof Array ? values : [values]) { + item.metadata[key] = [ + { + language: 'en_US', + value: value, + }, + ] as MetadataValue[]; + } + } + + let component: ItemPageLicenseFieldComponent = fixture.componentInstance; + component.item = item; + + fixture.detectChanges(); +} + + +describe('ItemPageLicenseFieldComponent', () => { + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + void TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock, + }, + }), + ItemPageLicenseFieldComponent, + ], + providers: [{ provide: APP_CONFIG, useValue: environment }], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(ItemPageLicenseFieldComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents(); + })); + + beforeEach(waitForAsync(() => { + fixture = TestBed.createComponent(ItemPageLicenseFieldComponent); + })); + + testCases.forEach((testCase) => { + describe('', () => { + beforeEach(async () => { + configureFixture(fixture, testCase.testInstance); + }); + + it('should render or not the component', + () => { + const componentEl = fixture.debugElement.query(By.css('.item-page-field')); + expect(Boolean(componentEl)).toBe(testCase.expected.render); + }, + ); + + it('should show/hide license as plain text', + () => { + const textEl = fixture.debugElement.queryAll(By.css('.license-text')); + expect(textEl.length).toBe(testCase.expected.textElements.length); + if (textEl && testCase.expected.textElements.length > 0) { + textEl.forEach((elt, idx) => + expect(elt.nativeElement.innerHTML).toContain(testCase.expected.textElements[idx]) + ); + } + }, + ); + + it('should show/hide the license as link', + () => { + const linkEl = fixture.debugElement.queryAll(By.css('.license-link')); + expect(linkEl.length).toBe(testCase.expected.linkElements.length); + if (linkEl && testCase.expected.linkElements.length > 0) { + linkEl.forEach((elt, idx) => + expect(elt.query(By.css('.license-text')).nativeElement.innerHTML).toContain(testCase.expected.linkElements[idx]) + ); + } + }, + ); + }); + }); +}); diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts new file mode 100644 index 00000000000..4a559ffaa85 --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -0,0 +1,85 @@ +import { + NgClass, + NgIf, + NgFor, + NgStyle, +} from '@angular/common'; +import { + Component, + Input, + OnInit, + ViewContainerRef, +} from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { Item } from 'src/app/core/shared/item.model'; +import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; +import { ItemPageCcLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component'; +import { Metadata } from 'src/app/core/shared/metadata.utils'; + +@Component({ + selector: 'ds-item-page-license-field', + templateUrl: './item-page-license-field.component.html', + styleUrl: './item-page-license-field.scss', + standalone: true, + imports: [NgIf, NgFor, NgClass, NgStyle, TranslateModule, MetadataFieldWrapperComponent, ItemPageCcLicenseFieldComponent], +}) +/** + * Displays the item's licenses + * + * If the number of 'dc.rights*' values (excepting 'dc.rights.uri') and the number of 'dc.rights.uri' + * match, they will be printed as a list of links, where the text of the link will be the dc.right* + * value and the link the corresponding 'dc.rights.uri'. The match will be done in the order they + * appear. In any other case, all the 'dc.rights*' fields will be shown as a list (where the URIs + * will be rendered as links). + */ +export class ItemPageLicenseFieldComponent implements OnInit { + /** + * The item to display the license for + */ + @Input() item: Item; + + /** + * String to use as a separator if multiple license entries are specified + */ + @Input() separator: String = '•'; + + uris: string[]; + licenses: string[]; + isCcLicense: boolean; + + constructor(private viewRef: ViewContainerRef) {} + + ngOnInit() { + // This is a workaround to determine which fields are relevant to render a CC license + // until https://github.com/DSpace/dspace-angular/pull/3165 is merged in the main branch + // (which will provide a configuration property). + // Until then, we must dynamically create a ItemPageCcLicenseFieldComponent to retrieve + // the fields that are relevant to render a CC license field (since developers may have + // customized them directly in the code). + // This approach avoids hardcoding the values (thus breaking developers' customizations) + // and makes this code compatible with a future merge of the above PR + const ccComponentRef = this.viewRef.createComponent(ItemPageCcLicenseFieldComponent); + ccComponentRef.setInput('item', this.item); + // The regex below has been copied from ItemPageCcLicenseFieldComponent + // We duplicate the regex here to avoid changing the implementation of + // ItemPageCcLicenseFieldComponent to avoid breaking changes. + // It may be desirable to further refactor this code in the future + const regex = /.*creativecommons.org\/(licenses|publicdomain)\/([^/]+)/gm; + + // If the license information is a CC License, we will delegate the rendering + // of this field to ItemPageCcLicenseFieldComponent + this.isCcLicense = this.item.allMetadataValues(ccComponentRef.instance.ccLicenseUriField).length == 1 + && regex.exec(this.item.firstMetadataValue(ccComponentRef.instance.ccLicenseUriField)) != null + && this.item.allMetadataValues(ccComponentRef.instance.ccLicenseNameField).length == 1; + // We no longer need the ItemPageCcLicenseFieldComponent (and we don't want it to be rendered here) + ccComponentRef.destroy(); + + // In either case... + // get all non-empty dc.rights* values, excepting the URIs... + this.licenses = Metadata.all(this.item.metadata, Object.keys(this.item.metadata).filter(key => + key != 'dc.rights.uri' && (key.startsWith('dc.rights') || key.startsWith('dc.rights.'))) + ).map(mdValue => mdValue.value).filter(value => value); + // and get the URIs + this.uris = this.item.allMetadataValues('dc.rights.uri').filter(value => value); + } +} diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss new file mode 100644 index 00000000000..c81c3055053 --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss @@ -0,0 +1,4 @@ +.separator { + padding-left: 0.5rem; + padding-right: 0.5rem; +} \ No newline at end of file diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 05f528af072..5055b76591d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -6738,6 +6738,8 @@ "search.filters.filter.notifyEndorsement.label": "Search Notify Endorsement", + "item.page.license.title": "Rights and licensing", + "item.page.cc.license.title": "Creative Commons license", "item.page.cc.license.disclaimer": "Except where otherwised noted, this item's license is described as", From 049d3a4f7c60b54bf01cd8aaa3f3676eaece538e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Fri, 20 Sep 2024 15:09:50 +0200 Subject: [PATCH 02/18] Update UntypedItemComponent to use ItemPageLicenseFieldComponent rather than ItemPageCcLicenseFieldComponent (fixes #3170) --- .../item-types/untyped-item/untyped-item.component.html | 4 ++-- .../simple/item-types/untyped-item/untyped-item.component.ts | 4 ++-- .../simple/item-types/untyped-item/untyped-item.component.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html index 962cc2fcad8..feaa420bd73 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -86,8 +86,8 @@ [fields]="['datacite.relation.isReferencedBy']" [label]="'item.page.referenced'"> - - + +
{{"item.page.link.full" | translate}} diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts index a9ef9b2eadf..e56ac166e4d 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts @@ -21,7 +21,7 @@ import { ThemedMediaViewerComponent } from '../../../media-viewer/themed-media-v import { MiradorViewerComponent } from '../../../mirador-viewer/mirador-viewer.component'; import { ThemedFileSectionComponent } from '../../field-components/file-section/themed-file-section.component'; import { ItemPageAbstractFieldComponent } from '../../field-components/specific-field/abstract/item-page-abstract-field.component'; -import { ItemPageCcLicenseFieldComponent } from '../../field-components/specific-field/cc-license/item-page-cc-license-field.component'; +import { ItemPageLicenseFieldComponent } from '../../field-components/specific-field/license/item-page-license-field.component'; import { ItemPageDateFieldComponent } from '../../field-components/specific-field/date/item-page-date-field.component'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../field-components/specific-field/title/themed-item-page-field.component'; @@ -59,7 +59,7 @@ import { ItemComponent } from '../shared/item.component'; RouterLink, AsyncPipe, TranslateModule, - ItemPageCcLicenseFieldComponent, + ItemPageLicenseFieldComponent, ], }) export class UntypedItemComponent extends ItemComponent {} diff --git a/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts b/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts index 6fbb035d2f2..ff3e1c34aed 100644 --- a/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts +++ b/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts @@ -17,7 +17,7 @@ import { ThemedMediaViewerComponent } from '../../../../../../../app/item-page/m import { MiradorViewerComponent } from '../../../../../../../app/item-page/mirador-viewer/mirador-viewer.component'; import { ThemedFileSectionComponent } from '../../../../../../../app/item-page/simple/field-components/file-section/themed-file-section.component'; import { ItemPageAbstractFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component'; -import { ItemPageCcLicenseFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component'; +import { ItemPageLicenseFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/license/item-page-license-field.component'; import { ItemPageDateFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/date/item-page-date-field.component'; import { GenericItemPageFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; @@ -64,7 +64,7 @@ import { ThemedThumbnailComponent } from '../../../../../../../app/thumbnail/the RouterLink, AsyncPipe, TranslateModule, - ItemPageCcLicenseFieldComponent, + ItemPageLicenseFieldComponent, ], }) export class UntypedItemComponent extends BaseComponent {} From 8250d7672ea8324a09d5606ddd4681d4b900a2d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Fri, 20 Sep 2024 15:11:25 +0200 Subject: [PATCH 03/18] Add rights information to Publication EntityType template (PublicationComponent) (fixes #3167) --- .../publication/publication.component.html | 2 ++ .../publication/publication.component.ts | 24 ++++++++++++++++++- .../publication/publication.component.ts | 23 +++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index 86f203deca2..715b66fb17e 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -100,6 +100,8 @@ [fields]="['datacite.relation.isReferencedBy']" [label]="'item.page.referenced'"> + +
{{"item.page.link.full" | translate}} diff --git a/src/app/item-page/simple/item-types/publication/publication.component.ts b/src/app/item-page/simple/item-types/publication/publication.component.ts index 2acb02377b1..9cdc28f7391 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.ts @@ -21,6 +21,7 @@ import { MiradorViewerComponent } from '../../../mirador-viewer/mirador-viewer.c import { ThemedFileSectionComponent } from '../../field-components/file-section/themed-file-section.component'; import { ItemPageAbstractFieldComponent } from '../../field-components/specific-field/abstract/item-page-abstract-field.component'; import { ItemPageDateFieldComponent } from '../../field-components/specific-field/date/item-page-date-field.component'; +import { ItemPageLicenseFieldComponent } from '../../field-components/specific-field/license/item-page-license-field.component'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../field-components/specific-field/title/themed-item-page-field.component'; import { ItemPageUriFieldComponent } from '../../field-components/specific-field/uri/item-page-uri-field.component'; @@ -39,7 +40,28 @@ import { ItemComponent } from '../shared/item.component'; templateUrl: './publication.component.html', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [NgIf, ThemedResultsBackButtonComponent, MiradorViewerComponent, ThemedItemPageTitleFieldComponent, DsoEditMenuComponent, MetadataFieldWrapperComponent, ThemedThumbnailComponent, ThemedMediaViewerComponent, ThemedFileSectionComponent, ItemPageDateFieldComponent, ThemedMetadataRepresentationListComponent, GenericItemPageFieldComponent, RelatedItemsComponent, ItemPageAbstractFieldComponent, ItemPageUriFieldComponent, CollectionsComponent, RouterLink, AsyncPipe, TranslateModule], + imports: [ + NgIf, + ThemedResultsBackButtonComponent, + MiradorViewerComponent, + ThemedItemPageTitleFieldComponent, + DsoEditMenuComponent, + MetadataFieldWrapperComponent, + ThemedThumbnailComponent, + ThemedMediaViewerComponent, + ThemedFileSectionComponent, + ItemPageDateFieldComponent, + ThemedMetadataRepresentationListComponent, + GenericItemPageFieldComponent, + RelatedItemsComponent, + ItemPageAbstractFieldComponent, + ItemPageUriFieldComponent, + CollectionsComponent, + RouterLink, + AsyncPipe, + TranslateModule, + ItemPageLicenseFieldComponent, + ], }) export class PublicationComponent extends ItemComponent { diff --git a/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts b/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts index ce56724e4ab..1463024df04 100644 --- a/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts +++ b/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts @@ -20,6 +20,7 @@ import { ItemPageDateFieldComponent } from '../../../../../../../app/item-page/s import { GenericItemPageFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; import { ItemPageUriFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component'; +import { ItemPageLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component'; import { PublicationComponent as BaseComponent } from '../../../../../../../app/item-page/simple/item-types/publication/publication.component'; import { ThemedMetadataRepresentationListComponent } from '../../../../../../../app/item-page/simple/metadata-representation-list/themed-metadata-representation-list.component'; import { RelatedItemsComponent } from '../../../../../../../app/item-page/simple/related-items/related-items-component'; @@ -42,7 +43,27 @@ import { ThemedThumbnailComponent } from '../../../../../../../app/thumbnail/the templateUrl: '../../../../../../../app/item-page/simple/item-types/publication/publication.component.html', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [NgIf, ThemedResultsBackButtonComponent, MiradorViewerComponent, ThemedItemPageTitleFieldComponent, DsoEditMenuComponent, MetadataFieldWrapperComponent, ThemedThumbnailComponent, ThemedMediaViewerComponent, ThemedFileSectionComponent, ItemPageDateFieldComponent, ThemedMetadataRepresentationListComponent, GenericItemPageFieldComponent, RelatedItemsComponent, ItemPageAbstractFieldComponent, ItemPageUriFieldComponent, CollectionsComponent, RouterLink, AsyncPipe, TranslateModule], + imports: [NgIf, + ThemedResultsBackButtonComponent, + MiradorViewerComponent, + ThemedItemPageTitleFieldComponent, + DsoEditMenuComponent, + MetadataFieldWrapperComponent, + ThemedThumbnailComponent, + ThemedMediaViewerComponent, + ThemedFileSectionComponent, + ItemPageDateFieldComponent, + ThemedMetadataRepresentationListComponent, + GenericItemPageFieldComponent, + RelatedItemsComponent, + ItemPageAbstractFieldComponent, + ItemPageUriFieldComponent, + CollectionsComponent, + RouterLink, + AsyncPipe, + TranslateModule, + ItemPageLicenseFieldComponent, + ], }) export class PublicationComponent extends BaseComponent { From 13ffbef8f5b4e0d98489dc7365988376e7fad2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Fri, 20 Sep 2024 16:58:41 +0200 Subject: [PATCH 04/18] Fix lint errors * Trailing spaces * Missing trailing commas * Imports sorting * Equalities and typing --- .../item-page-license-field.component.html | 2 +- .../item-page-license-field.component.spec.ts | 31 +++++++++---------- .../item-page-license-field.component.ts | 30 +++++++++--------- .../publication/publication.component.ts | 2 +- .../untyped-item/untyped-item.component.ts | 2 +- .../publication/publication.component.ts | 2 +- .../untyped-item/untyped-item.component.ts | 2 +- 7 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html index 2f07cda100b..8cee4373c12 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html @@ -6,7 +6,7 @@ - + diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts index 2dbb6f09822..759a0d518dc 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts @@ -27,8 +27,7 @@ import { ItemPageLicenseFieldComponent } from './item-page-license-field.compone interface TestInstance { - metadata: { - }; + metadata: any; } @@ -50,7 +49,7 @@ const ccUriMock = 'https://creativecommons.org/licenses/by/4.0'; const testCases: TestCase[] = [ { testInstance: { - metadata: { 'dc.rights': undefined, 'dc.rights.uri': undefined, }, + metadata: { 'dc.rights': undefined, 'dc.rights.uri': undefined }, }, expected: { render: false, @@ -60,7 +59,7 @@ const testCases: TestCase[] = [ }, { testInstance: { - metadata: { 'dc.rights': [ undefined, undefined ], 'dc.rights.uri': undefined, }, + metadata: { 'dc.rights': [ undefined, undefined ], 'dc.rights.uri': undefined }, }, expected: { render: false, @@ -70,7 +69,7 @@ const testCases: TestCase[] = [ }, { testInstance: { - metadata: { 'dc.rights.license': undefined, 'dc.rights.uri': undefined, }, + metadata: { 'dc.rights.license': undefined, 'dc.rights.uri': undefined }, }, expected: { render: false, @@ -80,7 +79,7 @@ const testCases: TestCase[] = [ }, { testInstance: { - metadata: { 'dc.rights': undefined, 'dc.rights.license': undefined, 'dc.rights.uri': [ undefined, undefined ], }, + metadata: { 'dc.rights': undefined, 'dc.rights.license': undefined, 'dc.rights.uri': [ undefined, undefined ] }, }, expected: { render: false, @@ -90,7 +89,7 @@ const testCases: TestCase[] = [ }, { testInstance: { - metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': null, }, + metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': null }, }, expected: { render: false, @@ -100,7 +99,7 @@ const testCases: TestCase[] = [ }, { testInstance: { - metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': [ null, null ], }, + metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': [ null, null ] }, }, expected: { render: false, @@ -110,7 +109,7 @@ const testCases: TestCase[] = [ }, { testInstance: { - metadata: { 'dc.rights.uri': exampleUriMock, }, + metadata: { 'dc.rights.uri': exampleUriMock }, }, expected: { render: true, @@ -120,7 +119,7 @@ const testCases: TestCase[] = [ }, { testInstance: { - metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': exampleUriMock, }, + metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': exampleUriMock }, }, expected: { render: true, @@ -130,7 +129,7 @@ const testCases: TestCase[] = [ }, { testInstance: { - metadata: { 'dc.rights.uri': ccUriMock, }, + metadata: { 'dc.rights.uri': ccUriMock }, }, expected: { render: true, @@ -150,7 +149,7 @@ const testCases: TestCase[] = [ }, { testInstance: { - metadata: { 'dc.rights': licenseNameMock, 'dc.rights.uri': ccUriMock, }, + metadata: { 'dc.rights': licenseNameMock, 'dc.rights.uri': ccUriMock }, }, expected: { render: true, @@ -257,8 +256,8 @@ describe('ItemPageLicenseFieldComponent', () => { const textEl = fixture.debugElement.queryAll(By.css('.license-text')); expect(textEl.length).toBe(testCase.expected.textElements.length); if (textEl && testCase.expected.textElements.length > 0) { - textEl.forEach((elt, idx) => - expect(elt.nativeElement.innerHTML).toContain(testCase.expected.textElements[idx]) + textEl.forEach((elt, idx) => + expect(elt.nativeElement.innerHTML).toContain(testCase.expected.textElements[idx]), ); } }, @@ -269,8 +268,8 @@ describe('ItemPageLicenseFieldComponent', () => { const linkEl = fixture.debugElement.queryAll(By.css('.license-link')); expect(linkEl.length).toBe(testCase.expected.linkElements.length); if (linkEl && testCase.expected.linkElements.length > 0) { - linkEl.forEach((elt, idx) => - expect(elt.query(By.css('.license-text')).nativeElement.innerHTML).toContain(testCase.expected.linkElements[idx]) + linkEl.forEach((elt, idx) => + expect(elt.query(By.css('.license-text')).nativeElement.innerHTML).toContain(testCase.expected.linkElements[idx]), ); } }, diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts index 4a559ffaa85..b6ff8ad7490 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -1,7 +1,7 @@ import { NgClass, - NgIf, NgFor, + NgIf, NgStyle, } from '@angular/common'; import { @@ -12,9 +12,9 @@ import { } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { Item } from 'src/app/core/shared/item.model'; -import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; -import { ItemPageCcLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component'; import { Metadata } from 'src/app/core/shared/metadata.utils'; +import { ItemPageCcLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component'; +import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; @Component({ selector: 'ds-item-page-license-field', @@ -25,11 +25,11 @@ import { Metadata } from 'src/app/core/shared/metadata.utils'; }) /** * Displays the item's licenses - * + * * If the number of 'dc.rights*' values (excepting 'dc.rights.uri') and the number of 'dc.rights.uri' - * match, they will be printed as a list of links, where the text of the link will be the dc.right* - * value and the link the corresponding 'dc.rights.uri'. The match will be done in the order they - * appear. In any other case, all the 'dc.rights*' fields will be shown as a list (where the URIs + * match, they will be printed as a list of links, where the text of the link will be the dc.right* + * value and the link the corresponding 'dc.rights.uri'. The match will be done in the order they + * appear. In any other case, all the 'dc.rights*' fields will be shown as a list (where the URIs * will be rendered as links). */ export class ItemPageLicenseFieldComponent implements OnInit { @@ -41,7 +41,7 @@ export class ItemPageLicenseFieldComponent implements OnInit { /** * String to use as a separator if multiple license entries are specified */ - @Input() separator: String = '•'; + @Input() separator = '•'; uris: string[]; licenses: string[]; @@ -65,20 +65,20 @@ export class ItemPageLicenseFieldComponent implements OnInit { // ItemPageCcLicenseFieldComponent to avoid breaking changes. // It may be desirable to further refactor this code in the future const regex = /.*creativecommons.org\/(licenses|publicdomain)\/([^/]+)/gm; - + // If the license information is a CC License, we will delegate the rendering // of this field to ItemPageCcLicenseFieldComponent - this.isCcLicense = this.item.allMetadataValues(ccComponentRef.instance.ccLicenseUriField).length == 1 - && regex.exec(this.item.firstMetadataValue(ccComponentRef.instance.ccLicenseUriField)) != null - && this.item.allMetadataValues(ccComponentRef.instance.ccLicenseNameField).length == 1; + this.isCcLicense = this.item.allMetadataValues(ccComponentRef.instance.ccLicenseUriField).length === 1 + && regex.exec(this.item.firstMetadataValue(ccComponentRef.instance.ccLicenseUriField)) != null + && this.item.allMetadataValues(ccComponentRef.instance.ccLicenseNameField).length === 1; // We no longer need the ItemPageCcLicenseFieldComponent (and we don't want it to be rendered here) ccComponentRef.destroy(); // In either case... // get all non-empty dc.rights* values, excepting the URIs... - this.licenses = Metadata.all(this.item.metadata, Object.keys(this.item.metadata).filter(key => - key != 'dc.rights.uri' && (key.startsWith('dc.rights') || key.startsWith('dc.rights.'))) - ).map(mdValue => mdValue.value).filter(value => value); + this.licenses = Metadata + .all(this.item.metadata, Object.keys(this.item.metadata).filter(key => key !== 'dc.rights.uri' && (key.startsWith('dc.rights') || key.startsWith('dc.rights.')))) + .map(mdValue => mdValue.value).filter(value => value); // and get the URIs this.uris = this.item.allMetadataValues('dc.rights.uri').filter(value => value); } diff --git a/src/app/item-page/simple/item-types/publication/publication.component.ts b/src/app/item-page/simple/item-types/publication/publication.component.ts index 9cdc28f7391..af4908c2a48 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.ts @@ -21,8 +21,8 @@ import { MiradorViewerComponent } from '../../../mirador-viewer/mirador-viewer.c import { ThemedFileSectionComponent } from '../../field-components/file-section/themed-file-section.component'; import { ItemPageAbstractFieldComponent } from '../../field-components/specific-field/abstract/item-page-abstract-field.component'; import { ItemPageDateFieldComponent } from '../../field-components/specific-field/date/item-page-date-field.component'; -import { ItemPageLicenseFieldComponent } from '../../field-components/specific-field/license/item-page-license-field.component'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; +import { ItemPageLicenseFieldComponent } from '../../field-components/specific-field/license/item-page-license-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../field-components/specific-field/title/themed-item-page-field.component'; import { ItemPageUriFieldComponent } from '../../field-components/specific-field/uri/item-page-uri-field.component'; import { ThemedMetadataRepresentationListComponent } from '../../metadata-representation-list/themed-metadata-representation-list.component'; diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts index e56ac166e4d..74f01705470 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts @@ -21,9 +21,9 @@ import { ThemedMediaViewerComponent } from '../../../media-viewer/themed-media-v import { MiradorViewerComponent } from '../../../mirador-viewer/mirador-viewer.component'; import { ThemedFileSectionComponent } from '../../field-components/file-section/themed-file-section.component'; import { ItemPageAbstractFieldComponent } from '../../field-components/specific-field/abstract/item-page-abstract-field.component'; -import { ItemPageLicenseFieldComponent } from '../../field-components/specific-field/license/item-page-license-field.component'; import { ItemPageDateFieldComponent } from '../../field-components/specific-field/date/item-page-date-field.component'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; +import { ItemPageLicenseFieldComponent } from '../../field-components/specific-field/license/item-page-license-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../field-components/specific-field/title/themed-item-page-field.component'; import { ItemPageUriFieldComponent } from '../../field-components/specific-field/uri/item-page-uri-field.component'; import { ThemedMetadataRepresentationListComponent } from '../../metadata-representation-list/themed-metadata-representation-list.component'; diff --git a/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts b/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts index 1463024df04..63827b6f327 100644 --- a/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts +++ b/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts @@ -8,6 +8,7 @@ import { } from '@angular/core'; import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; +import { ItemPageLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component'; import { Context } from '../../../../../../../app/core/shared/context.model'; import { ViewMode } from '../../../../../../../app/core/shared/view-mode.model'; @@ -20,7 +21,6 @@ import { ItemPageDateFieldComponent } from '../../../../../../../app/item-page/s import { GenericItemPageFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; import { ItemPageUriFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component'; -import { ItemPageLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component'; import { PublicationComponent as BaseComponent } from '../../../../../../../app/item-page/simple/item-types/publication/publication.component'; import { ThemedMetadataRepresentationListComponent } from '../../../../../../../app/item-page/simple/metadata-representation-list/themed-metadata-representation-list.component'; import { RelatedItemsComponent } from '../../../../../../../app/item-page/simple/related-items/related-items-component'; diff --git a/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts b/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts index ff3e1c34aed..e5429b098b0 100644 --- a/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts +++ b/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts @@ -17,9 +17,9 @@ import { ThemedMediaViewerComponent } from '../../../../../../../app/item-page/m import { MiradorViewerComponent } from '../../../../../../../app/item-page/mirador-viewer/mirador-viewer.component'; import { ThemedFileSectionComponent } from '../../../../../../../app/item-page/simple/field-components/file-section/themed-file-section.component'; import { ItemPageAbstractFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component'; -import { ItemPageLicenseFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/license/item-page-license-field.component'; import { ItemPageDateFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/date/item-page-date-field.component'; import { GenericItemPageFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; +import { ItemPageLicenseFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/license/item-page-license-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; import { ItemPageUriFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component'; import { UntypedItemComponent as BaseComponent } from '../../../../../../../app/item-page/simple/item-types/untyped-item/untyped-item.component'; From 4db6e92c3213b53d3d3d595b6e31a440c919ca9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Fri, 20 Sep 2024 15:09:50 +0200 Subject: [PATCH 05/18] Update UntypedItemComponent to use ItemPageLicenseFieldComponent rather than ItemPageCcLicenseFieldComponent (fixes #3170) From da42278a3081f98b0638f3d54f3743877103568b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Fri, 20 Sep 2024 15:11:25 +0200 Subject: [PATCH 06/18] Add rights information to Publication EntityType template (PublicationComponent) (fixes #3167) --- .../simple/item-types/publication/publication.component.ts | 1 + .../simple/item-types/publication/publication.component.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/app/item-page/simple/item-types/publication/publication.component.ts b/src/app/item-page/simple/item-types/publication/publication.component.ts index af4908c2a48..02ff4b47064 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.ts @@ -21,6 +21,7 @@ import { MiradorViewerComponent } from '../../../mirador-viewer/mirador-viewer.c import { ThemedFileSectionComponent } from '../../field-components/file-section/themed-file-section.component'; import { ItemPageAbstractFieldComponent } from '../../field-components/specific-field/abstract/item-page-abstract-field.component'; import { ItemPageDateFieldComponent } from '../../field-components/specific-field/date/item-page-date-field.component'; +import { ItemPageLicenseFieldComponent } from '../../field-components/specific-field/license/item-page-license-field.component'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; import { ItemPageLicenseFieldComponent } from '../../field-components/specific-field/license/item-page-license-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../field-components/specific-field/title/themed-item-page-field.component'; diff --git a/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts b/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts index 63827b6f327..af920003dfd 100644 --- a/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts +++ b/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts @@ -21,6 +21,7 @@ import { ItemPageDateFieldComponent } from '../../../../../../../app/item-page/s import { GenericItemPageFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; import { ItemPageUriFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component'; +import { ItemPageLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component'; import { PublicationComponent as BaseComponent } from '../../../../../../../app/item-page/simple/item-types/publication/publication.component'; import { ThemedMetadataRepresentationListComponent } from '../../../../../../../app/item-page/simple/metadata-representation-list/themed-metadata-representation-list.component'; import { RelatedItemsComponent } from '../../../../../../../app/item-page/simple/related-items/related-items-component'; From a89b1e17a6df66bd1388660812e4a724a9f17357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Fri, 20 Sep 2024 16:58:41 +0200 Subject: [PATCH 07/18] Fix lint errors * Trailing spaces * Missing trailing commas * Imports sorting * Equalities and typing --- .../simple/item-types/publication/publication.component.ts | 1 - .../simple/item-types/publication/publication.component.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/app/item-page/simple/item-types/publication/publication.component.ts b/src/app/item-page/simple/item-types/publication/publication.component.ts index 02ff4b47064..af4908c2a48 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.ts @@ -21,7 +21,6 @@ import { MiradorViewerComponent } from '../../../mirador-viewer/mirador-viewer.c import { ThemedFileSectionComponent } from '../../field-components/file-section/themed-file-section.component'; import { ItemPageAbstractFieldComponent } from '../../field-components/specific-field/abstract/item-page-abstract-field.component'; import { ItemPageDateFieldComponent } from '../../field-components/specific-field/date/item-page-date-field.component'; -import { ItemPageLicenseFieldComponent } from '../../field-components/specific-field/license/item-page-license-field.component'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; import { ItemPageLicenseFieldComponent } from '../../field-components/specific-field/license/item-page-license-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../field-components/specific-field/title/themed-item-page-field.component'; diff --git a/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts b/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts index af920003dfd..63827b6f327 100644 --- a/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts +++ b/src/themes/custom/app/item-page/simple/item-types/publication/publication.component.ts @@ -21,7 +21,6 @@ import { ItemPageDateFieldComponent } from '../../../../../../../app/item-page/s import { GenericItemPageFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; import { ItemPageUriFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component'; -import { ItemPageLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component'; import { PublicationComponent as BaseComponent } from '../../../../../../../app/item-page/simple/item-types/publication/publication.component'; import { ThemedMetadataRepresentationListComponent } from '../../../../../../../app/item-page/simple/metadata-representation-list/themed-metadata-representation-list.component'; import { RelatedItemsComponent } from '../../../../../../../app/item-page/simple/related-items/related-items-component'; From c8c4d99fc1afc35c598dbbcf12cf9a24c08851ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Fri, 20 Sep 2024 17:47:21 +0200 Subject: [PATCH 08/18] Fix comments --- .../license/item-page-license-field.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts index b6ff8ad7490..e767f0b3699 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -27,7 +27,7 @@ import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wra * Displays the item's licenses * * If the number of 'dc.rights*' values (excepting 'dc.rights.uri') and the number of 'dc.rights.uri' - * match, they will be printed as a list of links, where the text of the link will be the dc.right* + * match, they will be printed as a list of links, where the text of the link will be the 'dc.rights*' * value and the link the corresponding 'dc.rights.uri'. The match will be done in the order they * appear. In any other case, all the 'dc.rights*' fields will be shown as a list (where the URIs * will be rendered as links). @@ -39,7 +39,7 @@ export class ItemPageLicenseFieldComponent implements OnInit { @Input() item: Item; /** - * String to use as a separator if multiple license entries are specified + * String to use as a separator if multiple rights entries are specified */ @Input() separator = '•'; @@ -75,7 +75,7 @@ export class ItemPageLicenseFieldComponent implements OnInit { ccComponentRef.destroy(); // In either case... - // get all non-empty dc.rights* values, excepting the URIs... + // get all non-empty 'dc.rights*' values, excepting the URIs... this.licenses = Metadata .all(this.item.metadata, Object.keys(this.item.metadata).filter(key => key !== 'dc.rights.uri' && (key.startsWith('dc.rights') || key.startsWith('dc.rights.')))) .map(mdValue => mdValue.value).filter(value => value); From 397b7f47d016b66b803a615378979a045a474ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Mon, 21 Oct 2024 20:49:54 +0200 Subject: [PATCH 09/18] Use of the back-end config to check whether license is CC This PR relates to https://github.com/DSpace/dspace-angular/pull/3165 and https://github.com/DSpace/DSpace/pull/9882. --- .../item-page-cc-license-field.component.ts | 15 +++- .../item-page-license-field.component.html | 2 +- .../item-page-license-field.component.spec.ts | 22 +++++- .../item-page-license-field.component.ts | 77 ++++++++++++------- .../publication/publication.component.spec.ts | 3 + .../untyped-item.component.spec.ts | 3 + 6 files changed, 88 insertions(+), 34 deletions(-) diff --git a/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts index e06d0c25b56..b5685005ef5 100644 --- a/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts @@ -60,14 +60,23 @@ export class ItemPageCcLicenseFieldComponent implements OnInit { showImage = true; imgSrc: string; + /** + * Parse a URI an return its CC code. URIs pointing to non-CC licenses will return null. + * @param uri + * @returns the CC code or null if uri is not a valid CC URI + */ + public static parseCcCode(uri: string): string { + const regex = /.*creativecommons.org\/(licenses|publicdomain)\/([^/]+)/gm; + const matches = regex.exec(uri ?? '') ?? []; + return matches.length > 2 ? matches[2] : null; + } + ngOnInit() { this.uri = this.item.firstMetadataValue(this.ccLicenseUriField); this.name = this.item.firstMetadataValue(this.ccLicenseNameField); // Extracts the CC license code from the URI - const regex = /.*creativecommons.org\/(licenses|publicdomain)\/([^/]+)/gm; - const matches = regex.exec(this.uri ?? '') ?? []; - const ccCode = matches.length > 2 ? matches[2] : null; + const ccCode = ItemPageCcLicenseFieldComponent.parseCcCode(this.uri); this.imgSrc = ccCode ? `assets/images/cc-licenses/${ccCode}.png` : null; } } diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html index 8cee4373c12..acd022ccd39 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html @@ -1,5 +1,5 @@
- + diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts index 759a0d518dc..45e021f9c31 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts @@ -12,12 +12,15 @@ import { TranslateLoader, TranslateModule, } from '@ngx-translate/core'; +import { ConfigurationDataService } from 'src/app/core/data/configuration-data.service'; +import { ConfigurationProperty } from 'src/app/core/shared/configuration-property.model'; import { Item } from 'src/app/core/shared/item.model'; import { MetadataMap, MetadataValue, } from 'src/app/core/shared/metadata.models'; import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils'; +import { ConfigurationDataServiceStub } from 'src/app/shared/testing/configuration-data.service.stub'; import { createPaginatedList } from 'src/app/shared/testing/utils.test'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; @@ -213,8 +216,22 @@ function configureFixture( describe('ItemPageLicenseFieldComponent', () => { let fixture: ComponentFixture; + let configurationDataService = new ConfigurationDataServiceStub(); beforeEach(waitForAsync(() => { + configurationDataService.findByPropertyName = jasmine.createSpy() + .withArgs('cc.license.name').and.returnValue(createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: 'cc.license.name', + values: [ 'dc.rights' ], + }, + )) + .withArgs('cc.license.uri').and.returnValue(createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: 'cc.license.uri', + values: [ 'dc.rights.uri' ], + }, + )); void TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot({ @@ -225,7 +242,10 @@ describe('ItemPageLicenseFieldComponent', () => { }), ItemPageLicenseFieldComponent, ], - providers: [{ provide: APP_CONFIG, useValue: environment }], + providers: [ + { provide: APP_CONFIG, useValue: environment }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + ], schemas: [NO_ERRORS_SCHEMA], }) .overrideComponent(ItemPageLicenseFieldComponent, { diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts index e767f0b3699..f061ceefeca 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -1,4 +1,5 @@ import { + AsyncPipe, NgClass, NgFor, NgIf, @@ -7,12 +8,24 @@ import { import { Component, Input, + OnDestroy, OnInit, ViewContainerRef, } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; +import { + Observable, + of, + Subscription, +} from 'rxjs'; +import { ConfigurationDataService } from 'src/app/core/data/configuration-data.service'; +import { ConfigurationProperty } from 'src/app/core/shared/configuration-property.model'; import { Item } from 'src/app/core/shared/item.model'; import { Metadata } from 'src/app/core/shared/metadata.utils'; +import { + getFirstCompletedRemoteData, + getRemoteDataPayload, +} from 'src/app/core/shared/operators'; import { ItemPageCcLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component'; import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; @@ -21,7 +34,7 @@ import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wra templateUrl: './item-page-license-field.component.html', styleUrl: './item-page-license-field.scss', standalone: true, - imports: [NgIf, NgFor, NgClass, NgStyle, TranslateModule, MetadataFieldWrapperComponent, ItemPageCcLicenseFieldComponent], + imports: [NgIf, NgFor, NgClass, NgStyle, TranslateModule, MetadataFieldWrapperComponent, ItemPageCcLicenseFieldComponent, AsyncPipe], }) /** * Displays the item's licenses @@ -32,7 +45,7 @@ import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wra * appear. In any other case, all the 'dc.rights*' fields will be shown as a list (where the URIs * will be rendered as links). */ -export class ItemPageLicenseFieldComponent implements OnInit { +export class ItemPageLicenseFieldComponent implements OnInit, OnDestroy { /** * The item to display the license for */ @@ -43,39 +56,45 @@ export class ItemPageLicenseFieldComponent implements OnInit { */ @Input() separator = '•'; - uris: string[]; + subscriptions: Subscription[] = []; + + hasCcLicenseName$: Observable; + hasCcLicenseUri$: Observable; + licenses: string[]; - isCcLicense: boolean; + uris: string[]; - constructor(private viewRef: ViewContainerRef) {} + constructor( + protected viewRef: ViewContainerRef, + protected configService: ConfigurationDataService, + ) {} + + ngOnDestroy(): void { + this.subscriptions.forEach((sub: Subscription) => sub.unsubscribe()); + } ngOnInit() { - // This is a workaround to determine which fields are relevant to render a CC license - // until https://github.com/DSpace/dspace-angular/pull/3165 is merged in the main branch - // (which will provide a configuration property). - // Until then, we must dynamically create a ItemPageCcLicenseFieldComponent to retrieve - // the fields that are relevant to render a CC license field (since developers may have - // customized them directly in the code). - // This approach avoids hardcoding the values (thus breaking developers' customizations) - // and makes this code compatible with a future merge of the above PR - const ccComponentRef = this.viewRef.createComponent(ItemPageCcLicenseFieldComponent); - ccComponentRef.setInput('item', this.item); - // The regex below has been copied from ItemPageCcLicenseFieldComponent - // We duplicate the regex here to avoid changing the implementation of - // ItemPageCcLicenseFieldComponent to avoid breaking changes. - // It may be desirable to further refactor this code in the future - const regex = /.*creativecommons.org\/(licenses|publicdomain)\/([^/]+)/gm; + // First, retrieve from the back-end the configuration regarding CC fields... + this.subscriptions.push(this.configService.findByPropertyName('cc.license.uri').pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + ).subscribe((remoteData: ConfigurationProperty) => { + const ccLicenseUriField = remoteData?.values && remoteData?.values?.length > 0 ? remoteData.values[0] : 'dc.rights.uri'; + this.hasCcLicenseUri$ = of(!!ItemPageCcLicenseFieldComponent.parseCcCode(this.item.firstMetadataValue(ccLicenseUriField))); + }), + ); - // If the license information is a CC License, we will delegate the rendering - // of this field to ItemPageCcLicenseFieldComponent - this.isCcLicense = this.item.allMetadataValues(ccComponentRef.instance.ccLicenseUriField).length === 1 - && regex.exec(this.item.firstMetadataValue(ccComponentRef.instance.ccLicenseUriField)) != null - && this.item.allMetadataValues(ccComponentRef.instance.ccLicenseNameField).length === 1; - // We no longer need the ItemPageCcLicenseFieldComponent (and we don't want it to be rendered here) - ccComponentRef.destroy(); + this.subscriptions.push(this.configService.findByPropertyName('cc.license.name').pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + ).subscribe((remoteData: ConfigurationProperty) => { + const ccLicenseNameField = remoteData?.values && remoteData?.values?.length > 0 ? remoteData.values[0] : 'dc.rights'; + this.hasCcLicenseName$ = of(!!this.item.firstMetadataValue(ccLicenseNameField)); + }), + ); - // In either case... - // get all non-empty 'dc.rights*' values, excepting the URIs... + // Now, get the data for this component, in case we need to render the license data as a generic license... + // Get all non-empty 'dc.rights*' values, excepting the URIs... this.licenses = Metadata .all(this.item.metadata, Object.keys(this.item.metadata).filter(key => key !== 'dc.rights.uri' && (key.startsWith('dc.rights') || key.startsWith('dc.rights.')))) .map(mdValue => mdValue.value).filter(value => value); diff --git a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts index 49c486b16b2..c0a5a4a7a59 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts @@ -21,6 +21,8 @@ import { Observable, of, } from 'rxjs'; +import { ConfigurationDataService } from 'src/app/core/data/configuration-data.service'; +import { ConfigurationDataServiceStub } from 'src/app/shared/testing/configuration-data.service.stub'; import { APP_CONFIG, @@ -131,6 +133,7 @@ describe('PublicationComponent', () => { { provide: SearchService, useValue: {} }, { provide: RouteService, useValue: mockRouteService }, { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: ConfigurationDataService, useValue: new ConfigurationDataServiceStub() }, { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, ], diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts index a03b76e24eb..22ae36737fb 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts @@ -21,6 +21,8 @@ import { Observable, of, } from 'rxjs'; +import { ConfigurationDataService } from 'src/app/core/data/configuration-data.service'; +import { ConfigurationDataServiceStub } from 'src/app/shared/testing/configuration-data.service.stub'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { environment } from '../../../../../environments/environment.test'; @@ -130,6 +132,7 @@ describe('UntypedItemComponent', () => { { provide: ItemVersionsSharedService, useValue: {} }, { provide: RouteService, useValue: mockRouteService }, { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: ConfigurationDataService, useValue: new ConfigurationDataServiceStub() }, { provide: APP_CONFIG, useValue: environment }, ], schemas: [NO_ERRORS_SCHEMA], From 79b2fee5b24fbab43eee9501424718ef44074864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Wed, 26 Mar 2025 19:20:25 +0100 Subject: [PATCH 10/18] Use innerHTML for the separator --- .../license/item-page-license-field.component.html | 6 +++--- .../license/item-page-license-field.component.ts | 3 +-- .../specific-field/license/item-page-license-field.scss | 4 ---- 3 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html index acd022ccd39..cbcaaa98eac 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html @@ -11,17 +11,17 @@ {{ license }} - {{ separator }} + {{ license }} - {{ separator }} + {{ uri }} - {{ separator }} + diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts index f061ceefeca..f8e73afef85 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -32,7 +32,6 @@ import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wra @Component({ selector: 'ds-item-page-license-field', templateUrl: './item-page-license-field.component.html', - styleUrl: './item-page-license-field.scss', standalone: true, imports: [NgIf, NgFor, NgClass, NgStyle, TranslateModule, MetadataFieldWrapperComponent, ItemPageCcLicenseFieldComponent, AsyncPipe], }) @@ -54,7 +53,7 @@ export class ItemPageLicenseFieldComponent implements OnInit, OnDestroy { /** * String to use as a separator if multiple rights entries are specified */ - @Input() separator = '•'; + @Input() separator = '
'; subscriptions: Subscription[] = []; diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss deleted file mode 100644 index c81c3055053..00000000000 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss +++ /dev/null @@ -1,4 +0,0 @@ -.separator { - padding-left: 0.5rem; - padding-right: 0.5rem; -} \ No newline at end of file From 1a41dcbb22c19df2cf7b35156587d281819a45f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Wed, 26 Mar 2025 20:05:46 +0100 Subject: [PATCH 11/18] Migrate to new Angular control flow syntax --- .../item-page-license-field.component.html | 60 ++++++++++--------- .../item-page-license-field.component.ts | 2 +- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html index cbcaaa98eac..65002103e44 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html @@ -1,29 +1,31 @@ -
- - - - - - - - - - - - {{ license }} - - - - - - {{ license }} - - - - {{ uri }} - - - - - -
+@if (licenses.length > 0 || uris.length > 0) { +
+ @if ((hasCcLicenseName$ | async) && (hasCcLicenseUri$ | async)) { + + } @else { + + @if (licenses.length === uris.length) { + @for (license of licenses; track license; let last=$last; let i=$index) { + {{ license }} + @if (!last) { + + } + } + } @else { + @for (license of licenses; track license; let last=$last) { + {{ license }} + @if (!last || uris.length > 0) { + + } + } + @for (uri of uris; track uri; let last=$last) { + {{ uri }} + @if (!last) { + + } + } + } + + } +
+} diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts index f8e73afef85..3002179c88e 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -33,7 +33,7 @@ import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wra selector: 'ds-item-page-license-field', templateUrl: './item-page-license-field.component.html', standalone: true, - imports: [NgIf, NgFor, NgClass, NgStyle, TranslateModule, MetadataFieldWrapperComponent, ItemPageCcLicenseFieldComponent, AsyncPipe], + imports: [NgClass, NgStyle, TranslateModule, MetadataFieldWrapperComponent, ItemPageCcLicenseFieldComponent, AsyncPipe], }) /** * Displays the item's licenses From a5931135a92e062598439afc9b29b41ac39ed17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Wed, 26 Mar 2025 20:13:50 +0100 Subject: [PATCH 12/18] Remove unused imports --- .../specific-field/license/item-page-license-field.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts index 3002179c88e..26b527ff48b 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -1,8 +1,6 @@ import { AsyncPipe, NgClass, - NgFor, - NgIf, NgStyle, } from '@angular/common'; import { From 808d44b5b478f9fe02578938df15f5f1ea53a18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Wed, 26 Mar 2025 20:25:47 +0100 Subject: [PATCH 13/18] Move parsing functions for CC licenses to a utility file --- .../item-page-cc-license-field.component.ts | 14 ++------------ .../item-page-license-field.component.ts | 3 ++- src/app/shared/utils/license.utils.ts | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 src/app/shared/utils/license.utils.ts diff --git a/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts index eee02b6075e..27337473512 100644 --- a/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts @@ -10,6 +10,7 @@ import { import { TranslateModule } from '@ngx-translate/core'; import { Item } from 'src/app/core/shared/item.model'; import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; +import { parseCcCode } from 'src/app/shared/utils/license.utils'; @Component({ selector: 'ds-item-page-cc-license-field', @@ -59,23 +60,12 @@ export class ItemPageCcLicenseFieldComponent implements OnInit { showImage = true; imgSrc: string; - /** - * Parse a URI an return its CC code. URIs pointing to non-CC licenses will return null. - * @param uri - * @returns the CC code or null if uri is not a valid CC URI - */ - public static parseCcCode(uri: string): string { - const regex = /.*creativecommons.org\/(licenses|publicdomain)\/([^/]+)/gm; - const matches = regex.exec(uri ?? '') ?? []; - return matches.length > 2 ? matches[2] : null; - } - ngOnInit() { this.uri = this.item.firstMetadataValue(this.ccLicenseUriField); this.name = this.item.firstMetadataValue(this.ccLicenseNameField); // Extracts the CC license code from the URI - const ccCode = ItemPageCcLicenseFieldComponent.parseCcCode(this.uri); + const ccCode = parseCcCode(this.uri); this.imgSrc = ccCode ? `assets/images/cc-licenses/${ccCode}.png` : null; } } diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts index 26b527ff48b..bd092090093 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -26,6 +26,7 @@ import { } from 'src/app/core/shared/operators'; import { ItemPageCcLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component'; import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; +import { isCcLicense } from 'src/app/shared/utils/license.utils'; @Component({ selector: 'ds-item-page-license-field', @@ -77,7 +78,7 @@ export class ItemPageLicenseFieldComponent implements OnInit, OnDestroy { getRemoteDataPayload(), ).subscribe((remoteData: ConfigurationProperty) => { const ccLicenseUriField = remoteData?.values && remoteData?.values?.length > 0 ? remoteData.values[0] : 'dc.rights.uri'; - this.hasCcLicenseUri$ = of(!!ItemPageCcLicenseFieldComponent.parseCcCode(this.item.firstMetadataValue(ccLicenseUriField))); + this.hasCcLicenseUri$ = of(isCcLicense(this.item.firstMetadataValue(ccLicenseUriField))); }), ); diff --git a/src/app/shared/utils/license.utils.ts b/src/app/shared/utils/license.utils.ts new file mode 100644 index 00000000000..0d2c7424577 --- /dev/null +++ b/src/app/shared/utils/license.utils.ts @@ -0,0 +1,19 @@ +/** + * Parse a URI an return its CC code. URIs pointing to non-CC licenses will return null. + * @param uri + * @returns the CC code or null if uri is not a valid CC URI + */ +export function parseCcCode(uri: string): string { + const regex = /.*creativecommons.org\/(licenses|publicdomain)\/([^/]+)/gm; + const matches = regex.exec(uri ?? '') ?? []; + return matches.length > 2 ? matches[2] : null; +} + +/** + * Returns whether a URI denotes a valid URI for CC licenses. + * @param uri + * @returns true if the URI corresponds to a reconigzable CC license URI or false otherwise + */ +export function isCcLicense(uri: string): boolean { + return !!parseCcCode(uri); +} From d7e8b87342fa56553bccb31cbb40caa0e436c8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Wed, 26 Mar 2025 20:33:07 +0100 Subject: [PATCH 14/18] Remove comment no longer needed --- .../cc-license/item-page-cc-license-field.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts index 27337473512..489312279b8 100644 --- a/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts @@ -64,7 +64,6 @@ export class ItemPageCcLicenseFieldComponent implements OnInit { this.uri = this.item.firstMetadataValue(this.ccLicenseUriField); this.name = this.item.firstMetadataValue(this.ccLicenseNameField); - // Extracts the CC license code from the URI const ccCode = parseCcCode(this.uri); this.imgSrc = ccCode ? `assets/images/cc-licenses/${ccCode}.png` : null; } From bca531c9c6bb17e272471ee4d665f9422ca77d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Tue, 1 Apr 2025 17:27:24 +0200 Subject: [PATCH 15/18] Don't use manual subscriptions --- .../item-page-license-field.component.ts | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts index bd092090093..f26dd134f41 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -12,6 +12,7 @@ import { } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { + map, Observable, of, Subscription, @@ -25,6 +26,7 @@ import { getRemoteDataPayload, } from 'src/app/core/shared/operators'; import { ItemPageCcLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component'; +import { hasValue } from 'src/app/shared/empty.util'; import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; import { isCcLicense } from 'src/app/shared/utils/license.utils'; @@ -43,7 +45,7 @@ import { isCcLicense } from 'src/app/shared/utils/license.utils'; * appear. In any other case, all the 'dc.rights*' fields will be shown as a list (where the URIs * will be rendered as links). */ -export class ItemPageLicenseFieldComponent implements OnInit, OnDestroy { +export class ItemPageLicenseFieldComponent implements OnInit { /** * The item to display the license for */ @@ -67,28 +69,24 @@ export class ItemPageLicenseFieldComponent implements OnInit, OnDestroy { protected configService: ConfigurationDataService, ) {} - ngOnDestroy(): void { - this.subscriptions.forEach((sub: Subscription) => sub.unsubscribe()); - } - ngOnInit() { // First, retrieve from the back-end the configuration regarding CC fields... - this.subscriptions.push(this.configService.findByPropertyName('cc.license.uri').pipe( + this.hasCcLicenseName$ = this.configService.findByPropertyName('cc.license.name').pipe( getFirstCompletedRemoteData(), getRemoteDataPayload(), - ).subscribe((remoteData: ConfigurationProperty) => { - const ccLicenseUriField = remoteData?.values && remoteData?.values?.length > 0 ? remoteData.values[0] : 'dc.rights.uri'; - this.hasCcLicenseUri$ = of(isCcLicense(this.item.firstMetadataValue(ccLicenseUriField))); - }), + map((configurationProperty: ConfigurationProperty) => configurationProperty?.values?.[0]), + map((metadataField: string) => hasValue(metadataField) ? metadataField : 'dc.rights'), + map((metadataField: string) => this.item.firstMetadataValue(metadataField)), + map((metadataValue: string) => hasValue(metadataValue)), ); - this.subscriptions.push(this.configService.findByPropertyName('cc.license.name').pipe( + this.hasCcLicenseUri$ = this.configService.findByPropertyName('cc.license.uri').pipe( getFirstCompletedRemoteData(), getRemoteDataPayload(), - ).subscribe((remoteData: ConfigurationProperty) => { - const ccLicenseNameField = remoteData?.values && remoteData?.values?.length > 0 ? remoteData.values[0] : 'dc.rights'; - this.hasCcLicenseName$ = of(!!this.item.firstMetadataValue(ccLicenseNameField)); - }), + map((configurationProperty: ConfigurationProperty) => configurationProperty?.values?.[0]), + map((metadataField: string) => hasValue(metadataField) ? metadataField : 'dc.rights'), + map((metadataField: string) => this.item.firstMetadataValue(metadataField)), + map((metadataValue: string) => isCcLicense(metadataValue)), ); // Now, get the data for this component, in case we need to render the license data as a generic license... From 57bea016a8ea56d680dee504b571e2b260513aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Tue, 1 Apr 2025 17:27:24 +0200 Subject: [PATCH 16/18] Don't use manual subscriptions --- .../item-page-license-field.component.ts | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts index bd092090093..0162bc3616b 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -6,15 +6,13 @@ import { import { Component, Input, - OnDestroy, OnInit, ViewContainerRef, } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { + map, Observable, - of, - Subscription, } from 'rxjs'; import { ConfigurationDataService } from 'src/app/core/data/configuration-data.service'; import { ConfigurationProperty } from 'src/app/core/shared/configuration-property.model'; @@ -25,6 +23,7 @@ import { getRemoteDataPayload, } from 'src/app/core/shared/operators'; import { ItemPageCcLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component'; +import { hasValue } from 'src/app/shared/empty.util'; import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; import { isCcLicense } from 'src/app/shared/utils/license.utils'; @@ -43,7 +42,7 @@ import { isCcLicense } from 'src/app/shared/utils/license.utils'; * appear. In any other case, all the 'dc.rights*' fields will be shown as a list (where the URIs * will be rendered as links). */ -export class ItemPageLicenseFieldComponent implements OnInit, OnDestroy { +export class ItemPageLicenseFieldComponent implements OnInit { /** * The item to display the license for */ @@ -54,8 +53,6 @@ export class ItemPageLicenseFieldComponent implements OnInit, OnDestroy { */ @Input() separator = '
'; - subscriptions: Subscription[] = []; - hasCcLicenseName$: Observable; hasCcLicenseUri$: Observable; @@ -67,28 +64,24 @@ export class ItemPageLicenseFieldComponent implements OnInit, OnDestroy { protected configService: ConfigurationDataService, ) {} - ngOnDestroy(): void { - this.subscriptions.forEach((sub: Subscription) => sub.unsubscribe()); - } - ngOnInit() { // First, retrieve from the back-end the configuration regarding CC fields... - this.subscriptions.push(this.configService.findByPropertyName('cc.license.uri').pipe( + this.hasCcLicenseName$ = this.configService.findByPropertyName('cc.license.name').pipe( getFirstCompletedRemoteData(), getRemoteDataPayload(), - ).subscribe((remoteData: ConfigurationProperty) => { - const ccLicenseUriField = remoteData?.values && remoteData?.values?.length > 0 ? remoteData.values[0] : 'dc.rights.uri'; - this.hasCcLicenseUri$ = of(isCcLicense(this.item.firstMetadataValue(ccLicenseUriField))); - }), + map((configurationProperty: ConfigurationProperty) => configurationProperty?.values?.[0]), + map((metadataField: string) => hasValue(metadataField) ? metadataField : 'dc.rights'), + map((metadataField: string) => this.item.firstMetadataValue(metadataField)), + map((metadataValue: string) => hasValue(metadataValue)), ); - this.subscriptions.push(this.configService.findByPropertyName('cc.license.name').pipe( + this.hasCcLicenseUri$ = this.configService.findByPropertyName('cc.license.uri').pipe( getFirstCompletedRemoteData(), getRemoteDataPayload(), - ).subscribe((remoteData: ConfigurationProperty) => { - const ccLicenseNameField = remoteData?.values && remoteData?.values?.length > 0 ? remoteData.values[0] : 'dc.rights'; - this.hasCcLicenseName$ = of(!!this.item.firstMetadataValue(ccLicenseNameField)); - }), + map((configurationProperty: ConfigurationProperty) => configurationProperty?.values?.[0]), + map((metadataField: string) => hasValue(metadataField) ? metadataField : 'dc.rights'), + map((metadataField: string) => this.item.firstMetadataValue(metadataField)), + map((metadataValue: string) => isCcLicense(metadataValue)), ); // Now, get the data for this component, in case we need to render the license data as a generic license... From 256b7e2c3af0bdad3260d84f5e4f968752394f2b Mon Sep 17 00:00:00 2001 From: Abel Date: Mon, 16 Feb 2026 21:58:16 +0100 Subject: [PATCH 17/18] Fix tests --- .../item-page-license-field.component.spec.ts | 24 +++++++++---------- .../publication/publication.component.spec.ts | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts index 45e021f9c31..7e34aa7255d 100644 --- a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts @@ -8,24 +8,24 @@ import { waitForAsync, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { APP_CONFIG } from '@dspace/config/app-config.interface'; +import { ConfigurationDataService } from '@dspace/core/data/configuration-data.service'; +import { ConfigurationProperty } from '@dspace/core/shared/configuration-property.model'; +import { Item } from '@dspace/core/shared/item.model'; +import { + MetadataMap, + MetadataValue, +} from '@dspace/core/shared/metadata.models'; +import { ConfigurationDataServiceStub } from '@dspace/core/testing/configuration-data.service.stub'; +import { TranslateLoaderMock } from '@dspace/core/testing/translate-loader.mock'; +import { createPaginatedList } from '@dspace/core/testing/utils.test'; +import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils'; import { TranslateLoader, TranslateModule, } from '@ngx-translate/core'; -import { ConfigurationDataService } from 'src/app/core/data/configuration-data.service'; -import { ConfigurationProperty } from 'src/app/core/shared/configuration-property.model'; -import { Item } from 'src/app/core/shared/item.model'; -import { - MetadataMap, - MetadataValue, -} from 'src/app/core/shared/metadata.models'; -import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils'; -import { ConfigurationDataServiceStub } from 'src/app/shared/testing/configuration-data.service.stub'; -import { createPaginatedList } from 'src/app/shared/testing/utils.test'; -import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; -import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; import { ItemPageLicenseFieldComponent } from './item-page-license-field.component'; diff --git a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts index e33e64dff8d..bd8b7efa347 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts @@ -18,6 +18,7 @@ import { RemoteDataBuildService } from '@dspace/core/cache/builders/remote-data- import { ObjectCacheService } from '@dspace/core/cache/object-cache.service'; import { BitstreamDataService } from '@dspace/core/data/bitstream-data.service'; import { CommunityDataService } from '@dspace/core/data/community-data.service'; +import { ConfigurationDataService } from '@dspace/core/data/configuration-data.service'; import { DefaultChangeAnalyzer } from '@dspace/core/data/default-change-analyzer.service'; import { DSOChangeAnalyzer } from '@dspace/core/data/dso-change-analyzer.service'; import { ItemDataService } from '@dspace/core/data/item-data.service'; @@ -35,6 +36,7 @@ import { MetadataMap } from '@dspace/core/shared/metadata.models'; import { UUIDService } from '@dspace/core/shared/uuid.service'; import { WorkspaceitemDataService } from '@dspace/core/submission/workspaceitem-data.service'; import { BrowseDefinitionDataServiceStub } from '@dspace/core/testing/browse-definition-data-service.stub'; +import { ConfigurationDataServiceStub } from '@dspace/core/testing/configuration-data.service.stub'; import { mockTruncatableService } from '@dspace/core/testing/mock-trucatable.service'; import { TranslateLoaderMock } from '@dspace/core/testing/translate-loader.mock'; import { createPaginatedList } from '@dspace/core/testing/utils.test'; @@ -48,8 +50,6 @@ import { Observable, of, } from 'rxjs'; -import { ConfigurationDataService } from 'src/app/core/data/configuration-data.service'; -import { ConfigurationDataServiceStub } from 'src/app/shared/testing/configuration-data.service.stub'; import { environment } from '../../../../../environments/environment.test'; import { DsoEditMenuComponent } from '../../../../shared/dso-page/dso-edit-menu/dso-edit-menu.component'; From 8b0d83fdf326823787bb8b1d3700e7f3a59a12c9 Mon Sep 17 00:00:00 2001 From: Abel Date: Tue, 17 Feb 2026 01:07:04 +0100 Subject: [PATCH 18/18] Trigger Build