diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index fe2837c6e3..8430454d63 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -132,3 +132,13 @@ export const SUBSCRIPTIONS_MODULE_PATH = 'subscriptions'; export function getSubscriptionsModuleRoute() { return `/${SUBSCRIPTIONS_MODULE_PATH}`; } + +export const EDIT_ITEM_PATH = 'edit-items'; +export function getEditItemPageRoute() { + return `/${EDIT_ITEM_PATH}`; +} +export const CORRECTION_TYPE_PATH = 'corrections'; +export function getCorrectionTypePageRoute(itemUuid: string, typeId: string) { + return `/items/${itemUuid}/${CORRECTION_TYPE_PATH}/${typeId}`; +} + diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index e176af7d55..0eeb476d69 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -185,6 +185,8 @@ import { FlatBrowseDefinition } from './shared/flat-browse-definition.model'; import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model'; import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition'; import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; +import { CorrectionTypeMode } from './submission/models/correction-type-mode.model'; +import { CorrectionTypeDataService } from './submission/correctiontype-data.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -308,6 +310,7 @@ const PROVIDERS = [ OrcidQueueDataService, OrcidHistoryDataService, SupervisionOrderDataService + CorrectionTypeDataService, ]; /** @@ -387,6 +390,7 @@ export const models = Subscription, ItemRequest, BulkAccessConditionOptions + CorrectionTypeMode ]; @NgModule({ diff --git a/src/app/core/submission/correctiontype-data.service.ts b/src/app/core/submission/correctiontype-data.service.ts new file mode 100644 index 0000000000..4d293558f7 --- /dev/null +++ b/src/app/core/submission/correctiontype-data.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@angular/core'; + +import { dataService } from '../data/base/data-service.decorator'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { RequestService } from '../data/request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { IdentifiableDataService } from '../data/base/identifiable-data.service'; +import { SearchDataImpl } from '../data/base/search-data'; +import { CorrectionTypeMode } from './models/correction-type-mode.model'; +import { Observable, map } from 'rxjs'; +import { RemoteData } from '../data/remote-data'; +import { PaginatedList } from '../data/paginated-list.model'; +import { FindListOptions } from '../data/find-list-options.model'; +import { RequestParam } from '../cache/models/request-param.model'; +import { getAllSucceededRemoteDataPayload, getPaginatedListPayload } from '../shared/operators'; + +/** + * A service that provides methods to make REST requests with correctiontypes endpoint. + */ +@Injectable() +@dataService(CorrectionTypeMode.type) +export class CorrectionTypeDataService extends IdentifiableDataService { + protected linkPath = 'correctiontypes'; + protected searchByTopic = 'findByTopic'; + protected searchFindByItem = 'findByItem'; + private searchData: SearchDataImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + ) { + super('correctiontypes', requestService, rdbService, objectCache, halService); + + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + } + + /** + * Get the correction type by id + * @param id the id of the correction type + * @param useCachedVersionIfAvailable use the cached version if available + * @param reRequestOnStale re-request on stale + * @returns {Observable>} the correction type + */ + getCorrectionTypeById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable> { + return this.findById(id, useCachedVersionIfAvailable, reRequestOnStale); + } + + /** + * Search for the correction types for the item + * @param itemUuid the uuid of the item + * @param useCachedVersionIfAvailable use the cached version if available + * @returns the list of correction types for the item + */ + findByItem(itemUuid: string, useCachedVersionIfAvailable): Observable>> { + const options = new FindListOptions(); + options.searchParams = [new RequestParam('uuid', itemUuid)]; + return this.searchData.searchBy(this.searchFindByItem, options, useCachedVersionIfAvailable); + } + + /** + * Find the correction type for the topic + * @param topic the topic of the correction type to search for + * @param useCachedVersionIfAvailable use the cached version if available + * @param reRequestOnStale re-request on stale + * @returns the correction type for the topic + */ + findByTopic(topic: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable { + const options = new FindListOptions(); + options.searchParams = [ + { + fieldName: 'topic', + fieldValue: topic, + }, + ]; + + return this.searchData.searchBy(this.searchByTopic, options, useCachedVersionIfAvailable, reRequestOnStale).pipe( + getAllSucceededRemoteDataPayload(), + getPaginatedListPayload(), + map((list: CorrectionTypeMode[]) => { + return list[0]; + }) + ); + } +} diff --git a/src/app/core/submission/models/correction-type-mode.model.ts b/src/app/core/submission/models/correction-type-mode.model.ts new file mode 100644 index 0000000000..b9bb033d86 --- /dev/null +++ b/src/app/core/submission/models/correction-type-mode.model.ts @@ -0,0 +1,35 @@ +import { autoserialize, deserialize } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { CacheableObject } from '../../cache/cacheable-object.model'; +import { ResourceType } from '../../shared/resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { HALLink } from '../../shared/hal-link.model'; + +@typedObject +export class CorrectionTypeMode extends CacheableObject { + static type = new ResourceType('correctiontype'); + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + @autoserialize + id: string; + + @autoserialize + topic: string; + + @autoserialize + discoveryConfiguration: string; + + @autoserialize + creationForm: string; + + @deserialize + _links: { + self: HALLink; + }; +} diff --git a/src/app/shared/context-menu/correction-type-menu/correction-type-menu.component.html b/src/app/shared/context-menu/correction-type-menu/correction-type-menu.component.html new file mode 100644 index 0000000000..6504d6ecaf --- /dev/null +++ b/src/app/shared/context-menu/correction-type-menu/correction-type-menu.component.html @@ -0,0 +1,7 @@ + + + diff --git a/src/app/shared/context-menu/correction-type-menu/correction-type-menu.component.spec.ts b/src/app/shared/context-menu/correction-type-menu/correction-type-menu.component.spec.ts new file mode 100644 index 0000000000..0f2eb02f60 --- /dev/null +++ b/src/app/shared/context-menu/correction-type-menu/correction-type-menu.component.spec.ts @@ -0,0 +1,116 @@ +import { Item } from './../../../core/shared/item.model'; +import { DSpaceObject } from './../../../core/shared/dspace-object.model'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CorrectionTypeMenuComponent } from './correction-type-menu.component'; +import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; +import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { createPaginatedList } from '../../testing/utils.test'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../mocks/translate-loader.mock'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CorrectionTypeDataService } from '../../../core/submission/correctiontype-data.service'; +import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { By } from '@angular/platform-browser'; +import { CorrectionTypeMode } from '../../../core/submission/models/correction-type-mode.model'; + +describe('CorrectionTypeMenuComponent', () => { + let component: CorrectionTypeMenuComponent; + let fixture: ComponentFixture; + let componentAsAny: any; + + let correctionTypeService: any; + let dso: DSpaceObject; + const notificationService = new NotificationsServiceStub(); + const correctionType: CorrectionTypeMode = Object.assign(new CorrectionTypeMode(), { + id: 'addpersonalpath', + creationForm:'manageRelation', + discoveryConfiguration: 'RELATION.PersonPath.Items', + topic: '/DSPACEUSERS/RELATIONADD/PERSONALPATH' + }); + + const correctionTypeObjRDList$ = createSuccessfulRemoteDataObject$(createPaginatedList([correctionType])); + + beforeEach(async () => { + dso = Object.assign(new Item(), { + id: 'test-item', + _links: { + self: { href: 'test-item-selflink' } + } + }); + + correctionTypeService = jasmine.createSpyObj('CorrectionTypeDataService', { + findByItem: jasmine.createSpy('findByItem') + }); + + await TestBed.configureTestingModule({ + declarations: [ CorrectionTypeMenuComponent ], + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule.withRoutes([])], + providers: [ + { provide: CorrectionTypeDataService, useValue: correctionTypeService }, + { provide: 'contextMenuObjectProvider', useValue: dso }, + { provide: 'contextMenuObjectTypeProvider', useValue: DSpaceObjectType.ITEM }, + { provide: NotificationsService, useValue: notificationService } + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CorrectionTypeMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('when correction types are available', () => { + beforeEach(() => { + correctionTypeService.findByItem.and.returnValue(correctionTypeObjRDList$); + fixture = TestBed.createComponent(CorrectionTypeMenuComponent); + component = fixture.componentInstance; + componentAsAny = fixture.componentInstance; + component.contextMenuObject = dso; + fixture.detectChanges(); + }); + + it('should init properly', () => { + expect(componentAsAny.correctionTypes$.value).toEqual([correctionType]); + }); + + it('should render a button', () => { + const link = fixture.debugElement.query(By.css('button')); + expect(link).not.toBeNull(); + }); + }); + + describe('when is no data are available', () => { + beforeEach(() => { + correctionTypeService.findByItem.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([]))); + fixture = TestBed.createComponent(CorrectionTypeMenuComponent); + component = fixture.componentInstance; + componentAsAny = fixture.componentInstance; + component.contextMenuObject = dso; + fixture.detectChanges(); + }); + + it('should init edit mode properly', () => { + expect(componentAsAny.correctionTypes$.value).toEqual([]); + }); + + it('should render a button', () => { + const link = fixture.debugElement.query(By.css('button')); + expect(link).toBeNull(); + }); + }); +}); diff --git a/src/app/shared/context-menu/correction-type-menu/correction-type-menu.component.ts b/src/app/shared/context-menu/correction-type-menu/correction-type-menu.component.ts new file mode 100644 index 0000000000..2f1b341b9f --- /dev/null +++ b/src/app/shared/context-menu/correction-type-menu/correction-type-menu.component.ts @@ -0,0 +1,105 @@ +import { getAllSucceededRemoteDataPayload, getPaginatedListPayload } from './../../../core/shared/operators'; +import { CorrectionTypeDataService } from './../../../core/submission/correctiontype-data.service'; +import { DSpaceObject } from './../../../core/shared/dspace-object.model'; +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { ContextMenuEntryComponent } from '../context-menu-entry.component'; +import { ContextMenuEntryType } from '../context-menu-entry-type'; +import { BehaviorSubject, Observable, Subscription, map, startWith, tap } from 'rxjs'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { CorrectionTypeMode } from '../../../core/submission/models/correction-type-mode.model'; +import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { hasValue, isNotEmpty } from '../../empty.util'; +import { rendersContextMenuEntriesForType } from '../context-menu.decorator'; +import { getCorrectionTypePageRoute } from '../../../app-routing-paths'; + +@Component({ + selector: 'ds-correction-type-menu', + templateUrl: './correction-type-menu.component.html', +}) +@rendersContextMenuEntriesForType(DSpaceObjectType.ITEM) +export class CorrectionTypeMenuComponent extends ContextMenuEntryComponent implements OnInit, OnDestroy { + + /** + * The menu entry type + */ + public static menuEntryType: ContextMenuEntryType = ContextMenuEntryType.CorrectionType; + + /** + * A boolean representing if a request operation is pending + * @type {BehaviorSubject} + */ + public processing$ = new BehaviorSubject(false); + + /** + * Reference to NgbModal + */ + public modalRef: NgbModalRef; + + /** + * List of Edit Modes available on this item + * for the current user + */ + private correctionTypes$: BehaviorSubject = new BehaviorSubject([]); + + /** + * Variable to track subscription and unsubscribe it onDestroy + */ + private sub: Subscription; + + constructor( + @Inject('contextMenuObjectProvider') protected injectedContextMenuObject: DSpaceObject, + @Inject('contextMenuObjectTypeProvider') protected injectedContextMenuObjectType: DSpaceObjectType, + private correctionTypeService: CorrectionTypeDataService, + public notificationService: NotificationsService + ) { + super(injectedContextMenuObject, injectedContextMenuObjectType, ContextMenuEntryType.EditSubmission); + } + + ngOnInit(): void { + this.notificationService.claimedProfile.subscribe(() => { + this.getData(); + }); + } + + /** + * Check if edit mode is available + */ + getCorrectionTypes(): Observable { + return this.correctionTypes$; + } + + /** + * Check if edit mode is available + */ + isAvailable(): Observable { + return this.correctionTypes$.asObservable().pipe( + map((type) => isNotEmpty(type) && type.length > 0) + ); + } + + getData(): void { + this.sub = this.correctionTypeService.findByItem(this.contextMenuObject.id, true).pipe( + tap((types) => console.log(types)), + getAllSucceededRemoteDataPayload(), + getPaginatedListPayload(), + startWith([]) + ).subscribe((types: CorrectionTypeMode[]) => { + console.log(types); + this.correctionTypes$.next(types); + }); + } + + getTypeRoute(id: string) { + return getCorrectionTypePageRoute(this.contextMenuObject.id, id); + } + + /** + * Make sure the subscription is unsubscribed from when this component is destroyed + */ + ngOnDestroy(): void { + if (hasValue(this.sub)) { + this.sub.unsubscribe(); + } + } +}