diff --git a/src/app/core/handle/HandleResourceTypeIdserializer.ts b/src/app/core/handle/HandleResourceTypeIdserializer.ts
index 8e2f765c018..e88af9af483 100644
--- a/src/app/core/handle/HandleResourceTypeIdserializer.ts
+++ b/src/app/core/handle/HandleResourceTypeIdserializer.ts
@@ -14,6 +14,8 @@ export const HandleResourceTypeIdSerializer = {
return 3;
case COMMUNITY:
return 4;
+ case SITE:
+ return 5;
default:
return null;
}
@@ -27,8 +29,10 @@ export const HandleResourceTypeIdSerializer = {
return COLLECTION;
case 4:
return COMMUNITY;
- default:
+ case 5:
return SITE;
+ default:
+ return null;
}
}
};
diff --git a/src/app/core/handle/handle.resource-type.ts b/src/app/core/handle/handle.resource-type.ts
index f4728c150be..61e155c770b 100644
--- a/src/app/core/handle/handle.resource-type.ts
+++ b/src/app/core/handle/handle.resource-type.ts
@@ -9,6 +9,7 @@ import { ResourceType } from '../shared/resource-type';
export const HANDLE = new ResourceType('handle');
export const SUCCESSFUL_RESPONSE_START_CHAR = '2';
+export const INVALID_RESOURCE_TYPE_ID = -1;
export const COMMUNITY = 'Community';
export const COLLECTION = 'Collection';
export const ITEM = 'Item';
diff --git a/src/app/handle-page/handle-table/handle-table.component.html b/src/app/handle-page/handle-table/handle-table.component.html
index ae1d34ee202..553992d3b90 100644
--- a/src/app/handle-page/handle-table/handle-table.component.html
+++ b/src/app/handle-page/handle-table/handle-table.component.html
@@ -11,7 +11,7 @@
class="btn btn-outline-secondary dropdown-toggle"
data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
- {{searchOption}}
+ {{searchOption}}
{{'handle-table.dropdown.search-option' | translate}}
@@ -20,9 +20,53 @@
-
+
+
+
+
@@ -70,7 +114,7 @@
- {{handle?.resourceTypeID}}
+ {{getTranslatedResourceType(handle?.resourceTypeID)}}
|
diff --git a/src/app/handle-page/handle-table/handle-table.component.scss b/src/app/handle-page/handle-table/handle-table.component.scss
index d1e780d255e..a2bf95ccb9b 100644
--- a/src/app/handle-page/handle-table/handle-table.component.scss
+++ b/src/app/handle-page/handle-table/handle-table.component.scss
@@ -1,3 +1,48 @@
/**
- The file for styling `handle-table.component.html`. No styling needed.
+ * Styling for handle-table component search functionality
*/
+
+// Make select elements look like buttons to match previous dropdown styling
+.search-input-container {
+ .select-wrapper {
+ position: relative;
+ display: inline-block;
+ width: 100%;
+ }
+
+ .form-select {
+ background: white;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+ padding: 0.375rem 2.5rem 0.375rem 0.75rem;
+ color: #495057;
+ appearance: none;
+ cursor: pointer;
+
+ &:focus {
+ border-color: #80bdff;
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
+ outline: 0;
+ }
+
+ &:disabled {
+ background-color: #f8f9fa;
+ color: #6c757d;
+ cursor: not-allowed;
+ }
+ }
+
+ .select-icon {
+ position: absolute;
+ right: 0.75rem;
+ top: 50%;
+ transform: translateY(-50%);
+ pointer-events: none;
+ color: #495057;
+ font-size: 0.875rem;
+ }
+
+ .form-control {
+ border-radius: 0;
+ }
+}
diff --git a/src/app/handle-page/handle-table/handle-table.component.ts b/src/app/handle-page/handle-table/handle-table.component.ts
index 2c86e160e0c..2cd0d35965e 100644
--- a/src/app/handle-page/handle-table/handle-table.component.ts
+++ b/src/app/handle-page/handle-table/handle-table.component.ts
@@ -24,6 +24,7 @@ import { Handle } from '../../core/handle/handle.model';
import {
COLLECTION,
COMMUNITY,
+ INVALID_RESOURCE_TYPE_ID,
ITEM,
SITE,
SUCCESSFUL_RESPONSE_START_CHAR
@@ -31,6 +32,7 @@ import {
import { getCommunityPageRoute } from '../../community-page/community-page-routing-paths';
import { getCollectionPageRoute } from '../../collection-page/collection-page-routing-paths';
import { getEntityPageRoute } from '../../item-page/item-page-routing-paths';
+import { HandleResourceTypeIdSerializer } from '../../core/handle/HandleResourceTypeIdserializer';
/**
* Constants for converting the searchQuery for the server
@@ -359,6 +361,47 @@ export class HandleTableComponent implements OnInit {
*/
setSearchOption(event) {
this.searchOption = event?.target?.innerHTML;
+ // Reset search query when changing search option
+ this.searchQuery = '';
+ }
+
+ /**
+ * Get translated resource type name for table display
+ * Converts constants like 'Community', 'Collection', 'Item', 'Site' to translated strings
+ */
+ getTranslatedResourceType(resourceTypeID: string): string {
+ if (!resourceTypeID) {
+ return '';
+ }
+
+ // Map the constant values to lowercase for translation keys
+ const resourceTypeKey = resourceTypeID.toLowerCase();
+ const translationKey = `handle-table.search.resource-type.${resourceTypeKey}`;
+
+ // Return translated value, fallback to original if translation not found
+ const translated = this.translateService.instant(translationKey);
+ return translated !== translationKey ? translated : resourceTypeID;
+ }
+
+ /**
+ * Parse internal search query to server format
+ */
+ private parseInternalSearchQuery(searchQuery: string): string {
+ const normalizedQuery = searchQuery.toLowerCase();
+ if (normalizedQuery === 'yes') {
+ return 'internal';
+ } else if (normalizedQuery === 'no') {
+ return 'external';
+ }
+ return searchQuery;
+ }
+
+ /**
+ * Parse resource type search query to server format (converts to numeric ID)
+ */
+ private parseResourceTypeSearchQuery(searchQuery: string): string {
+ const id = HandleResourceTypeIdSerializer.Serialize(searchQuery);
+ return id ? id.toString() : INVALID_RESOURCE_TYPE_ID.toString();
}
/**
@@ -381,35 +424,12 @@ export class HandleTableComponent implements OnInit {
parsedSearchOption = HANDLE_SEARCH_OPTION;
break;
case this.internalOption:
- // if the handle doesn't have the URL - is internal, if it does - is external
parsedSearchOption = URL_SEARCH_OPTION;
- if (this.searchQuery.toLowerCase() === 'yes') {
- parsedSearchQuery = 'internal';
- } else if (this.searchQuery.toLowerCase() === 'no') {
- parsedSearchQuery = 'external';
- }
+ parsedSearchQuery = this.parseInternalSearchQuery(this.searchQuery);
break;
case this.resourceTypeOption:
parsedSearchOption = RESOURCE_TYPE_SEARCH_OPTION;
- // parse resourceType from string to the number because the resourceType is integer on the server
- switch (this.searchQuery.toLowerCase()) {
- case ITEM.toLowerCase():
- parsedSearchQuery = '' + 2;
- break;
- case COLLECTION.toLowerCase():
- parsedSearchQuery = '' + 3;
- break;
- case COMMUNITY.toLowerCase():
- parsedSearchQuery = '' + 4;
- break;
- case SITE.toLowerCase():
- parsedSearchQuery = '' + 5;
- break;
- // no results for invalid search inputs
- default:
- parsedSearchQuery = '' + -1;
- break;
- }
+ parsedSearchQuery = this.parseResourceTypeSearchQuery(this.searchQuery);
break;
}
}
diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts
index 18bbc4fd606..bc6d6879332 100644
--- a/src/app/item-page/item-page.module.ts
+++ b/src/app/item-page/item-page.module.ts
@@ -90,6 +90,8 @@ import { ClarinIdentifierItemFieldComponent } from './simple/field-components/cl
import { ClarinDateItemFieldComponent } from './simple/field-components/clarin-date-item-field/clarin-date-item-field.component';
import { ClarinDescriptionItemFieldComponent } from './simple/field-components/clarin-description-item-field/clarin-description-item-field.component';
import { ClarinFilesSectionComponent } from './clarin-files-section/clarin-files-section.component';
+import { UsageReportDataService } from '../core/statistics/usage-report-data.service';
+import { CreativeCommonsLicenseFieldComponent } from './simple/field-components/creative-commons-license-field/creative-commons-license-field.component';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -154,7 +156,8 @@ const DECLARATIONS = [
ClarinIdentifierItemFieldComponent,
ClarinDateItemFieldComponent,
ClarinDescriptionItemFieldComponent,
- ClarinFilesSectionComponent
+ ClarinFilesSectionComponent,
+ CreativeCommonsLicenseFieldComponent
];
@NgModule({
diff --git a/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.html b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.html
new file mode 100644
index 00000000000..21566fb02aa
--- /dev/null
+++ b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.html
@@ -0,0 +1,66 @@
+
+
+
+ {{ 'item.page.cc-license' | translate }}
+
+
+
diff --git a/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.scss b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.scss
new file mode 100644
index 00000000000..57e9f8102e0
--- /dev/null
+++ b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.scss
@@ -0,0 +1,113 @@
+@import '../../item-page.component';
+
+@media (min-width: 992px) {
+ .col-2-5 {
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+}
+
+.clarin-item-page-field {
+ .cc-license-content {
+ .cc-license-row {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .cc-license-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ text-decoration: none;
+ color: #007bff;
+ font-weight: 500;
+ transition: all 0.2s ease;
+
+ &:hover {
+ color: #0056b3;
+ text-decoration: none;
+ }
+
+ .cc-icons {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+
+ i {
+ font-size: 1.1rem;
+
+ &.fa-creative-commons {
+ color: #f39c12;
+ }
+
+ &.fa-creative-commons-by {
+ color: #3498db;
+ }
+
+ &.fa-creative-commons-sa {
+ color: #27ae60;
+ }
+
+ &.fa-creative-commons-nc {
+ color: #e74c3c;
+ }
+
+ &.fa-creative-commons-nd {
+ color: #9b59b6;
+ }
+
+ &.fa-creative-commons-zero {
+ color: #34495e;
+ }
+ }
+ }
+
+ .cc-license-text {
+ font-size: 0.95rem;
+ font-weight: 500;
+ }
+
+ &:hover .cc-icons i {
+ font-size: 1.2rem;
+ }
+ }
+
+ .cc-license-description {
+ font-size: 0.85rem;
+ color: #6c757d;
+ font-style: italic;
+ margin-left: 0;
+ }
+ }
+}
+
+// Responsive design
+@media (max-width: 991.98px) {
+ .clarin-item-page-field {
+ .cc-license-content {
+ .cc-license-link {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+
+ .cc-icons {
+ gap: 1px;
+
+ i {
+ font-size: 1rem;
+ }
+ }
+
+ .cc-license-text {
+ font-size: 0.9rem;
+ }
+ }
+
+ .cc-license-description {
+ font-size: 0.8rem;
+ margin-top: 2px;
+ }
+ }
+ }
+}
diff --git a/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.spec.ts b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.spec.ts
new file mode 100644
index 00000000000..a89dd992f0a
--- /dev/null
+++ b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.spec.ts
@@ -0,0 +1,384 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+import { TranslateModule } from '@ngx-translate/core';
+import { of } from 'rxjs';
+
+import { CreativeCommonsLicenseFieldComponent } from './creative-commons-license-field.component';
+import { BundleDataService } from '../../../../core/data/bundle-data.service';
+import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
+import { Item } from '../../../../core/shared/item.model';
+import { RemoteData } from '../../../../core/data/remote-data';
+import { Bundle } from '../../../../core/shared/bundle.model';
+import { PaginatedList, buildPaginatedList } from '../../../../core/data/paginated-list.model';
+import { Bitstream } from '../../../../core/shared/bitstream.model';
+import { createSuccessfulRemoteDataObject, createFailedRemoteDataObject } from '../../../../shared/remote-data.utils';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { PageInfo } from '../../../../core/shared/page-info.model';
+
+describe('CreativeCommonsLicenseFieldComponent', () => {
+ let component: CreativeCommonsLicenseFieldComponent;
+ let fixture: ComponentFixture;
+ let bundleDataService: jasmine.SpyObj;
+ let bitstreamDataService: jasmine.SpyObj;
+
+ const mockItem = {
+ uuid: 'test-item-uuid',
+ metadata: {
+ 'dc.rights.uri': [{ value: 'https://creativecommons.org/licenses/by/4.0/' }],
+ 'dc.rights': [{ value: 'Attribution 4.0 International' }]
+ },
+ allMetadata: jasmine.createSpy('allMetadata').and.callFake((field: string) => {
+ return mockItem.metadata[field] || [];
+ })
+ } as any as Item;
+
+ const mockItemWithoutLicense = {
+ uuid: 'test-item-uuid-no-license',
+ metadata: {
+ 'dc.title': [{ value: 'Test Item' }]
+ },
+ allMetadata: jasmine.createSpy('allMetadata').and.callFake((field: string) => {
+ return mockItemWithoutLicense.metadata[field] || [];
+ })
+ } as any as Item;
+
+ const mockBundle = {
+ uuid: 'test-bundle-uuid',
+ name: 'CC_LICENSE'
+ } as Bundle;
+
+ const mockBitstream = {
+ uuid: 'test-bitstream-uuid',
+ name: 'license.txt',
+ metadata: {
+ 'dc.identifier.uri': [{ value: 'https://creativecommons.org/licenses/by-nc-sa/3.0/' }]
+ }
+ } as any as Bitstream;
+
+ beforeEach(async () => {
+ const bundleDataServiceSpy = jasmine.createSpyObj('BundleDataService', ['findByItemAndName']);
+ const bitstreamDataServiceSpy = jasmine.createSpyObj('BitstreamDataService', ['findAllByItemAndBundleName']);
+
+ await TestBed.configureTestingModule({
+ declarations: [CreativeCommonsLicenseFieldComponent],
+ imports: [
+ TranslateModule.forRoot(),
+ NoopAnimationsModule
+ ],
+ providers: [
+ { provide: BundleDataService, useValue: bundleDataServiceSpy },
+ { provide: BitstreamDataService, useValue: bitstreamDataServiceSpy }
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(CreativeCommonsLicenseFieldComponent);
+ component = fixture.componentInstance;
+ bundleDataService = TestBed.inject(BundleDataService) as jasmine.SpyObj;
+ bitstreamDataService = TestBed.inject(BitstreamDataService) as jasmine.SpyObj;
+ });
+
+ describe('Component Initialization', () => {
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should initialize with item input', () => {
+ component.item = mockItem;
+ expect(component.item).toBe(mockItem);
+ });
+ });
+
+ describe('Creative Commons License Detection', () => {
+ beforeEach(() => {
+ component.item = mockItem;
+ });
+
+ it('should detect Creative Commons license from metadata', (done) => {
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.hasCcLicense$.subscribe(hasLicense => {
+ expect(hasLicense).toBe(true);
+ done();
+ });
+ });
+
+ it('should detect Creative Commons license from CC_LICENSE bundle', (done) => {
+ bundleDataService.findByItemAndName.and.returnValue(of(createSuccessfulRemoteDataObject(mockBundle)));
+ bitstreamDataService.findAllByItemAndBundleName.and.returnValue(
+ of(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), [mockBitstream])))
+ );
+
+ component.ngOnInit();
+
+ component.hasCcLicense$.subscribe(hasLicense => {
+ expect(hasLicense).toBe(true);
+ done();
+ });
+ });
+
+ it('should return false when no Creative Commons license is found', (done) => {
+ component.item = mockItemWithoutLicense;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.hasCcLicense$.subscribe(hasLicense => {
+ expect(hasLicense).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('License URL Extraction', () => {
+ beforeEach(() => {
+ component.item = mockItem;
+ });
+
+ it('should extract license URL from metadata', (done) => {
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.ccLicenseUrl$.subscribe(url => {
+ expect(url).toBe('https://creativecommons.org/licenses/by/4.0/');
+ done();
+ });
+ });
+
+ it('should extract license URL from bitstream metadata', (done) => {
+ bundleDataService.findByItemAndName.and.returnValue(of(createSuccessfulRemoteDataObject(mockBundle)));
+ bitstreamDataService.findAllByItemAndBundleName.and.returnValue(
+ of(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), [mockBitstream])))
+ );
+
+ component.ngOnInit();
+
+ component.ccLicenseUrl$.subscribe(url => {
+ expect(url).toBe('https://creativecommons.org/licenses/by-nc-sa/3.0/');
+ done();
+ });
+ });
+
+ it('should return empty string when no license URL is found', (done) => {
+ component.item = mockItemWithoutLicense;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.ccLicenseUrl$.subscribe(url => {
+ expect(url).toBe('');
+ done();
+ });
+ });
+ });
+
+ describe('License Name Extraction', () => {
+ beforeEach(() => {
+ component.item = mockItem;
+ });
+
+ it('should extract license name from URL', (done) => {
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.ccLicenseName$.subscribe(name => {
+ expect(name).toBe('CC BY 4.0');
+ done();
+ });
+ });
+
+ it('should return empty string when no license URL is available', (done) => {
+ component.item = mockItemWithoutLicense;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.ccLicenseName$.subscribe(name => {
+ expect(name).toBe('');
+ done();
+ });
+ });
+ });
+
+ describe('License Type Detection', () => {
+ const testCases = [
+ { input: 'CC BY 4.0', expected: 'by' },
+ { input: 'CC BY-SA 3.0', expected: 'by-sa' },
+ { input: 'CC BY-NC 2.0', expected: 'by-nc' },
+ { input: 'CC BY-NC-SA 4.0', expected: 'by-nc-sa' },
+ { input: 'CC BY-ND 3.0', expected: 'by-nd' },
+ { input: 'CC BY-NC-ND 2.0', expected: 'by-nc-nd' },
+ { input: 'CC0 1.0', expected: 'cc0' },
+ { input: 'Public Domain Mark', expected: 'cc0' },
+ { input: '', expected: '' },
+ { input: 'Unknown License', expected: '' }
+ ];
+
+ testCases.forEach(testCase => {
+ it(`should return '${testCase.expected}' for license '${testCase.input}'`, () => {
+ const result = component.getLicenseType(testCase.input);
+ expect(result).toBe(testCase.expected);
+ });
+ });
+ });
+
+ describe('License Name Extraction from URL', () => {
+ const urlTestCases = [
+ {
+ url: 'https://creativecommons.org/licenses/by/4.0/',
+ expected: 'CC BY 4.0'
+ },
+ {
+ url: 'https://creativecommons.org/licenses/by-sa/3.0/',
+ expected: 'CC BY-SA 3.0'
+ },
+ {
+ url: 'https://creativecommons.org/licenses/by-nc/2.0/',
+ expected: 'CC BY-NC 2.0'
+ },
+ {
+ url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
+ expected: 'CC BY-NC-SA 4.0'
+ },
+ {
+ url: 'https://creativecommons.org/licenses/by-nd/3.0/',
+ expected: 'CC BY-ND 3.0'
+ },
+ {
+ url: 'https://creativecommons.org/licenses/by-nc-nd/2.0/',
+ expected: 'CC BY-NC-ND 2.0'
+ },
+ {
+ url: 'https://creativecommons.org/publicdomain/zero/1.0/',
+ expected: 'CC0 1.0'
+ },
+ {
+ url: 'https://creativecommons.org/publicdomain/mark/1.0/',
+ expected: 'Public Domain 1.0'
+ },
+ {
+ url: 'https://example.com/not-cc-license',
+ expected: ''
+ },
+ {
+ url: '',
+ expected: ''
+ }
+ ];
+
+ urlTestCases.forEach(testCase => {
+ it(`should extract '${testCase.expected}' from URL '${testCase.url}'`, () => {
+ const result = component['extractLicenseNameFromUrl'](testCase.url);
+ expect(result).toBe(testCase.expected);
+ });
+ });
+ });
+
+ describe('Metadata Extraction', () => {
+ it('should extract Creative Commons URL from dc.rights.uri', () => {
+ component.item = mockItem;
+ const result = component['extractUrlFromMetadata']();
+ expect(result).toBe('https://creativecommons.org/licenses/by/4.0/');
+ });
+
+ it('should return empty string when no Creative Commons URL is found in metadata', () => {
+ component.item = mockItemWithoutLicense;
+ const result = component['extractUrlFromMetadata']();
+ expect(result).toBe('');
+ });
+
+ it('should check multiple metadata fields for Creative Commons URL', () => {
+ const itemWithDifferentField = {
+ uuid: 'test-item-uuid',
+ metadata: {
+ 'dc.rights': [{ value: 'https://creativecommons.org/licenses/by-sa/3.0/' }]
+ },
+ allMetadata: jasmine.createSpy('allMetadata').and.callFake((field: string) => {
+ return itemWithDifferentField.metadata[field] || [];
+ })
+ } as any as Item;
+
+ component.item = itemWithDifferentField;
+ const result = component['extractUrlFromMetadata']();
+ expect(result).toBe('https://creativecommons.org/licenses/by-sa/3.0/');
+ });
+ });
+
+ describe('Component Template Integration', () => {
+ it('should not display license field when no license is present', () => {
+ component.item = mockItemWithoutLicense;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const licenseElement = fixture.debugElement.query(By.css('.clarin-item-page-field'));
+ expect(licenseElement).toBeNull();
+ });
+
+ it('should display license field when Creative Commons license is present', () => {
+ component.item = mockItem;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ setTimeout(() => {
+ fixture.detectChanges();
+ const licenseElement = fixture.debugElement.query(By.css('.clarin-item-page-field'));
+ expect(licenseElement).toBeTruthy();
+ }, 100);
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should handle bundle service errors gracefully', (done) => {
+ component.item = mockItem;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.hasCcLicense$.subscribe(hasLicense => {
+ expect(hasLicense).toBe(true); // Should still detect from metadata
+ done();
+ });
+ });
+
+ it('should handle bitstream service errors gracefully', (done) => {
+ component.item = mockItem;
+ bundleDataService.findByItemAndName.and.returnValue(of(createSuccessfulRemoteDataObject(mockBundle)));
+ bitstreamDataService.findAllByItemAndBundleName.and.returnValue(of(createFailedRemoteDataObject>()));
+
+ component.ngOnInit();
+
+ component.ccLicenseUrl$.subscribe(url => {
+ expect(url).toBe('https://creativecommons.org/licenses/by/4.0/'); // Fallback to metadata
+ done();
+ });
+ });
+ });
+
+ describe('Debug Methods', () => {
+ it('should return metadata values for debug purposes', () => {
+ component.item = mockItem;
+ const result = component.getMetadataValues('dc.rights.uri');
+ expect(result).toBe('https://creativecommons.org/licenses/by/4.0/');
+ });
+
+ it('should return "No values found" when metadata field does not exist', () => {
+ component.item = mockItem;
+ const result = component.getMetadataValues('nonexistent.field');
+ expect(result).toBe('No values found');
+ });
+
+ it('should return "No metadata available" when item has no metadata', () => {
+ component.item = { metadata: null } as any as Item;
+ const result = component.getMetadataValues('dc.rights.uri');
+ expect(result).toBe('No metadata available');
+ });
+ });
+});
diff --git a/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.ts b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.ts
new file mode 100644
index 00000000000..fec3ac0ca1d
--- /dev/null
+++ b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.ts
@@ -0,0 +1,257 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { Observable, of } from 'rxjs';
+import { map, switchMap, catchError } from 'rxjs/operators';
+import { Item } from '../../../../core/shared/item.model';
+import { BundleDataService } from '../../../../core/data/bundle-data.service';
+import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
+import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
+import { RemoteData } from '../../../../core/data/remote-data';
+import { Bundle } from '../../../../core/shared/bundle.model';
+import { PaginatedList } from '../../../../core/data/paginated-list.model';
+import { Bitstream } from '../../../../core/shared/bitstream.model';
+import { isNotEmpty } from '../../../../shared/empty.util';
+
+/**
+ * Constants for Creative Commons license handling
+ */
+const CC_CONSTANTS = {
+ BUNDLE_NAME: 'CC_LICENSE',
+ DOMAIN: 'creativecommons.org',
+ METADATA_FIELDS: [
+ 'dc.rights.uri',
+ 'dc.rights',
+ 'dc.identifier.uri'
+ ],
+ LICENSE_TYPES: {
+ 'by/': 'CC BY',
+ 'by-sa/': 'CC BY-SA',
+ 'by-nc/': 'CC BY-NC',
+ 'by-nc-sa/': 'CC BY-NC-SA',
+ 'by-nd/': 'CC BY-ND',
+ 'by-nc-nd/': 'CC BY-NC-ND',
+ 'publicdomain/zero/': 'CC0',
+ 'publicdomain/mark/': 'Public Domain'
+ },
+ LICENSE_TYPE_PATTERNS: {
+ CC0: ['cc0', 'public domain'],
+ BY_NC_ND: ['by-nc-nd'],
+ BY_NC_SA: ['by-nc-sa'],
+ BY_NC: ['by-nc'],
+ BY_ND: ['by-nd'],
+ BY_SA: ['by-sa'],
+ BY: ['by']
+ },
+ ICON_CLASSES: {
+ BASE: 'fab fa-creative-commons',
+ BY: 'fab fa-creative-commons-by',
+ SA: 'fab fa-creative-commons-sa',
+ NC: 'fab fa-creative-commons-nc',
+ ND: 'fab fa-creative-commons-nd',
+ ZERO: 'fab fa-creative-commons-zero'
+ },
+ TEMPLATE_SWITCH_CASES: {
+ CC0: 'cc0',
+ BY_NC_ND: 'by-nc-nd',
+ BY_NC_SA: 'by-nc-sa',
+ BY_NC: 'by-nc',
+ BY_ND: 'by-nd',
+ BY_SA: 'by-sa',
+ BY: 'by'
+ },
+ DEFAULT_MESSAGES: {
+ NO_METADATA: 'No metadata available',
+ NO_VALUES: 'No values found',
+ DEFAULT_LICENSE: 'Creative Commons License'
+ }
+} as const;
+
+/**
+ * Component for displaying Creative Commons license information on item pages
+ */
+@Component({
+ selector: 'ds-creative-commons-license-field',
+ templateUrl: './creative-commons-license-field.component.html',
+ styleUrls: ['./creative-commons-license-field.component.scss']
+})
+export class CreativeCommonsLicenseFieldComponent implements OnInit {
+
+ /**
+ * The item to display Creative Commons license for
+ */
+ @Input() item: Item;
+
+ /**
+ * Creative Commons license URL
+ */
+ ccLicenseUrl$: Observable;
+
+ /**
+ * Creative Commons license name
+ */
+ ccLicenseName$: Observable;
+
+ /**
+ * Whether the item has a Creative Commons license
+ */
+ hasCcLicense$: Observable;
+
+ constructor(
+ private bundleService: BundleDataService,
+ private bitstreamService: BitstreamDataService
+ ) { }
+
+ ngOnInit(): void {
+ this.initializeCcLicense();
+ }
+
+ /**
+ * Initialize Creative Commons license information
+ */
+ private initializeCcLicense(): void {
+ // Cache the metadata URL extraction to avoid repeated calls
+ const metadataUrl = this.extractUrlFromMetadata();
+
+ // Check if item has CC_LICENSE bundle and extract license information
+ const ccLicenseBundle$ = this.bundleService.findByItemAndName(this.item, CC_CONSTANTS.BUNDLE_NAME);
+
+ this.hasCcLicense$ = ccLicenseBundle$.pipe(
+ map((bundleRD: RemoteData) => {
+ // Check if CC_LICENSE bundle exists OR if CC license metadata exists
+ const hasBundleLicense = bundleRD.hasSucceeded && isNotEmpty(bundleRD.payload);
+ const hasMetadataLicense = isNotEmpty(metadataUrl);
+ return hasBundleLicense || hasMetadataLicense;
+ }),
+ catchError(() => of(false))
+ );
+
+ // Get the license URL from bitstreams in CC_LICENSE bundle
+ this.ccLicenseUrl$ = ccLicenseBundle$.pipe(
+ switchMap((bundleRD: RemoteData) => {
+ if (bundleRD.hasSucceeded && isNotEmpty(bundleRD.payload)) {
+ return this.bitstreamService.findAllByItemAndBundleName(this.item, CC_CONSTANTS.BUNDLE_NAME);
+ }
+ return of(null);
+ }),
+ switchMap((bitstreamsRD) => {
+ if (bitstreamsRD && bitstreamsRD.hasSucceeded && isNotEmpty(bitstreamsRD.payload)) {
+ const bitstreams = bitstreamsRD.payload;
+ if (bitstreams.page.length > 0) {
+ // Look for license URL in bitstream metadata or name
+ const licenseBitstream = bitstreams.page.find(bitstream =>
+ bitstream.name.includes('license') ||
+ bitstream.metadata[CC_CONSTANTS.METADATA_FIELDS[0]]?.[0]?.value
+ );
+ const url = licenseBitstream?.metadata[CC_CONSTANTS.METADATA_FIELDS[0]]?.[0]?.value ||
+ metadataUrl ||
+ '';
+ return of(url);
+ }
+ }
+ // Fallback to cached metadata-based detection
+ return of(metadataUrl);
+ }),
+ catchError(() => of(''))
+ );
+
+ // Extract license name from URL or metadata
+ this.ccLicenseName$ = this.ccLicenseUrl$.pipe(
+ map((url: string) => {
+ if (isNotEmpty(url)) {
+ return this.extractLicenseNameFromUrl(url);
+ }
+ return '';
+ })
+ );
+ }
+
+ /**
+ * Extract Creative Commons license URL from item metadata
+ */
+ private extractUrlFromMetadata(): string {
+ // Check for common CC license metadata fields
+ for (const field of CC_CONSTANTS.METADATA_FIELDS) {
+ const values = this.item.allMetadata(field);
+ if (values) {
+ for (const value of values) {
+ if (value.value && value.value.includes(CC_CONSTANTS.DOMAIN)) {
+ return value.value;
+ }
+ }
+ }
+ }
+ return '';
+ }
+
+ /**
+ * Extract license name from Creative Commons URL
+ */
+ private extractLicenseNameFromUrl(url: string): string {
+ if (!url || !url.includes(CC_CONSTANTS.DOMAIN)) {
+ return '';
+ }
+
+ // Parse common CC license types from URL
+ for (const [pattern, name] of Object.entries(CC_CONSTANTS.LICENSE_TYPES)) {
+ if (url.includes(pattern)) {
+ // Extract version if present
+ const versionMatch = url.match(/(\d+\.\d+)/);
+ const version = versionMatch ? ` ${versionMatch[1]}` : '';
+ return `${name}${version}`;
+ }
+ }
+
+ return CC_CONSTANTS.DEFAULT_MESSAGES.DEFAULT_LICENSE;
+ }
+
+ /**
+ * Get Creative Commons icon class based on license type
+ */
+ getCcIconClass(licenseName: string): string {
+ const lowerName = licenseName.toLowerCase();
+
+ if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.CC0.some(pattern => lowerName.includes(pattern))) {
+ return CC_CONSTANTS.ICON_CLASSES.ZERO;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC_ND.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY} ${CC_CONSTANTS.ICON_CLASSES.NC} ${CC_CONSTANTS.ICON_CLASSES.ND}`;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC_SA.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY} ${CC_CONSTANTS.ICON_CLASSES.NC} ${CC_CONSTANTS.ICON_CLASSES.SA}`;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY} ${CC_CONSTANTS.ICON_CLASSES.NC}`;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_ND.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY} ${CC_CONSTANTS.ICON_CLASSES.ND}`;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_SA.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY} ${CC_CONSTANTS.ICON_CLASSES.SA}`;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY}`;
+ }
+
+ return CC_CONSTANTS.ICON_CLASSES.BASE;
+ }
+
+ /**
+ * Get license type for switch case in template
+ */
+ getLicenseType(licenseName: string): string {
+ if (!licenseName) return '';
+
+ const name = licenseName.toLowerCase();
+
+ if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.CC0.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.CC0;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC_ND.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY_NC_ND;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC_SA.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY_NC_SA;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY_NC;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_ND.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY_ND;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_SA.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY_SA;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY;
+ } else {
+ return '';
+ }
+ }
+}
diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts
index ddb3be855d7..410926c9530 100644
--- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts
+++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts
@@ -1,11 +1,11 @@
import { ChangeDetectorRef, Component, Inject, OnChanges, SimpleChanges, OnInit } from '@angular/core';
-import { Observable, of as observableOf, Subscription, tap } from 'rxjs';
+import { Observable, of as observableOf, Subscription, tap, Subject } from 'rxjs';
import { Field, Option, SubmissionCcLicence } from '../../../core/submission/models/submission-cc-license.model';
import {
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload,
getRemoteDataPayload
} from '../../../core/shared/operators';
-import { distinctUntilChanged, filter, map, take } from 'rxjs/operators';
+import { distinctUntilChanged, filter, map, take, debounceTime, switchMap, startWith, shareReplay } from 'rxjs/operators';
import { SubmissionCcLicenseDataService } from '../../../core/submission/submission-cc-license-data.service';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { renderSectionFor } from '../sections-decorator';
@@ -89,6 +89,11 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
*/
private _isLastPage: boolean;
+ /**
+ * Subject to trigger CC license link updates with debouncing
+ */
+ private ccLicenseLinkTrigger$ = new Subject();
+
/**
* The Creative Commons link saved in the workspace item.
*/
@@ -129,14 +134,20 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
ngOnInit(): void {
super.ngOnInit();
- if (hasNoValue(this.ccLicenseLink$)) {
- this.ccLicenseLink$ = this.getCcLicenseLink$();
- }
+ // Initialize the debounced license link observable
+ this.ccLicenseLink$ = this.ccLicenseLinkTrigger$.pipe(
+ startWith(undefined), // Start with initial trigger
+ debounceTime(300), // Debounce rapid clicks
+ switchMap(() => this.getCcLicenseLink$() || observableOf(null)),
+ shareReplay(1), // Cache the latest result
+ distinctUntilChanged()
+ );
}
ngOnChanges(changes: SimpleChanges): void {
if (hasValue(changes.sectionData) || hasValue(changes.submissionCcLicenses)) {
- this.ccLicenseLink$ = this.getCcLicenseLink$();
+ // Trigger the debounced license link update
+ this.ccLicenseLinkTrigger$.next();
}
}
@@ -164,7 +175,8 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
},
uri: undefined,
});
- this.ccLicenseLink$ = this.getCcLicenseLink$();
+ // Trigger the debounced license link update
+ this.ccLicenseLinkTrigger$.next();
}
/**
@@ -196,7 +208,8 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
},
accepted: false,
});
- this.ccLicenseLink$ = this.getCcLicenseLink$();
+ // Trigger the debounced license link update
+ this.ccLicenseLinkTrigger$.next();
}
/**
@@ -272,6 +285,8 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
*/
onSectionDestroy(): void {
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
+ // Complete the subject to prevent memory leaks
+ this.ccLicenseLinkTrigger$.complete();
}
/**
@@ -284,18 +299,35 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
filter((sectionState) => {
return isNotEmpty(sectionState) && (isNotEmpty(sectionState.data) || isNotEmpty(sectionState.errorsToShow));
}),
- distinctUntilChanged(),
+ distinctUntilChanged((prev, curr) => {
+ // More precise comparison to prevent unnecessary updates
+ const prevData = prev?.data as WorkspaceitemSectionCcLicenseObject;
+ const currData = curr?.data as WorkspaceitemSectionCcLicenseObject;
+ return prevData?.accepted === currData?.accepted &&
+ prevData?.uri === currData?.uri &&
+ JSON.stringify(prevData?.ccLicense) === JSON.stringify(currData?.ccLicense);
+ }),
map((sectionState) => sectionState.data as WorkspaceitemSectionCcLicenseObject),
).subscribe((data) => {
- if (this.data.accepted !== data.accepted) {
+ const wasAccepted = this.data.accepted;
+ const wasUri = this.data.uri;
+
+ // Only process if acceptance state actually changed
+ if (wasAccepted !== data.accepted && data.accepted !== undefined) {
const path = this.pathCombiner.getPath('uri');
- if (data.accepted) {
- this.getCcLicenseLink$().pipe(
- take(1),
- ).subscribe((link) => {
- this.operationsBuilder.add(path, link.toString(), false, true);
- });
- } else if (!!this.data.uri) {
+ if (data.accepted && !wasAccepted) {
+ // Only add URI if we're switching from not accepted to accepted
+ const licenseLink$ = this.getCcLicenseLink$();
+ if (licenseLink$) {
+ licenseLink$.pipe(
+ take(1),
+ filter(link => !!link && link !== wasUri) // Only proceed if link exists and is different
+ ).subscribe((link) => {
+ this.operationsBuilder.add(path, link.toString(), false, true);
+ });
+ }
+ } else if (!data.accepted && wasAccepted && !!this.data.uri) {
+ // Only remove URI if we're switching from accepted to not accepted
this.operationsBuilder.remove(path);
}
}
@@ -305,12 +337,12 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
getFirstCompletedRemoteData(),
getRemoteDataPayload()
).subscribe((remoteData) => {
- if (remoteData === undefined || remoteData.values.length === 0) {
- // No value configured, use blank value (International jurisdiction)
- this.defaultJurisdiction = '';
- } else {
- this.defaultJurisdiction = remoteData.values[0];
- }
+ if (remoteData === undefined || remoteData.values.length === 0) {
+ // No value configured, use blank value (International jurisdiction)
+ this.defaultJurisdiction = '';
+ } else {
+ this.defaultJurisdiction = remoteData.values[0];
+ }
})
);
this.loadCcLicences();
diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5
index 6e2e6d8c281..7b2e9275f8a 100644
--- a/src/assets/i18n/cs.json5
+++ b/src/assets/i18n/cs.json5
@@ -3812,6 +3812,12 @@
// "item.page.journal.search.title": "Articles in this journal",
"item.page.journal.search.title": "Články v tomto časopise",
+ // "item.page.cc-license": "License",
+ "item.page.cc-license": "Licence",
+
+ // "item.page.cc-license.description": "This work is licensed under a Creative Commons license.",
+ "item.page.cc-license.description": "Tato práce je licencována podle licence Creative Commons.",
+
// "item.page.link.full": "Show full item record",
"item.page.link.full": "Zobrazit celý záznam",
@@ -8513,7 +8519,7 @@
// "handle-table.table.handle": "Handle",
"handle-table.table.handle": "Handle",
- // "handle-table.table.internal": "Is Internal",
+ // "handle-table.table.internal": "Is internal",
"handle-table.table.internal": "Je interní",
// "handle-table.table.is-internal": "Yes",
@@ -8546,6 +8552,42 @@
// "handle-table.dropdown.search-button": "Search",
"handle-table.dropdown.search-button": "Hledat",
+ // "handle-table.search.placeholder.no-option": "Please select a search option first",
+ "handle-table.search.placeholder.no-option": "Nejprve vyberte možnost vyhledávání",
+
+ // "handle-table.search.placeholder.handle": "Enter handle",
+ "handle-table.search.placeholder.handle": "Zadejte Handle",
+
+ // "handle-table.search.internal.select": "Select internal option",
+ "handle-table.search.internal.select": "Vyberte možnost interní",
+
+ // "handle-table.search.internal.yes": "Yes",
+ "handle-table.search.internal.yes": "Ano",
+
+ // "handle-table.search.internal.no": "No",
+ "handle-table.search.internal.no": "Ne",
+
+ // "handle-table.search.resource-type.select": "Select resource type",
+ "handle-table.search.resource-type.select": "Vyberte typ zdroje",
+
+ // "handle-table.search.resource-type.site": "Site",
+ "handle-table.search.resource-type.site": "Stránka",
+
+ // "handle-table.search.resource-type.community": "Community",
+ "handle-table.search.resource-type.community": "Komunita",
+
+ // "handle-table.search.resource-type.collection": "Collection",
+ "handle-table.search.resource-type.collection": "Kolekce",
+
+ // "handle-table.search.resource-type.item": "Item",
+ "handle-table.search.resource-type.item": "Záznam",
+
+ // "handle-table.search.aria.select-internal": "Select internal option",
+ "handle-table.search.aria.select-internal": "Vyberte možnost interní",
+
+ // "handle-table.search.aria.select-resource-type": "Select resource type",
+ "handle-table.search.aria.select-resource-type": "Vyberte typ zdroje",
+
// "handle-table.global-actions.title": "Global Actions",
"handle-table.global-actions.title": "Globální akce",
diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5
index 4a26129445c..bd6a37ddd45 100644
--- a/src/assets/i18n/en.json5
+++ b/src/assets/i18n/en.json5
@@ -2541,6 +2541,10 @@
"item.page.journal.search.title": "Articles in this journal",
+ "item.page.cc-license": "License",
+
+ "item.page.cc-license.description": "This work is licensed under a Creative Commons license.",
+
"item.page.link.full": "Show full item record",
"item.page.link.simple": "Show simple item record",
@@ -5675,7 +5679,7 @@
"handle-table.table.handle": "Handle",
- "handle-table.table.internal": "Is Internal",
+ "handle-table.table.internal": "Is internal",
"handle-table.table.is-internal": "Yes",
@@ -5697,6 +5701,30 @@
"handle-table.dropdown.search-button": "Search",
+ "handle-table.search.placeholder.no-option": "Please select a search option first",
+
+ "handle-table.search.placeholder.handle": "Enter handle",
+
+ "handle-table.search.internal.select": "Select internal option",
+
+ "handle-table.search.internal.yes": "Yes",
+
+ "handle-table.search.internal.no": "No",
+
+ "handle-table.search.resource-type.select": "Select resource type",
+
+ "handle-table.search.resource-type.site": "Site",
+
+ "handle-table.search.resource-type.community": "Community",
+
+ "handle-table.search.resource-type.collection": "Collection",
+
+ "handle-table.search.resource-type.item": "Item",
+
+ "handle-table.search.aria.select-internal": "Select internal option",
+
+ "handle-table.search.aria.select-resource-type": "Select resource type",
+
"handle-table.global-actions.title": "Global Actions",
"handle-table.global-actions.actions-list-message": "This is the list of available global actions.",
|