From 2ffb72320221b923d0064fec6fc47a04d34606ac Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 10 Mar 2022 13:04:22 +0530 Subject: [PATCH 01/87] [CST-5329] Add validate only check in the Import > Metadata page --- .../metadata-import-page.component.html | 4 +++ .../metadata-import-page.component.spec.ts | 25 ++++++++++++++++++- .../metadata-import-page.component.ts | 8 ++++++ src/assets/i18n/en.json5 | 2 ++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html index 42a04b0de6..c70bc45947 100644 --- a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html +++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html @@ -1,6 +1,10 @@

{{'admin.metadata-import.page.help' | translate}}

+

+ + {{'admin.metadata-import.page.validateOnly' | translate}} +

{ comp.setFile(fileMock); }); - describe('if proceed button is pressed', () => { + describe('if proceed button is pressed without validate only', () => { beforeEach(fakeAsync(() => { + comp.validateOnly = false; const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; proceed.click(); fixture.detectChanges(); @@ -107,6 +108,28 @@ describe('MetadataImportPageComponent', () => { }); }); + describe('if proceed button is pressed with validate only', () => { + beforeEach(fakeAsync(() => { + comp.validateOnly = true; + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('metadata-import script is invoked with -f fileName and the mockFile and -v validate-only', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }), + Object.assign(new ProcessParameter(), { name: '-v', value: true }), + ]; + expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); + }); + it('success notification is shown', () => { + expect(notificationService.success).toHaveBeenCalled(); + }); + it('redirected to process page', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45'); + }); + }); + describe('if proceed is pressed; but script invoke fails', () => { beforeEach(fakeAsync(() => { jasmine.getEnv().allowRespy(true); diff --git a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts index 3bdcca3084..deb16c0d73 100644 --- a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts +++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts @@ -30,6 +30,11 @@ export class MetadataImportPageComponent { */ fileObject: File; + /** + * The validate only flag + */ + validateOnly = true; + public constructor(private location: Location, protected translate: TranslateService, protected notificationsService: NotificationsService, @@ -62,6 +67,9 @@ export class MetadataImportPageComponent { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }), ]; + if (this.validateOnly) { + parameterValues.push(Object.assign(new ProcessParameter(), { name: '-v', value: true })); + } this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe( getFirstCompletedRemoteData(), diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index f33a195cfe..426fcb12d2 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -538,6 +538,8 @@ "admin.metadata-import.page.error.addFile": "Select file first!", + "admin.metadata-import.page.validateOnly": "Validate Only", + From 027b281d7a61031370a62f1388d23b8369410808 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 10 Mar 2022 14:45:53 +0530 Subject: [PATCH 02/87] [CST-5329] Add validate only check in the Import > Metadata page --- .../metadata-import-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html index c70bc45947..fb96c4becd 100644 --- a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html +++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html @@ -3,7 +3,7 @@

{{'admin.metadata-import.page.help' | translate}}

- {{'admin.metadata-import.page.validateOnly' | translate}} + {{'admin.metadata-import.page.validateOnly' | translate}}

Date: Mon, 18 Apr 2022 13:18:40 +0530 Subject: [PATCH 03/87] [CST-5676] Bitstream edit page is broken if no policies are set --- .../edit-bitstream-page.component.ts | 17 +++++++++++++++++ .../resource-policies.component.html | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 9a59df4b95..2042efbd52 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -387,6 +387,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ protected subs: Subscription[] = []; + /** + * Set to blank to detect changes in format. + */ + bitstreamFormat = {}; + + /** + * Set to true to detect changes in bundle. + */ + bitstreamBundle = {}; + constructor(private route: ActivatedRoute, private router: Router, @@ -685,6 +695,13 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const regexExcludeBundles = /OTHERCONTENT|THUMBNAIL|LICENSE/; const regexIIIFItem = /true|yes/i; + this.bitstream.format.subscribe(res => { + this.bitstreamFormat = res; + }); + this.bitstream.bundle.subscribe(res => { + this.bitstreamBundle = res; + }); + const isImage$ = this.bitstream.format.pipe( getFirstSucceededRemoteData(), map((format: RemoteData) => format.payload.mimetype.includes('image/'))); diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html index b06946ad25..4667e22217 100644 --- a/src/app/shared/resource-policies/resource-policies.component.html +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -1,4 +1,4 @@ -
+
@@ -29,7 +29,7 @@ - + - +
{{'resource-policies.table.headers.edit' | translate}}
From 4fe82112d6649e0e725be4b514683ced71a1b595 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Mon, 18 Apr 2022 13:39:36 +0530 Subject: [PATCH 04/87] [CST-5676] Bitstream edit page is broken if no policies are set --- .../edit-bitstream-page.component.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 2042efbd52..a2b8f7c9d3 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -706,11 +706,20 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { getFirstSucceededRemoteData(), map((format: RemoteData) => format.payload.mimetype.includes('image/'))); + let isImageBitstream = false; + isImage$.subscribe(res => { + isImageBitstream = res; + }); + const isIIIFBundle$ = this.bitstream.bundle.pipe( getFirstSucceededRemoteData(), map((bundle: RemoteData) => this.dsoNameService.getName(bundle.payload).match(regexExcludeBundles) == null)); + let isIIIFBundleBitstream = false; + isIIIFBundle$.subscribe(res => { + isIIIFBundleBitstream = res; + }); const isEnabled$ = this.bitstream.bundle.pipe( getFirstSucceededRemoteData(), map((bundle: RemoteData) => bundle.payload.item.pipe( @@ -720,6 +729,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { item.payload.firstMetadataValue('dspace.iiif.enabled').match(regexIIIFItem) !== null) )))); + let isEnabledBitstream: Observable; + isEnabled$.subscribe(res => { + isEnabledBitstream = res; + }); + const iiifSub = combineLatest( isImage$, isIIIFBundle$, From 6480b75aed6df0aef1ef02773af0299c38b91e61 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Mon, 18 Apr 2022 16:27:36 +0530 Subject: [PATCH 05/87] [CST-5676] Bitstream edit page is broken if no policies are set --- .../edit-bitstream-page.component.ts | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index a2b8f7c9d3..9a59df4b95 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -387,16 +387,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ protected subs: Subscription[] = []; - /** - * Set to blank to detect changes in format. - */ - bitstreamFormat = {}; - - /** - * Set to true to detect changes in bundle. - */ - bitstreamBundle = {}; - constructor(private route: ActivatedRoute, private router: Router, @@ -695,31 +685,15 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const regexExcludeBundles = /OTHERCONTENT|THUMBNAIL|LICENSE/; const regexIIIFItem = /true|yes/i; - this.bitstream.format.subscribe(res => { - this.bitstreamFormat = res; - }); - this.bitstream.bundle.subscribe(res => { - this.bitstreamBundle = res; - }); - const isImage$ = this.bitstream.format.pipe( getFirstSucceededRemoteData(), map((format: RemoteData) => format.payload.mimetype.includes('image/'))); - let isImageBitstream = false; - isImage$.subscribe(res => { - isImageBitstream = res; - }); - const isIIIFBundle$ = this.bitstream.bundle.pipe( getFirstSucceededRemoteData(), map((bundle: RemoteData) => this.dsoNameService.getName(bundle.payload).match(regexExcludeBundles) == null)); - let isIIIFBundleBitstream = false; - isIIIFBundle$.subscribe(res => { - isIIIFBundleBitstream = res; - }); const isEnabled$ = this.bitstream.bundle.pipe( getFirstSucceededRemoteData(), map((bundle: RemoteData) => bundle.payload.item.pipe( @@ -729,11 +703,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { item.payload.firstMetadataValue('dspace.iiif.enabled').match(regexIIIFItem) !== null) )))); - let isEnabledBitstream: Observable; - isEnabled$.subscribe(res => { - isEnabledBitstream = res; - }); - const iiifSub = combineLatest( isImage$, isIIIFBundle$, From 01f3cbcaea3685d4cd50d167adaedf13219b26c2 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Wed, 20 Apr 2022 15:44:01 +0530 Subject: [PATCH 06/87] [CST-5676] Breadcrumbs changes --- .../bitstream-page-routing.module.ts | 23 ++++++++++----- .../bitstream-page/bitstream-page.resolver.ts | 13 +++++++++ .../bitstream-breadcrumb.resolver.ts | 28 +++++++++++++++++++ src/app/core/shared/bitstream.model.ts | 8 ++++-- 4 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts diff --git a/src/app/bitstream-page/bitstream-page-routing.module.ts b/src/app/bitstream-page/bitstream-page-routing.module.ts index 27b9db9a05..5da9135846 100644 --- a/src/app/bitstream-page/bitstream-page-routing.module.ts +++ b/src/app/bitstream-page/bitstream-page-routing.module.ts @@ -10,6 +10,7 @@ import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/re import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component'; import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component'; import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; +import { BitstreamBreadcrumbResolver } from '../core/breadcrumbs/bitstream-breadcrumb.resolver'; const EDIT_BITSTREAM_PATH = ':id/edit'; const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; @@ -25,7 +26,8 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; path: 'handle/:prefix/:suffix/:filename', component: BitstreamDownloadPageComponent, resolve: { - bitstream: LegacyBitstreamUrlResolver + bitstream: LegacyBitstreamUrlResolver, + breadcrumb: BitstreamBreadcrumbResolver }, }, { @@ -33,7 +35,8 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; path: ':prefix/:suffix/:sequence_id/:filename', component: BitstreamDownloadPageComponent, resolve: { - bitstream: LegacyBitstreamUrlResolver + bitstream: LegacyBitstreamUrlResolver, + breadcrumb: BitstreamBreadcrumbResolver }, }, { @@ -41,14 +44,16 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; path: ':id/download', component: BitstreamDownloadPageComponent, resolve: { - bitstream: BitstreamPageResolver + bitstream: BitstreamPageResolver, + breadcrumb: BitstreamBreadcrumbResolver }, }, { path: EDIT_BITSTREAM_PATH, component: EditBitstreamPageComponent, resolve: { - bitstream: BitstreamPageResolver + bitstream: BitstreamPageResolver, + breadcrumb: BitstreamBreadcrumbResolver }, canActivate: [AuthenticatedGuard] }, @@ -59,7 +64,8 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; { path: 'create', resolve: { - resourcePolicyTarget: ResourcePolicyTargetResolver + resourcePolicyTarget: ResourcePolicyTargetResolver, + breadcrumb: BitstreamBreadcrumbResolver }, component: ResourcePolicyCreateComponent, data: { title: 'resource-policies.create.page.title', showBreadcrumbs: true } @@ -67,7 +73,8 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; { path: 'edit', resolve: { - resourcePolicy: ResourcePolicyResolver + resourcePolicy: ResourcePolicyResolver, + breadcrumb: BitstreamBreadcrumbResolver }, component: ResourcePolicyEditComponent, data: { title: 'resource-policies.edit.page.title', showBreadcrumbs: true } @@ -75,7 +82,8 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; { path: '', resolve: { - bitstream: BitstreamPageResolver + bitstream: BitstreamPageResolver, + breadcrumb: BitstreamBreadcrumbResolver }, component: BitstreamAuthorizationsComponent, data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true } @@ -86,6 +94,7 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; ], providers: [ BitstreamPageResolver, + BitstreamBreadcrumbResolver ] }) export class BitstreamPageRoutingModule { diff --git a/src/app/bitstream-page/bitstream-page.resolver.ts b/src/app/bitstream-page/bitstream-page.resolver.ts index fd9d5b351b..7da80cff5f 100644 --- a/src/app/bitstream-page/bitstream-page.resolver.ts +++ b/src/app/bitstream-page/bitstream-page.resolver.ts @@ -7,6 +7,19 @@ import { BitstreamDataService } from '../core/data/bitstream-data.service'; import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; +/** + * The self links defined in this list are expected to be requested somewhere in the near future + * Requesting them as embeds will limit the number of requests + */ + export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ + followLink('format', {}, + followLink('parentCommunity', {}, + followLink('parentCommunity')) + ), + followLink('bundle'), + followLink('thumbnail') +]; + /** * This class represents a resolver that requests a specific bitstream before the route is activated */ diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts new file mode 100644 index 0000000000..00be49166c --- /dev/null +++ b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { Bitstream } from '../shared/bitstream.model'; +import { BitstreamDataService } from '../data/bitstream-data.service'; +import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from 'src/app/bitstream-page/bitstream-page.resolver'; + +/** + * The class that resolves the BreadcrumbConfig object for an Item + */ +@Injectable({ + providedIn: 'root' +}) +export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { + constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: BitstreamDataService) { + super(breadcrumbService, dataService); + } + + /** + * Method that returns the follow links to already resolve + * The self links defined in this list are expected to be requested somewhere in the near future + * Requesting them as embeds will limit the number of requests + */ + get followLinks(): FollowLinkConfig[] { + return BITSTREAM_PAGE_LINKS_TO_FOLLOW; + } +} diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index baf2f82635..c855325d2d 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -7,13 +7,13 @@ import { BITSTREAM_FORMAT } from './bitstream-format.resource-type'; import { BITSTREAM } from './bitstream.resource-type'; import { DSpaceObject } from './dspace-object.model'; import { HALLink } from './hal-link.model'; -import { HALResource } from './hal-resource.model'; import {BUNDLE} from './bundle.resource-type'; import {Bundle} from './bundle.model'; +import { ChildHALResource } from './child-hal-resource.model'; @typedObject @inheritSerialization(DSpaceObject) -export class Bitstream extends DSpaceObject implements HALResource { +export class Bitstream extends DSpaceObject implements ChildHALResource { static type = BITSTREAM; /** @@ -66,4 +66,8 @@ export class Bitstream extends DSpaceObject implements HALResource { */ @link(BUNDLE) bundle?: Observable>; + + getParentLinkKey(): keyof this['_links'] { + return 'format'; + } } From ffe5922990bf67a8c762dff01219c933dcd3394d Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 21 Apr 2022 12:50:18 +0530 Subject: [PATCH 07/87] [CST-5676] resolver overwrite --- .../bitstream-breadcrumb.resolver.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts index 00be49166c..de8da19a41 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts @@ -1,10 +1,16 @@ import { Injectable } from '@angular/core'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { Bitstream } from '../shared/bitstream.model'; import { BitstreamDataService } from '../data/bitstream-data.service'; import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from 'src/app/bitstream-page/bitstream-page.resolver'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { BreadcrumbConfig } from 'src/app/breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators'; +import { map } from 'rxjs/operators'; +import { hasValue } from 'src/app/shared/empty.util'; +import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; /** * The class that resolves the BreadcrumbConfig object for an Item @@ -17,6 +23,29 @@ export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver> { + const uuid = route.params.id; + return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + map((object: Bitstream) => { + if (hasValue(object)) { + const fullPath = state.url; + const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; + return {provider: this.breadcrumbService, key: object, url: url}; + } else { + return undefined; + } + }) + ); + } + /** * Method that returns the follow links to already resolve * The self links defined in this list are expected to be requested somewhere in the near future From 459da211bede61fb6f2170b96a80293885bdac6c Mon Sep 17 00:00:00 2001 From: nibou230 Date: Wed, 13 Apr 2022 13:03:37 -0400 Subject: [PATCH 08/87] Display the access status badges --- config/config.example.yml | 2 + src/app/core/core.module.ts | 4 +- src/app/core/data/item-data.service.ts | 22 +++ .../access-status-badge.component.html | 3 + .../access-status-badge.component.spec.ts | 160 ++++++++++++++++++ .../access-status-badge.component.ts | 45 +++++ .../access-status.model.ts | 23 +++ .../access-status.resource-type.ts | 9 + ...-search-result-list-element.component.html | 5 +- ...em-search-result-list-element.component.ts | 6 + src/app/shared/shared.module.ts | 2 + src/assets/i18n/en.json5 | 10 ++ src/assets/i18n/fr.json5 | 15 ++ src/config/default-app-config.ts | 5 +- src/config/ui-server-config.interface.ts | 2 + src/environments/environment.test.ts | 4 +- 16 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 src/app/shared/object-list/access-status-badge/access-status-badge.component.html create mode 100644 src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts create mode 100644 src/app/shared/object-list/access-status-badge/access-status-badge.component.ts create mode 100644 src/app/shared/object-list/access-status-badge/access-status.model.ts create mode 100644 src/app/shared/object-list/access-status-badge/access-status.resource-type.ts diff --git a/config/config.example.yml b/config/config.example.yml index 771c7b1653..724d7f8a64 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -13,6 +13,8 @@ ui: rateLimiter: windowMs: 60000 # 1 minute max: 500 # limit each IP to 500 requests per windowMs + # Show the file access status in items lists + showAccessStatuses: false # The REST API server settings # NOTE: these must be 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index e9e242dbc0..b90a3edd46 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -163,6 +163,7 @@ import { SequenceService } from './shared/sequence.service'; import { CoreState } from './core-state.model'; import { GroupDataService } from './eperson/group-data.service'; import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; +import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -346,7 +347,8 @@ export const models = UsageReport, Root, SearchConfig, - SubmissionAccessesModel + SubmissionAccessesModel, + AccessStatusObject ]; @NgModule({ diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index cb5d7a3d57..9a2dc5a8af 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -36,6 +36,7 @@ import { sendRequest } from '../shared/request.operators'; import { RestRequest } from './rest-request.model'; import { CoreState } from '../core-state.model'; import { FindListOptions } from './find-list-options.model'; +import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model'; @Injectable() @dataService(ITEM) @@ -291,6 +292,27 @@ export class ItemDataService extends DataService { ); } + /** + * Get the the access status + * @param itemId + */ + public getAccessStatus(itemId: string): Observable> { + const requestId = this.requestService.generateRequestId(); + const href$ = this.halService.getEndpoint('accessStatus').pipe( + map((href) => href.replace('{?uuid}', `?uuid=${itemId}`)) + ); + + href$.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new GetRequest(requestId, href); + this.requestService.send(request); + }) + ).subscribe(); + + return this.rdbService.buildFromRequestUUID(requestId); + } + /** * Invalidate the cache of the item * @param itemUUID diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.html b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html new file mode 100644 index 0000000000..a7b3edc9b3 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html @@ -0,0 +1,3 @@ +
+ {{ status | translate }} +
diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts new file mode 100644 index 0000000000..da0677a5de --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts @@ -0,0 +1,160 @@ +import { Item } from '../../../core/shared/item.model'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { TruncatePipe } from '../../utils/truncate.pipe'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { AccessStatusBadgeComponent } from './access-status-badge.component'; +import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { By } from '@angular/platform-browser'; +import { ItemDataService } from 'src/app/core/data/item-data.service'; +import { AccessStatusObject } from './access-status.model'; + +describe('ItemAccessStatusBadgeComponent', () => { + let component: AccessStatusBadgeComponent; + let fixture: ComponentFixture; + + let unknownStatus: AccessStatusObject; + let metadataOnlyStatus: AccessStatusObject; + let openAccessStatus: AccessStatusObject; + let embargoStatus: AccessStatusObject; + let restrictedStatus: AccessStatusObject; + + let itemDataService: ItemDataService; + + let item: Item; + + function init() { + unknownStatus = Object.assign(new AccessStatusObject(), { + status: 'unknown' + }); + + metadataOnlyStatus = Object.assign(new AccessStatusObject(), { + status: 'metadata.only' + }); + + openAccessStatus = Object.assign(new AccessStatusObject(), { + status: 'open.access' + }); + + embargoStatus = Object.assign(new AccessStatusObject(), { + status: 'embargo' + }); + + restrictedStatus = Object.assign(new AccessStatusObject(), { + status: 'restricted' + }); + + itemDataService = jasmine.createSpyObj('itemDataService', { + getAccessStatus: createSuccessfulRemoteDataObject$(unknownStatus) + }); + + item = Object.assign(new Item(), { + uuid: 'item-uuid' + }); + } + + function initTestBed() { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [AccessStatusBadgeComponent, TruncatePipe], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: ItemDataService, useValue: itemDataService} + ] + }).compileComponents(); + } + + function initFixtureAndComponent() { + fixture = TestBed.createComponent(AccessStatusBadgeComponent); + component = fixture.componentInstance; + component.uuid = item.uuid; + fixture.detectChanges(); + } + + function lookForAccessStatusBadge(status: string) { + const badge = fixture.debugElement.query(By.css('span.badge')); + expect(badge.nativeElement.textContent).toEqual(`access-status.${status.toLowerCase()}.listelement.badge`); + } + + describe('init', () => { + beforeEach(waitForAsync(() => { + init(); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should init the component', () => { + expect(component).toBeTruthy(); + }); + }); + + describe('When the getAccessStatus method returns unknown', () => { + beforeEach(waitForAsync(() => { + init(); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the unknown badge', () => { + lookForAccessStatusBadge('unknown'); + }); + }); + + describe('When the getAccessStatus method returns metadata.only', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(metadataOnlyStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the metadata only badge', () => { + lookForAccessStatusBadge('metadata.only'); + }); + }); + + describe('When the getAccessStatus method returns open.access', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(openAccessStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the open access badge', () => { + lookForAccessStatusBadge('open.access'); + }); + }); + + describe('When the getAccessStatus method returns embargo', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(embargoStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the embargo badge', () => { + lookForAccessStatusBadge('embargo'); + }); + }); + + describe('When the getAccessStatus method returns restricted', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(restrictedStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the restricted badge', () => { + lookForAccessStatusBadge('restricted'); + }); + }); +}); diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts new file mode 100644 index 0000000000..b181ae4104 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts @@ -0,0 +1,45 @@ +import { Component, Input } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { getFirstSucceededRemoteDataPayload } from 'src/app/core/shared/operators'; +import { ItemDataService } from 'src/app/core/data/item-data.service'; +import { AccessStatusObject } from './access-status.model'; +import { hasValue } from '../../empty.util'; + +@Component({ + selector: 'ds-access-status-badge', + templateUrl: './access-status-badge.component.html' +}) +/** + * Component rendering the access status of an item as a badge + */ +export class AccessStatusBadgeComponent { + + private _uuid: string; + private _accessStatus$: Observable; + + /** + * Initialize instance variables + * + * @param {ItemDataService} itemDataService + */ + constructor(private itemDataService: ItemDataService) { } + + ngOnInit(): void { + this._accessStatus$ = this.itemDataService + .getAccessStatus(this._uuid) + .pipe( + getFirstSucceededRemoteDataPayload(), + map((accessStatus: AccessStatusObject) => hasValue(accessStatus.status) ? accessStatus.status : 'unknown'), + map((status: string) => `access-status.${status.toLowerCase()}.listelement.badge`) + ); + } + + @Input() set uuid(uuid: string) { + this._uuid = uuid; + } + + get accessStatus$(): Observable { + return this._accessStatus$; + } +} diff --git a/src/app/shared/object-list/access-status-badge/access-status.model.ts b/src/app/shared/object-list/access-status-badge/access-status.model.ts new file mode 100644 index 0000000000..6cdcafdbd8 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status.model.ts @@ -0,0 +1,23 @@ +import { autoserialize } from 'cerialize'; +import { typedObject } from 'src/app/core/cache/builders/build-decorators'; +import { ResourceType } from 'src/app/core/shared/resource-type'; +import { excludeFromEquals } from 'src/app/core/utilities/equals.decorators'; +import { ACCESS_STATUS } from './access-status.resource-type'; + +@typedObject +export class AccessStatusObject { + static type = ACCESS_STATUS; + + /** + * The type for this AccessStatusObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The access status value + */ + @autoserialize + status: string; +} diff --git a/src/app/shared/object-list/access-status-badge/access-status.resource-type.ts b/src/app/shared/object-list/access-status-badge/access-status.resource-type.ts new file mode 100644 index 0000000000..ead2afc0b1 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from 'src/app/core/shared/resource-type'; + +/** + * The resource type for Access Status + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const ACCESS_STATUS = new ResourceType('accessStatus'); diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index 8898632eb5..8bea795cca 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -1,4 +1,7 @@ - +
+ + +
Date: Wed, 20 Apr 2022 09:12:59 -0400 Subject: [PATCH 09/87] Adapt the service to a LinkedRepository result --- src/app/core/data/item-data.service.spec.ts | 36 ++++++++++++++++--- src/app/core/data/item-data.service.ts | 30 +++++++++------- .../data/version-history-data.service.spec.ts | 2 +- .../access-status.model.ts | 14 ++++++-- .../testing/hal-endpoint-service.stub.ts | 6 +++- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index cc1e3b6e20..e85ddb2f38 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -10,12 +10,13 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { ItemDataService } from './item-data.service'; -import { DeleteRequest, PostRequest } from './request.models'; +import { DeleteRequest, GetRequest, PostRequest } from './request.models'; import { RequestService } from './request.service'; import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { CoreState } from '../core-state.model'; import { RequestEntry } from './request-entry.model'; import { FindListOptions } from './find-list-options.model'; +import { HALEndpointServiceStub } from 'src/app/shared/testing/hal-endpoint-service.stub'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -36,13 +37,11 @@ describe('ItemDataService', () => { }) as RequestService; const rdbService = getMockRemoteDataBuildService(); - const itemEndpoint = 'https://rest.api/core/items'; + const itemEndpoint = 'https://rest.api/core'; const store = {} as Store; const objectCache = {} as ObjectCacheService; - const halEndpointService = jasmine.createSpyObj('halService', { - getEndpoint: observableOf(itemEndpoint) - }); + const halEndpointService: any = new HALEndpointServiceStub(itemEndpoint); const bundleService = jasmine.createSpyObj('bundleService', { findByHref: {} }); @@ -185,6 +184,33 @@ describe('ItemDataService', () => { }); }); + describe('getAccessStatusEndpoint', () => { + beforeEach(() => { + service = initTestService(); + }); + it('should retrieve the access status endpoint', () => { + const itemId = '3de6ea60-ec39-419b-ae6f-065930ac1429'; + const result = service.getAccessStatusEndpoint(itemId); + result.subscribe(((value) => { + expect(value).toEqual(`${itemEndpoint}/items/${itemId}/accessStatus`); + })); + }); + }); + + describe('getAccessStatus', () => { + beforeEach(() => { + service = initTestService(); + }); + it('should send a GET request', (done) => { + const itemId = '3de6ea60-ec39-419b-ae6f-065930ac1429'; + const result = service.getAccessStatus(itemId); + result.subscribe(() => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest)); + done(); + }); + }); + }); + describe('when cache is invalidated', () => { beforeEach(() => { service = initTestService(); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 9a2dc5a8af..c7f0f541f8 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -292,25 +292,31 @@ export class ItemDataService extends DataService { ); } + /** + * Get the endpoint for an item's access status + * @param itemId + */ + public getAccessStatusEndpoint(itemId: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + switchMap((url: string) => this.halService.getEndpoint('accessStatus', `${url}/${itemId}`)) + ); + } + /** * Get the the access status * @param itemId */ public getAccessStatus(itemId: string): Observable> { - const requestId = this.requestService.generateRequestId(); - const href$ = this.halService.getEndpoint('accessStatus').pipe( - map((href) => href.replace('{?uuid}', `?uuid=${itemId}`)) - ); + const hrefObs = this.getAccessStatusEndpoint(itemId); - href$.pipe( - find((href: string) => hasValue(href)), - map((href: string) => { - const request = new GetRequest(requestId, href); - this.requestService.send(request); - }) - ).subscribe(); + hrefObs.pipe( + take(1) + ).subscribe((href) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.send(request); + }); - return this.rdbService.buildFromRequestUUID(requestId); + return this.rdbService.buildSingle(hrefObs); } /** diff --git a/src/app/core/data/version-history-data.service.spec.ts b/src/app/core/data/version-history-data.service.spec.ts index 207093b4d5..26ed370026 100644 --- a/src/app/core/data/version-history-data.service.spec.ts +++ b/src/app/core/data/version-history-data.service.spec.ts @@ -151,7 +151,7 @@ describe('VersionHistoryDataService', () => { describe('when getVersionsEndpoint is called', () => { it('should return the correct value', () => { service.getVersionsEndpoint(versionHistoryId).subscribe((res) => { - expect(res).toBe(url + '/versions'); + expect(res).toBe(url + '/versionhistories/version-history-id/versions'); }); }); }); diff --git a/src/app/shared/object-list/access-status-badge/access-status.model.ts b/src/app/shared/object-list/access-status-badge/access-status.model.ts index 6cdcafdbd8..31de1a3a38 100644 --- a/src/app/shared/object-list/access-status-badge/access-status.model.ts +++ b/src/app/shared/object-list/access-status-badge/access-status.model.ts @@ -1,11 +1,13 @@ -import { autoserialize } from 'cerialize'; +import { autoserialize, deserialize } from 'cerialize'; import { typedObject } from 'src/app/core/cache/builders/build-decorators'; +import { CacheableObject } from 'src/app/core/cache/object-cache.reducer'; +import { HALLink } from 'src/app/core/shared/hal-link.model'; import { ResourceType } from 'src/app/core/shared/resource-type'; import { excludeFromEquals } from 'src/app/core/utilities/equals.decorators'; import { ACCESS_STATUS } from './access-status.resource-type'; @typedObject -export class AccessStatusObject { +export class AccessStatusObject implements CacheableObject { static type = ACCESS_STATUS; /** @@ -20,4 +22,12 @@ export class AccessStatusObject { */ @autoserialize status: string; + + /** + * The {@link HALLink}s for this AccessStatusObject + */ + @deserialize + _links: { + self: HALLink; + }; } diff --git a/src/app/shared/testing/hal-endpoint-service.stub.ts b/src/app/shared/testing/hal-endpoint-service.stub.ts index 19f95d577c..753efcdb5d 100644 --- a/src/app/shared/testing/hal-endpoint-service.stub.ts +++ b/src/app/shared/testing/hal-endpoint-service.stub.ts @@ -1,9 +1,13 @@ import { of as observableOf } from 'rxjs'; +import { hasValue } from '../empty.util'; export class HALEndpointServiceStub { constructor(private url: string) {} - getEndpoint(path: string) { + getEndpoint(path: string, startHref?: string) { + if (hasValue(startHref)) { + return observableOf(startHref + '/' + path); + } return observableOf(this.url + '/' + path); } } From a2a241b906d457164f2954733f8481792e341269 Mon Sep 17 00:00:00 2001 From: nibou230 Date: Thu, 21 Apr 2022 12:32:11 -0400 Subject: [PATCH 10/87] Added the badge to some components for coherence --- ...m-search-result-grid-element.component.html | 1 + ...tem-search-result-grid-element.component.ts | 6 ++++++ .../access-status-badge.component.html | 4 ++-- .../access-status-badge.component.ts | 18 +++++++++++++----- .../item-list-preview.component.html | 5 ++++- .../item-list-preview.component.ts | 9 +++++++++ ...m-search-result-list-element.component.html | 2 +- 7 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html index 7fdb505d43..c4bb444d44 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html @@ -18,6 +18,7 @@
+

diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts index a628c1e7e8..77fc25ff29 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts @@ -6,6 +6,7 @@ import { SearchResultGridElementComponent } from '../../search-result-grid-eleme import { Item } from '../../../../../core/shared/item.model'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { getItemPageRoute } from '../../../../../item-page/item-page-routing-paths'; +import { environment } from 'src/environments/environment'; @listableObjectComponent('PublicationSearchResult', ViewMode.GridElement) @listableObjectComponent(ItemSearchResult, ViewMode.GridElement) @@ -23,9 +24,14 @@ export class ItemSearchResultGridElementComponent extends SearchResultGridElemen * Route to the item's page */ itemPageRoute: string; + /** + * Whether to show the access status badge or not + */ + showAccessStatus: boolean; ngOnInit(): void { super.ngOnInit(); this.itemPageRoute = getItemPageRoute(this.dso); + this.showAccessStatus = environment.ui.showAccessStatuses; } } diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.html b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html index a7b3edc9b3..8fc3a82740 100644 --- a/src/app/shared/object-list/access-status-badge/access-status-badge.component.html +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html @@ -1,3 +1,3 @@ -
- {{ status | translate }} +
+ {{ accessStatus | translate }}
diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts index b181ae4104..bf76a429f7 100644 --- a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core'; -import { map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; -import { getFirstSucceededRemoteDataPayload } from 'src/app/core/shared/operators'; +import { catchError, map } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; +import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators'; import { ItemDataService } from 'src/app/core/data/item-data.service'; import { AccessStatusObject } from './access-status.model'; import { hasValue } from '../../empty.util'; @@ -29,9 +29,17 @@ export class AccessStatusBadgeComponent { this._accessStatus$ = this.itemDataService .getAccessStatus(this._uuid) .pipe( - getFirstSucceededRemoteDataPayload(), + getFirstCompletedRemoteData(), + map((accessStatusRD) => { + if (accessStatusRD.statusCode !== 401 && hasValue(accessStatusRD.payload)) { + return accessStatusRD.payload; + } else { + return []; + } + }), map((accessStatus: AccessStatusObject) => hasValue(accessStatus.status) ? accessStatus.status : 'unknown'), - map((status: string) => `access-status.${status.toLowerCase()}.listelement.badge`) + map((status: string) => `access-status.${status.toLowerCase()}.listelement.badge`), + catchError(() => observableOf('access-status.unknown.listelement.badge')) ); } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html index c518d39bd9..4801e66a26 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html @@ -2,7 +2,10 @@ - +
+ + +

diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts index 840960d51f..6b2290c711 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts @@ -1,4 +1,5 @@ import { Component, Input } from '@angular/core'; +import { environment } from 'src/environments/environment'; import { Item } from '../../../../core/shared/item.model'; import { fadeInOut } from '../../../animations/fade'; @@ -36,4 +37,12 @@ export class ItemListPreviewComponent { */ @Input() showSubmitter = false; + /** + * Whether to show the access status badge or not + */ + showAccessStatus: boolean; + + ngOnInit(): void { + this.showAccessStatus = environment.ui.showAccessStatuses; + } } diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index 8bea795cca..8e216bb82c 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -1,6 +1,6 @@
- +
From 9f50b4997cbe0b6a93c701ceff93596af0b9922b Mon Sep 17 00:00:00 2001 From: nibou230 Date: Thu, 21 Apr 2022 14:30:23 -0400 Subject: [PATCH 11/87] Changed the import for CacheableObject --- .../object-list/access-status-badge/access-status.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/object-list/access-status-badge/access-status.model.ts b/src/app/shared/object-list/access-status-badge/access-status.model.ts index 31de1a3a38..69b5e920d0 100644 --- a/src/app/shared/object-list/access-status-badge/access-status.model.ts +++ b/src/app/shared/object-list/access-status-badge/access-status.model.ts @@ -1,6 +1,6 @@ import { autoserialize, deserialize } from 'cerialize'; import { typedObject } from 'src/app/core/cache/builders/build-decorators'; -import { CacheableObject } from 'src/app/core/cache/object-cache.reducer'; +import { CacheableObject } from 'src/app/core/cache/cacheable-object.model'; import { HALLink } from 'src/app/core/shared/hal-link.model'; import { ResourceType } from 'src/app/core/shared/resource-type'; import { excludeFromEquals } from 'src/app/core/utilities/equals.decorators'; From 361bb7f7dc4e4fb3b220f16f34e54b0d605f6824 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 3 Feb 2022 14:05:31 +1300 Subject: [PATCH 12/87] [TLC-254] Port submission field type binding from DSpace-CRIS 7 --- src/app/core/core.module.ts | 2 + src/app/shared/empty.util.ts | 26 ++ ...amic-form-control-container.component.html | 3 +- ...c-form-control-container.component.spec.ts | 10 + ...ynamic-form-control-container.component.ts | 26 +- .../ds-dynamic-type-bind-relation.service.ts | 222 ++++++++++++++++++ .../dynamic-form-array.component.html | 12 +- .../dynamic-form-array.component.ts | 3 + .../models/date-picker/date-picker.model.ts | 37 +++ .../models/ds-dynamic-input.model.ts | 18 +- .../models/ds-dynamic-row-array-model.ts | 28 ++- .../dynamic-form-group.component.html | 12 +- .../dynamic-form-group.component.ts | 5 +- .../form/builder/form-builder.service.ts | 157 ++++++++++++- .../form/builder/models/form-field.model.ts | 6 + .../form/builder/parsers/date-field-parser.ts | 6 +- .../form/builder/parsers/field-parser.ts | 41 +++- 17 files changed, 585 insertions(+), 29 deletions(-) create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index e9e242dbc0..71c3230a38 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -137,6 +137,7 @@ import { SiteAdministratorGuard } from './data/feature-authorization/feature-aut import { Registration } from './shared/registration.model'; import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; import { MetadataFieldDataService } from './data/metadata-field-data.service'; +import { DsDynamicTypeBindRelationService } from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; import { TokenResponseParsingService } from './auth/token-response-parsing.service'; import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service'; import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; @@ -250,6 +251,7 @@ const PROVIDERS = [ ClaimedTaskDataService, PoolTaskDataService, BitstreamDataService, + DsDynamicTypeBindRelationService, EntityTypeService, ContentSourceResponseParsingService, ItemTemplateDataService, diff --git a/src/app/shared/empty.util.ts b/src/app/shared/empty.util.ts index d79c520fda..355314550a 100644 --- a/src/app/shared/empty.util.ts +++ b/src/app/shared/empty.util.ts @@ -177,3 +177,29 @@ export const isNotEmptyOperator = () => export const ensureArrayHasValue = () => (source: Observable): Observable => source.pipe(map((arr: T[]): T[] => Array.isArray(arr) ? arr : [])); + +/** + * Verifies that a object keys are all empty or not. + * isObjectEmpty(); // true + * isObjectEmpty(null); // true + * isObjectEmpty(undefined); // true + * isObjectEmpty(''); // true + * isObjectEmpty([]); // true + * isObjectEmpty({}); // true + * isObjectEmpty({name: null}); // true + * isObjectEmpty({ name: 'Adam Hawkins', surname : null}); // false + */ +export function isObjectEmpty(obj?: any): boolean { + + if (typeof(obj) !== 'object') { + return true; + } + + for (const key in obj) { + if (obj.hasOwnProperty(key) && isNotEmpty(obj[key])) { + return false; + } + } + return true; +} + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 55e354ea7a..7eef1d8655 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -1,4 +1,5 @@
- +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index b67e6f9e46..785b0958d5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -65,6 +65,7 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a import { DsDynamicFormGroupComponent } from './models/form-group/dynamic-form-group.component'; import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; +import { DsDynamicTypeBindRelationService } from './ds-dynamic-type-bind-relation.service'; import { RelationshipService } from '../../../../core/data/relationship.service'; import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service'; import { ItemDataService } from '../../../../core/data/item-data.service'; @@ -79,6 +80,13 @@ import { SubmissionService } from '../../../../submission/submission.service'; import { FormBuilderService } from '../form-builder.service'; import { NgxMaskModule } from 'ngx-mask'; +function getMockDsDynamicTypeBindRelationService(): DsDynamicTypeBindRelationService { + return jasmine.createSpyObj('DsDynamicTypeBindRelationService', { + getRelatedFormModel: jasmine.createSpy('getRelatedFormModel'), + isFormControlToBeHidden: jasmine.createSpy('isFormControlToBeHidden') + }); +} + describe('DsDynamicFormControlContainerComponent test suite', () => { const vocabularyOptions: VocabularyOptions = { @@ -142,6 +150,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { submissionId: '1234', id: 'relationGroup', formConfiguration: [], + isInlineGroup: false, mandatoryField: '', name: 'relationGroup', relationFields: [], @@ -200,6 +209,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { providers: [ DsDynamicFormControlContainerComponent, DynamicFormService, + { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, { provide: RelationshipService, useValue: {} }, { provide: SelectableListService, useValue: {} }, { provide: ItemDataService, useValue: {} }, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index c3359fd65a..8d27a3bace 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -76,11 +76,13 @@ import { DsDynamicLookupComponent } from './models/lookup/dynamic-lookup.compone import { DsDynamicFormGroupComponent } from './models/form-group/dynamic-form-group.component'; import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-array.component'; import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; +import { DynamicRelationGroupModel } from './models/relation-group/dynamic-relation-group.model'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model'; import { CustomSwitchComponent } from './models/custom-switch/custom-switch.component'; import { find, map, startWith, switchMap, take } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; +import { DsDynamicTypeBindRelationService } from './ds-dynamic-type-bind-relation.service'; import { SearchResult } from '../../../search/models/search-result.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; @@ -194,8 +196,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList; // eslint-disable-next-line @angular-eslint/no-input-rename @Input('templates') inputTemplateList: QueryList; - + @Input() hasMetadataModel: any; @Input() formId: string; + @Input() formGroup: FormGroup; + @Input() formModel: DynamicFormControlModel[]; @Input() asBootstrapFormGroup = false; @Input() bindId = true; @Input() context: any | null = null; @@ -237,6 +241,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo protected dynamicFormComponentService: DynamicFormComponentService, protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, + protected typeBindRelationService: DsDynamicTypeBindRelationService, protected translateService: TranslateService, protected relationService: DynamicFormRelationService, private modalService: NgbModal, @@ -343,6 +348,9 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo if (this.model && this.model.placeholder) { this.model.placeholder = this.translateService.instant(this.model.placeholder); } + if (this.model.typeBindRelations && this.model.typeBindRelations.length > 0) { + this.subscriptions.push(...this.typeBindRelationService.subscribeRelations(this.model, this.control)); + } } } @@ -357,6 +365,22 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo this.showErrorMessagesPreviousStage = this.showErrorMessages; } + protected createFormControlComponent(): void { + super.createFormControlComponent(); + if (this.componentType !== null) { + let index; + + if (this.context && this.context instanceof DynamicFormArrayGroupModel) { + index = this.context.index; + } + const instance = this.dynamicFormComponentService.getFormControlRef(this.model, index); + if (instance) { + (instance as any).formModel = this.formModel; + (instance as any).formGroup = this.formGroup; + } + } + } + /** * Since Form Control Components created dynamically have 'OnPush' change detection strategy, * changes are not propagated. So use this method to force an update diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts new file mode 100644 index 0000000000..3ec6909c07 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts @@ -0,0 +1,222 @@ +import { Inject, Injectable, Injector, Optional } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +import { Subscription } from 'rxjs'; +import { startWith } from 'rxjs/operators'; + +import { + AND_OPERATOR, + DYNAMIC_MATCHERS, + DynamicFormControlCondition, + DynamicFormControlMatcher, + DynamicFormControlModel, + DynamicFormControlRelation, + DynamicFormRelationService, + OR_OPERATOR +} from '@ng-dynamic-forms/core'; + +import { isNotUndefined, isUndefined } from '../../../empty.util'; +import { FormBuilderService } from '../form-builder.service'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-constants'; + +/** + * Service to manage type binding for submission input fields + * Any form component with the typeBindRelations DynamicFormControlRelation property can be controlled this way + */ +@Injectable() +export class DsDynamicTypeBindRelationService { + + constructor(@Optional() @Inject(DYNAMIC_MATCHERS) private dynamicMatchers: DynamicFormControlMatcher[], + protected dynamicFormRelationService: DynamicFormRelationService, + protected formBuilderService: FormBuilderService, + protected injector: Injector) { + + } + + /** + * Return the string value of the type bind model + * @param bindModelValue + * @private + */ + private static getTypeBindValue(bindModelValue: string | FormFieldMetadataValueObject): string { + let value; + if (isUndefined(bindModelValue) || typeof bindModelValue === 'string') { + value = bindModelValue; + } else if (bindModelValue.hasAuthority()) { + value = bindModelValue.authority; + } else { + value = bindModelValue.value; + } + + return value; + } + + /** + * Get models for this bind type + * @param model + */ + public getRelatedFormModel(model: DynamicFormControlModel): DynamicFormControlModel[] { + + const models: DynamicFormControlModel[] = []; + + (model as any).typeBindRelations.forEach((relGroup) => relGroup.when.forEach((rel) => { + + if (model.id === rel.id) { + throw new Error(`FormControl ${model.id} cannot depend on itself`); + } + + const bindModel: DynamicFormControlModel = this.formBuilderService.getTypeBindModel(); + + if (model && !models.some((modelElement) => modelElement === bindModel)) { + models.push(bindModel); + } + })); + + return models; + } + + /** + * Return true if the type bind relation (eg. {MATCH_VISIBLE, OR, ['book', 'book part']}) matches the value in + * matcher.match (or matcher.opposingMatch? not sure what that is), which in this case would be the current dc.type + * of the submission item + * @param relation type bind relation (eg. {MATCH_VISIBLE, OR, ['book', 'book part']}) + * @param matcher contains 'match' value and an onChange() event listener + */ + public matchesCondition(relation: DynamicFormControlRelation, matcher: DynamicFormControlMatcher): boolean { + + // Default to OR for operator (OR is explicitly set in field-parser.ts anyway) + const operator = relation.operator || OR_OPERATOR; + + + return relation.when.reduce((hasAlreadyMatched: boolean, condition: DynamicFormControlCondition, index: number) => { + // Get the DynamicFormControlModel (typeBindModel) from the form builder service, set in the form builder + // in the form model at init time in formBuilderService.modelFromConfiguration (called by other form components + // like relation group component and submission section form component). + // This model (DynamicRelationGroupModel) contains eg. mandatory field, formConfiguration, relationFields, + // submission scope, form/section type and other high level properties + const bindModel: any = this.formBuilderService.getTypeBindModel(); + + let values: string[]; + let bindModelValue = bindModel.value; + + // If the form type is RELATION, map values to the mandatory field for the model? Don't totally understand + // what is going on here + if (bindModel.type === DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP) { + bindModelValue = bindModel.value.map((entry) => entry[bindModel.mandatoryField]); + } + // If we have an array of values, map the bindModelValue[] back to values[], looking up + // the type bind value for each in the static method here (this just handles cases where authority should + // be used, or where the entry doesn't have .value but is a string itself, etc) + // If values isn't an array, make it a single element array with the looked-up type bind value. + if (Array.isArray(bindModelValue)) { + values = [...bindModelValue.map((entry) => DsDynamicTypeBindRelationService.getTypeBindValue(entry))]; + } else { + values = [DsDynamicTypeBindRelationService.getTypeBindValue(bindModelValue)]; + } + + // If bind model evaluates to 'true' (is not undefined, is not null, is not false etc, + // AND the relation match (type bind) is equal to the matcher match (item publication type), then the return + // value is initialised as false. I'm not sure why the negation is used here! + // Perhaps as a fail-safe for a bad mind model but an exact match of the strings in relation and matcher + // passed to this method. + let returnValue = (!(bindModel && relation.match === matcher.match)); + + // Iterate the type bind values parsed and mapped from our form/relation group model + for (const value of values) { + if (bindModel && relation.match === matcher.match) { + // If we're not at the first array element, and we're using the AND operator, and we have not + // yet matched anything, return false. This is just a kind of short-hand put in here for some kind of + // optimisation, I guess, since the AND requires all values to match, and if we're on index > 0 but haven't + // matched then we've already failed. But surely it's simpler and just as optimal to break on the first + // non-match if using the AND operator?! + // In the case of default type bind usage, we always use OR anyway. + if (index > 0 && operator === AND_OPERATOR && !hasAlreadyMatched) { + return false; + } + // If we're not at the first array element, and we're using the OR operator (almost always the case) + // and we've already matched then there is no need to continue, just return true. + if (index > 0 && operator === OR_OPERATOR && hasAlreadyMatched) { + return true; + } + + // Do the actual match. Does condition.value (the item publication type) match the field model + // type bind currently being inspected? + returnValue = condition.value === value; + + // If return value is already true, break. + if (returnValue) { + break; + } + } + + // Here we have tests using 'opposingMatch' which I'm not certain about yet + // It looks like a negation of sorts? Or a 'not equals' comparison used in combination I think? + if (bindModel && relation.match === matcher.opposingMatch) { + // If we're not at the first element, using AND, and already matched, just return true here + if (index > 0 && operator === AND_OPERATOR && hasAlreadyMatched) { + return true; + } + + // If we're not at the first element, using OR, and we have NOT already matched, return false + if (index > 0 && operator === OR_OPERATOR && !hasAlreadyMatched) { + return false; + } + + // Negated comparison + returnValue = !(condition.value === value); + + // Break if already false + if (!returnValue) { + break; + } + } + } + return returnValue; + }, false); + } + + /** + * Return an array of subscriptions to a calling component + * @param model + * @param control + */ + subscribeRelations(model: DynamicFormControlModel, control: FormControl): Subscription[] { + + const relatedModels = this.getRelatedFormModel(model); + const subscriptions: Subscription[] = []; + + Object.values(relatedModels).forEach((relatedModel: any) => { + + if (isNotUndefined(relatedModel)) { + const initValue = (isUndefined(relatedModel.value) || typeof relatedModel.value === 'string') ? relatedModel.value : + (Array.isArray(relatedModel.value) ? relatedModel.value : relatedModel.value.value); + + const valueChanges = relatedModel.valueChanges.pipe( + startWith(initValue) + ); + + // Build up the subscriptions to watch for changes; + // I still don't fully understand what is happening here, or the triggers in various form usage that + // cause which / what to fire change events, why the matcher has onChange() instead of a field value or + // form model, etc. + subscriptions.push(valueChanges.subscribe(() => { + // Iterate each matcher + this.dynamicMatchers.forEach((matcher) => { + + // Find the relation + const relation = this.dynamicFormRelationService.findRelationByMatcher((model as any).typeBindRelations, matcher); + + // If the relation is defined, get matchesCondition result and pass it to the onChange event listener + if (relation !== undefined) { + const hasMatch = this.matchesCondition(relation, matcher); + matcher.onChange(hasMatch, model, control, this.injector); + } + }); + })); + } + }); + + return subscriptions; + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html index bc41ade088..29df7a34c4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html @@ -1,6 +1,7 @@
@@ -13,15 +14,17 @@ cdkDrag cdkDragHandle [cdkDragDisabled]="dragDisabled" - [cdkDragPreviewClass]="'ds-submission-reorder-dragging'"> + [cdkDragPreviewClass]="'ds-submission-reorder-dragging'" + [class.grey-background]="model.isInlineGroupArray"> -
- +
+
- - -
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts index 921b159718..01bba74cc8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts @@ -6,6 +6,7 @@ import { DynamicFormControlCustomEvent, DynamicFormControlEvent, DynamicFormControlLayout, + DynamicFormControlModel, DynamicFormLayout, DynamicFormLayoutService, DynamicFormValidationService, @@ -22,6 +23,8 @@ import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model'; }) export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent { + @Input() bindId = true; + @Input() formModel: DynamicFormControlModel[]; @Input() formLayout: DynamicFormLayout; @Input() group: FormGroup; @Input() layout: DynamicFormControlLayout; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts index 1c053ffc80..94b66b288a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts @@ -2,20 +2,35 @@ import { DynamicDateControlModel, DynamicDatePickerModelConfig, DynamicFormControlLayout, + DynamicFormControlModel, + DynamicFormControlRelation, serializable } from '@ng-dynamic-forms/core'; +import {BehaviorSubject, Subject} from 'rxjs'; +import {isEmpty, isNotEmpty, isNotUndefined} from '../../../../../empty.util'; +import {MetadataValue} from '../../../../../../core/shared/metadata.models'; export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DATE'; export interface DynamicDsDateControlModelConfig extends DynamicDatePickerModelConfig { legend?: string; + typeBindRelations?: DynamicFormControlRelation[]; + securityLevel?: number; + securityConfigLevel?: number[]; + toggleSecurityVisibility?: boolean; } /** * Dynamic Date Picker Model class */ export class DynamicDsDatePickerModel extends DynamicDateControlModel { + @serializable() hiddenUpdates: Subject; + @serializable() typeBindRelations: DynamicFormControlRelation[]; @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER; + @serializable() metadataValue: MetadataValue; + @serializable() securityLevel?: number; + @serializable() securityConfigLevel?: number[]; + @serializable() toggleSecurityVisibility = true; malformedDate: boolean; legend: string; hasLanguages = false; @@ -25,6 +40,28 @@ export class DynamicDsDatePickerModel extends DynamicDateControlModel { super(config, layout); this.malformedDate = false; this.legend = config.legend; + this.metadataValue = (config as any).metadataValue; + this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; + this.hiddenUpdates = new BehaviorSubject(this.hidden); + this.hiddenUpdates.subscribe((hidden: boolean) => { + this.hidden = hidden; + const parentModel = this.getRootParent(this); + if (parentModel && isNotUndefined(parentModel.hidden)) { + parentModel.hidden = hidden; + } + }); + } + + private getRootParent(model: any): DynamicFormControlModel { + if (isEmpty(model) || isEmpty(model.parent)) { + return model; + } else { + return this.getRootParent(model.parent); + } + } + + get hasSecurityLevel(): boolean { + return isNotEmpty(this.securityLevel); } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index 290e29dc65..a9adb9a8e9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -1,14 +1,15 @@ import { - DynamicFormControlLayout, + DynamicFormControlLayout, DynamicFormControlModel, + DynamicFormControlRelation, DynamicInputModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core'; -import { Subject } from 'rxjs'; +import {BehaviorSubject, Subject} from 'rxjs'; import { LanguageCode } from '../../models/form-field-language-value.model'; import { VocabularyOptions } from '../../../../../core/submission/vocabularies/models/vocabulary-options.model'; -import { hasValue } from '../../../../empty.util'; +import {hasValue, isEmpty, isNotUndefined} from '../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; @@ -18,12 +19,14 @@ export interface DsDynamicInputModelConfig extends DynamicInputModelConfig { language?: string; place?: number; value?: any; + typeBindRelations?: DynamicFormControlRelation[]; relationship?: RelationshipOptions; repeatable: boolean; metadataFields: string[]; submissionId: string; hasSelectableMetadata: boolean; metadataValue?: FormFieldMetadataValueObject; + isModelOfInnerForm?: boolean; } @@ -33,12 +36,17 @@ export class DsDynamicInputModel extends DynamicInputModel { @serializable() private _languageCodes: LanguageCode[]; @serializable() private _language: string; @serializable() languageUpdates: Subject; + @serializable() place: number; + @serializable() typeBindRelations: DynamicFormControlRelation[]; + @serializable() typeBindHidden = false; @serializable() relationship?: RelationshipOptions; @serializable() repeatable?: boolean; @serializable() metadataFields: string[]; @serializable() submissionId: string; @serializable() hasSelectableMetadata: boolean; @serializable() metadataValue: FormFieldMetadataValueObject; + @serializable() isModelOfInnerForm: boolean; + constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); @@ -51,6 +59,8 @@ export class DsDynamicInputModel extends DynamicInputModel { this.submissionId = config.submissionId; this.hasSelectableMetadata = config.hasSelectableMetadata; this.metadataValue = config.metadataValue; + this.place = config.place; + this.isModelOfInnerForm = (hasValue(config.isModelOfInnerForm) ? config.isModelOfInnerForm : false); this.language = config.language; if (!this.language) { @@ -71,6 +81,8 @@ export class DsDynamicInputModel extends DynamicInputModel { this.language = lang; }); + this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; + this.vocabularyOptions = config.vocabularyOptions; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts index d0b07de885..e4b18a4feb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts @@ -1,5 +1,12 @@ -import { DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; +import { + DynamicFormArrayModel, + DynamicFormArrayModelConfig, + DynamicFormControlLayout, + DynamicFormControlRelation, + serializable +} from '@ng-dynamic-forms/core'; import { RelationshipOptions } from '../../models/relationship-options.model'; +import { isNotUndefined } from '../../../../empty.util'; export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig { notRepeatable: boolean; @@ -10,6 +17,9 @@ export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig metadataFields: string[]; hasSelectableMetadata: boolean; isDraggable: boolean; + showButtons: boolean; + typeBindRelations?: DynamicFormControlRelation[]; + isInlineGroupArray?: boolean; } export class DynamicRowArrayModel extends DynamicFormArrayModel { @@ -21,17 +31,29 @@ export class DynamicRowArrayModel extends DynamicFormArrayModel { @serializable() metadataFields: string[]; @serializable() hasSelectableMetadata: boolean; @serializable() isDraggable: boolean; + @serializable() showButtons = true; + @serializable() typeBindRelations: DynamicFormControlRelation[]; isRowArray = true; + isInlineGroupArray = false; constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); - this.notRepeatable = config.notRepeatable; - this.required = config.required; + if (isNotUndefined(config.notRepeatable)) { + this.notRepeatable = config.notRepeatable; + } + if (isNotUndefined(config.required)) { + this.required = config.required; + } + if (isNotUndefined(config.showButtons)) { + this.showButtons = config.showButtons; + } this.submissionId = config.submissionId; this.relationshipConfig = config.relationshipConfig; this.metadataKey = config.metadataKey; this.metadataFields = config.metadataFields; this.hasSelectableMetadata = config.hasSelectableMetadata; this.isDraggable = config.isDraggable; + this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; + this.isInlineGroupArray = config.isInlineGroupArray ? config.isInlineGroupArray : false; } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html index 843ed95530..933590b459 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html @@ -1,8 +1,13 @@ +
-
- - +
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts index 789d5eb87c..9d8d73eab5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts @@ -5,7 +5,9 @@ import { DynamicFormControlCustomEvent, DynamicFormControlEvent, DynamicFormControlLayout, - DynamicFormGroupModel, DynamicFormLayout, + DynamicFormControlModel, + DynamicFormGroupModel, + DynamicFormLayout, DynamicFormLayoutService, DynamicFormValidationService, DynamicTemplateDirective @@ -18,6 +20,7 @@ import { }) export class DsDynamicFormGroupComponent extends DynamicFormControlComponent { + @Input() formModel: DynamicFormControlModel[]; @Input() formLayout: DynamicFormLayout; @Input() group: FormGroup; @Input() layout: DynamicFormControlLayout; diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 85d70f20dc..33703abf94 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { AbstractControl, FormGroup } from '@angular/forms'; +import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, @@ -7,6 +7,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_GROUP, DYNAMIC_FORM_CONTROL_TYPE_INPUT, DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP, + DynamicFormArrayGroupModel, DynamicFormArrayModel, DynamicFormComponentService, DynamicFormControlEvent, @@ -19,7 +20,7 @@ import { } from '@ng-dynamic-forms/core'; import { isObject, isString, mergeWith } from 'lodash'; -import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull } from '../../empty.util'; +import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull, isObjectEmpty } from '../../empty.util'; import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model'; @@ -36,12 +37,43 @@ import { VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models'; @Injectable() export class FormBuilderService extends DynamicFormService { + private typeBindModel: DynamicFormControlModel; + + /** + * This map contains the active forms model + */ + private formModels: Map; + + /** + * This map contains the active forms control groups + */ + private formGroups: Map; + constructor( componentService: DynamicFormComponentService, validationService: DynamicFormValidationService, protected rowParser: RowParser ) { super(componentService, validationService); + this.formModels = new Map(); + this.formGroups = new Map(); + } + + createDynamicFormControlEvent(control: FormControl, group: FormGroup, model: DynamicFormControlModel, type: string): DynamicFormControlEvent { + const $event = { + value: (model as any).value, + autoSave: false + }; + const context: DynamicFormArrayGroupModel = (model?.parent instanceof DynamicFormArrayGroupModel) ? model?.parent : null; + return {$event, context, control: control, group: group, model: model, type}; + } + + getTypeBindModel() { + return this.typeBindModel; + } + + setTypeBindModel(model: DynamicFormControlModel) { + this.typeBindModel = model; } findById(id: string, groupModel: DynamicFormControlModel[], arrayIndex = null): DynamicFormControlModel | null { @@ -223,10 +255,11 @@ export class FormBuilderService extends DynamicFormService { return result; } - modelFromConfiguration(submissionId: string, json: string | SubmissionFormsModel, scopeUUID: string, sectionData: any = {}, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never { - let rows: DynamicFormControlModel[] = []; - const rawData = typeof json === 'string' ? JSON.parse(json, parseReviver) : json; - + modelFromConfiguration(submissionId: string, json: string | SubmissionFormsModel, scopeUUID: string, sectionData: any = {}, + submissionScope?: string, readOnly = false, typeBindModel = null, + isInnerForm = false, securityConfig: any = null): DynamicFormControlModel[] | never { + let rows: DynamicFormControlModel[] = []; + const rawData = typeof json === 'string' ? JSON.parse(json, parseReviver) : json; if (rawData.rows && !isEmpty(rawData.rows)) { rawData.rows.forEach((currentRow) => { const rowParsed = this.rowParser.parse(submissionId, currentRow, scopeUUID, sectionData, submissionScope, readOnly); @@ -240,6 +273,13 @@ export class FormBuilderService extends DynamicFormService { }); } + if (isNull(typeBindModel)) { + typeBindModel = this.findById('dc_type', rows); + } + + if (typeBindModel !== null) { + this.setTypeBindModel(typeBindModel); + } return rows; } @@ -309,6 +349,10 @@ export class FormBuilderService extends DynamicFormService { return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null; } + getFormControlByModel(formGroup: FormGroup, fieldModel: DynamicFormControlModel): AbstractControl { + return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null; + } + /** * Note (discovered while debugging) this is not the ID as used in the form, * but the first part of the path needed in a patch operation: @@ -328,6 +372,82 @@ export class FormBuilderService extends DynamicFormService { return (tempModel.id !== tempModel.name) ? tempModel.name : tempModel.id; } + /** + * Add new form model to formModels map + * @param id id of model + * @param model model + */ + addFormModel(id: string, model: DynamicFormControlModel[]): void { + this.formModels.set(id, model); + } + + /** + * If present, remove form model from formModels map + * @param id id of model + */ + removeFormModel(id: string): void { + if (this.formModels.has(id)) { + this.formModels.delete(id); + } + } + + /** + * Add new form model to formModels map + * @param id id of model + * @param formGroup FormGroup + */ + addFormGroups(id: string, formGroup: FormGroup): void { + this.formGroups.set(id, formGroup); + } + + /** + * If present, remove form model from formModels map + * @param id id of model + */ + removeFormGroup(id: string): void { + if (this.formGroups.has(id)) { + this.formGroups.delete(id); + } + } + + /** + * This method searches a field in all forms instantiated + * by form.component and, if found, it updates its value + * + * @param fieldId id of field to update + * @param value new value to set + * @return the model updated if found + */ + updateModelValue(fieldId: string, value: FormFieldMetadataValueObject): DynamicFormControlModel { + let returnModel = null; + this.formModels.forEach((models, formId) => { + const fieldModel: any = this.findById(fieldId, models); + if (hasValue(fieldModel)) { + if (isNotEmpty(value)) { + if (fieldModel.repeatable && isNotEmpty(fieldModel.value)) { + // if model is repeatable and has already a value add a new field instead of replacing it + const formGroup = this.formGroups.get(formId); + const arrayContext = fieldModel.parent?.context; + if (isNotEmpty(formGroup) && isNotEmpty(arrayContext)) { + const formArrayControl = this.getFormControlByModel(formGroup, arrayContext) as FormArray; + const index = arrayContext?.groups?.length; + this.insertFormArrayGroup(index, formArrayControl, arrayContext); + const newAddedModel: any = this.findById(fieldId, models, index); + this.detectChanges(); + newAddedModel.value = value; + returnModel = newAddedModel; + } + } else { + fieldModel.value = value; + returnModel = fieldModel; + } + } + return; + } + }); + return returnModel; + } + /** * Calculate the metadata list related to the event. * @param event @@ -400,4 +520,29 @@ export class FormBuilderService extends DynamicFormService { return Object.keys(result); } + /** + * Add new formbuilder in forma array by copying current formBuilder index + * @param index index of formBuilder selected to be copied + * @param formArray formArray of the inline group forms + * @param formArrayModel formArrayModel model of forms that will be created + */ + copyFormArrayGroup(index: number, formArray: FormArray, formArrayModel: DynamicFormArrayModel) { + + const groupModel = formArrayModel.insertGroup(index); + const previousGroup = formArray.controls[index] as FormGroup; + const newGroup = this.createFormGroup(groupModel.group, null, groupModel); + const previousKey = Object.keys(previousGroup.getRawValue())[0]; + const newKey = Object.keys(newGroup.getRawValue())[0]; + + if (!isObjectEmpty(previousGroup.getRawValue()[previousKey])) { + newGroup.get(newKey).setValue(previousGroup.getRawValue()[previousKey]); + } + + formArray.insert(index, newGroup); + + return newGroup; + } + + + } diff --git a/src/app/shared/form/builder/models/form-field.model.ts b/src/app/shared/form/builder/models/form-field.model.ts index 95ee980aeb..be3150bae3 100644 --- a/src/app/shared/form/builder/models/form-field.model.ts +++ b/src/app/shared/form/builder/models/form-field.model.ts @@ -113,6 +113,12 @@ export class FormFieldModel { @autoserialize style: string; + /** + * Containing types to bind for this field + */ + @autoserialize + typeBind: string[]; + /** * Containing the value for this metadata field */ diff --git a/src/app/shared/form/builder/parsers/date-field-parser.ts b/src/app/shared/form/builder/parsers/date-field-parser.ts index aef0219579..c67c2c7695 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.ts @@ -1,7 +1,7 @@ import { FieldParser } from './field-parser'; import { - DynamicDsDateControlModelConfig, - DynamicDsDatePickerModel + DynamicDsDatePickerModel, + DynamicDsDateControlModelConfig } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { isNotEmpty } from '../../../empty.util'; import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/date-picker/date-picker.component'; @@ -13,7 +13,7 @@ export class DateFieldParser extends FieldParser { let malformedDate = false; const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, false, true); inputDateModelConfig.legend = this.configData.label; - + inputDateModelConfig.disabled = inputDateModelConfig.readOnly; inputDateModelConfig.toggleIcon = 'fas fa-calendar'; this.setValues(inputDateModelConfig as any, fieldValue); // Init Data and validity check diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index da304ca267..838816ebb1 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -1,7 +1,7 @@ import { Inject, InjectionToken } from '@angular/core'; import { uniqueId } from 'lodash'; -import { DynamicFormControlLayout } from '@ng-dynamic-forms/core'; +import {DynamicFormControlLayout, DynamicFormControlRelation, MATCH_VISIBLE, OR_OPERATOR} from '@ng-dynamic-forms/core'; import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util'; import { FormFieldModel } from '../models/form-field.model'; @@ -67,6 +67,7 @@ export abstract class FieldParser { metadataFields: this.getAllFieldIds(), hasSelectableMetadata: isNotEmpty(this.configData.selectableMetadata), isDraggable, + typeBindRelations: isNotEmpty(this.configData.typeBind) ? this.getTypeBindRelations(this.configData.typeBind) : null, groupFactory: () => { let model; if ((arrayCounter === 0)) { @@ -275,7 +276,7 @@ export abstract class FieldParser { // Set label this.setLabel(controlModel, label); if (hint) { - controlModel.hint = this.configData.hints; + controlModel.hint = this.configData.hints || ' '; } controlModel.placeholder = this.configData.label; @@ -292,9 +293,45 @@ export abstract class FieldParser { (controlModel as DsDynamicInputModel).languageCodes = this.configData.languageCodes; } + // If typeBind is configured + if (isNotEmpty(this.configData.typeBind)) { + (controlModel as DsDynamicInputModel).typeBindRelations = this.getTypeBindRelations(this.configData.typeBind); + } + return controlModel; } + /** + * Get the type bind values from the REST data for a specific field + * The return value is any[] in the method signature but in reality it's + * returning the 'relation' that'll be used for a dynamic matcher when filtering + * fields in type bind, made up of a 'match' outcome (make this field visible), an 'operator' + * (OR) and a 'when' condition (the bindValues array). + * @param configuredTypeBindValues array of types from the submission definition (CONFIG_DATA) + * @private + * @return DynamicFormControlRelation[] array with one relation in it, for type bind matching to show a field + */ + private getTypeBindRelations(configuredTypeBindValues: string[]): DynamicFormControlRelation[] { + const bindValues = []; + configuredTypeBindValues.forEach((value) => { + bindValues.push({ + id: 'dc_type', + value: value + }); + }); + // match: MATCH_VISIBLE means that if true, the field / component will be visible + // operator: OR means that all the values in the 'when' condition will be compared with OR, not AND + // when: the list of values to match against, in this case the list of strings from ... + // Example: Field [x] will be VISIBLE if dc_type = book OR dc_type = book_part + // + // The opposing match value will be the dc.type for the workspace item + return [{ + match: MATCH_VISIBLE, + operator: OR_OPERATOR, + when: bindValues + }]; + } + protected hasRegex() { return hasValue(this.configData.input.regex); } From be7f21eb3225dba4eb5c6edec4fc096e05bd0ec7 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 17 Feb 2022 11:50:40 +1300 Subject: [PATCH 13/87] [TLC-254] Make the item type field configurable (default dc.type) --- config/config.example.yml | 5 +++++ src/app/shared/form/builder/form-builder.service.ts | 10 +++++++++- src/app/shared/form/builder/parsers/field-parser.ts | 12 ++++++++++-- src/config/default-app-config.ts | 3 +++ src/config/submission-config.interface.ts | 5 +++++ src/environments/environment.test.ts | 3 +++ 6 files changed, 35 insertions(+), 3 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 771c7b1653..fb0b4fd589 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -77,6 +77,11 @@ submission: # NOTE: after how many time (milliseconds) submission is saved automatically # eg. timer: 5 * (1000 * 60); // 5 minutes timer: 0 + typeBind: + # NOTE: which field to use when matching to type-bind configuration, + # eg. dc.type, local.publicationType + # default: dc.type + field: dc.type icons: metadata: # NOTE: example of configuration diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 33703abf94..12d7585a1c 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -33,6 +33,7 @@ import { dateToString, isNgbDateStruct } from '../../date.util'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-ui/ds-dynamic-form-constants'; import { CONCAT_GROUP_SUFFIX, DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models'; +import { environment } from '../../../../environments/environment'; @Injectable() export class FormBuilderService extends DynamicFormService { @@ -49,6 +50,11 @@ export class FormBuilderService extends DynamicFormService { */ private formGroups: Map; + /** + * This is the field to use for type binding + */ + private typeField: string; + constructor( componentService: DynamicFormComponentService, validationService: DynamicFormValidationService, @@ -57,6 +63,8 @@ export class FormBuilderService extends DynamicFormService { super(componentService, validationService); this.formModels = new Map(); this.formGroups = new Map(); + // Replace . with _ in configured type field here, to make configuration more simple and user-friendly + this.typeField = environment.submission.typeBind.field.replace('\.', '_'); } createDynamicFormControlEvent(control: FormControl, group: FormGroup, model: DynamicFormControlModel, type: string): DynamicFormControlEvent { @@ -274,7 +282,7 @@ export class FormBuilderService extends DynamicFormService { } if (isNull(typeBindModel)) { - typeBindModel = this.findById('dc_type', rows); + typeBindModel = this.findById(this.typeField, rows); } if (typeBindModel !== null) { diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index 838816ebb1..6e1c03efe0 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -17,6 +17,7 @@ import { RelationshipOptions } from '../models/relationship-options.model'; import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model'; import { ParserType } from './parser-type'; import { isNgbDateStruct } from '../../../date.util'; +import { environment } from '../../../../../environments/environment'; export const SUBMISSION_ID: InjectionToken = new InjectionToken('submissionId'); export const CONFIG_DATA: InjectionToken = new InjectionToken('configData'); @@ -26,6 +27,11 @@ export const PARSER_OPTIONS: InjectionToken = new InjectionToken< export abstract class FieldParser { protected fieldId: string; + /** + * This is the field to use for type binding + * @protected + */ + protected typeField: string; constructor( @Inject(SUBMISSION_ID) protected submissionId: string, @@ -33,6 +39,8 @@ export abstract class FieldParser { @Inject(INIT_FORM_VALUES) protected initFormValues: any, @Inject(PARSER_OPTIONS) protected parserOptions: ParserOptions ) { + // Replace . with _ in configured type field here, to make configuration more simple and user-friendly + this.typeField = environment.submission.typeBind.field.replace('\.', '_'); } public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any; @@ -315,14 +323,14 @@ export abstract class FieldParser { const bindValues = []; configuredTypeBindValues.forEach((value) => { bindValues.push({ - id: 'dc_type', + id: this.typeField, value: value }); }); // match: MATCH_VISIBLE means that if true, the field / component will be visible // operator: OR means that all the values in the 'when' condition will be compared with OR, not AND // when: the list of values to match against, in this case the list of strings from ... - // Example: Field [x] will be VISIBLE if dc_type = book OR dc_type = book_part + // Example: Field [x] will be VISIBLE if item type = book OR item type = book_part // // The opposing match value will be the dc.type for the workspace item return [{ diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index dc54c2fcb0..7a4a5047ba 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -110,6 +110,9 @@ export class DefaultAppConfig implements AppConfig { */ timer: 0 }, + typeBind: { + field: 'dc.type' + }, icons: { metadata: [ /** diff --git a/src/config/submission-config.interface.ts b/src/config/submission-config.interface.ts index ce275b9bf8..a63af45e38 100644 --- a/src/config/submission-config.interface.ts +++ b/src/config/submission-config.interface.ts @@ -5,6 +5,10 @@ interface AutosaveConfig extends Config { timer: number; } +interface TypeBindConfig extends Config { + field: string; +} + interface IconsConfig extends Config { metadata: MetadataIconConfig[]; authority: { @@ -24,5 +28,6 @@ export interface ConfidenceIconConfig extends Config { export interface SubmissionConfig extends Config { autosave: AutosaveConfig; + typeBind: TypeBindConfig; icons: IconsConfig; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 7c24ef8f05..95237f4b7c 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -100,6 +100,9 @@ export const environment: BuildConfig = { // NOTE: every how many minutes submission is saved automatically timer: 5 }, + typeBind: { + field: 'dc.type' + }, icons: { metadata: [ { From 61d64d5e5ea0bbaa42117b1531cd76d9307e353e Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 17 Feb 2022 14:35:44 +1300 Subject: [PATCH 14/87] [TLC-254] Updating references in tests to reflect row array model changes --- .../ds-dynamic-form-control-container.component.spec.ts | 1 - src/app/shared/form/builder/form-builder.service.spec.ts | 7 +++++-- .../sections/form/section-form-operations.service.spec.ts | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index 785b0958d5..34d23ef719 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -150,7 +150,6 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { submissionId: '1234', id: 'relationGroup', formConfiguration: [], - isInlineGroup: false, mandatoryField: '', name: 'relationGroup', relationFields: [], diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 4055c84921..fe9d0a7bc6 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -197,7 +197,7 @@ describe('FormBuilderService test suite', () => { repeatable: false, metadataFields: [], submissionId: '1234', - hasSelectableMetadata: false + hasSelectableMetadata: false, }), new DynamicScrollableDropdownModel({ @@ -233,6 +233,7 @@ describe('FormBuilderService test suite', () => { hints: 'Enter the name of the author.', input: { type: 'onebox' }, label: 'Authors', + typeBind: [], languageCodes: [], mandatory: 'true', mandatoryMessage: 'Required field!', @@ -304,7 +305,9 @@ describe('FormBuilderService test suite', () => { required: false, metadataKey: 'dc.contributor.author', metadataFields: ['dc.contributor.author'], - hasSelectableMetadata: true + hasSelectableMetadata: true, + showButtons: true, + typeBindRelations: [] }, ), ]; diff --git a/src/app/submission/sections/form/section-form-operations.service.spec.ts b/src/app/submission/sections/form/section-form-operations.service.spec.ts index d5798b82c8..65ddbe0cb0 100644 --- a/src/app/submission/sections/form/section-form-operations.service.spec.ts +++ b/src/app/submission/sections/form/section-form-operations.service.spec.ts @@ -814,7 +814,9 @@ describe('SectionFormOperationsService test suite', () => { required: false, metadataKey: 'dc.contributor.author', metadataFields: ['dc.contributor.author'], - hasSelectableMetadata: true + hasSelectableMetadata: true, + showButtons: true, + typeBindRelations: [] } ); spyOn(serviceAsAny, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); From f97a87702ccfa7f2cb95acb04b1a89a7403ca40f Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 25 Feb 2022 12:21:22 +1300 Subject: [PATCH 15/87] [TLC-254] Hidden fields can collapse in row, revert drag-handle problem apply d-none class to form-control container when model is hidden css rule for ds-form-control-container.d-none forces collapse drag-handle fix from 9019b809 was recent and is applied manually here too --- .../ds-dynamic-form-ui/ds-dynamic-form.component.html | 1 + .../models/array-group/dynamic-form-array.component.html | 5 +++-- .../models/form-group/dynamic-form-group.component.html | 1 + src/styles/_global-styles.scss | 7 +++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html index 2a18565178..4c1ea2dd96 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html @@ -3,6 +3,7 @@ [group]="formGroup" [hasErrorMessaging]="model.hasErrorMessages" [hidden]="model.hidden" + [class.d-none]="model.hidden" [layout]="formLayout" [model]="model" [templates]="templates" diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html index 29df7a34c4..d518d59da2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html @@ -17,8 +17,8 @@ [cdkDragPreviewClass]="'ds-submission-reorder-dragging'" [class.grey-background]="model.isInlineGroupArray"> -
- +
+
+
From c2f57b448db1bc4716378e8ffcbb67f47390355d Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 Apr 2022 14:28:35 +1200 Subject: [PATCH 39/87] [TLC-254] Ensure matchers injected in testbed providers --- .../ds-dynamic-type-bind-relation.service.spec.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts index c65e3c6574..1a1f27c0b4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts @@ -8,7 +8,7 @@ import { OR_OPERATOR, HIDDEN_MATCHER, DISABLED_MATCHER, - REQUIRED_MATCHER, + REQUIRED_MATCHER, HIDDEN_MATCHER_PROVIDER, REQUIRED_MATCHER_PROVIDER, DISABLED_MATCHER_PROVIDER, } from '@ng-dynamic-forms/core'; import { @@ -34,7 +34,8 @@ describe('DSDynamicTypeBindRelationService test suite', () => { { provide: FormBuilderService, useValue: getMockFormBuilderService() }, { provide: DsDynamicTypeBindRelationService, useClass: DsDynamicTypeBindRelationService }, { provide: DynamicFormRelationService }, - { provide: Injector } + { provide: Injector }, + DISABLED_MATCHER_PROVIDER, HIDDEN_MATCHER_PROVIDER, REQUIRED_MATCHER_PROVIDER ] }).compileComponents().then(); }); @@ -64,7 +65,6 @@ describe('DSDynamicTypeBindRelationService test suite', () => { 'boundType', null, 'bound-auth-key', 'Bound Type' ); const bindType = service.getTypeBindValue(mockMetadataValueObject); - console.dir(bindType); expect(bindType).toBe('bound-auth-key'); }); it('Should get passed string returned directly as string passed instead of metadata', () => { @@ -97,7 +97,8 @@ describe('DSDynamicTypeBindRelationService test suite', () => { testModel.typeBindRelations = getTypeBindRelations(['boundType']); const dcTypeControl = new FormControl(); dcTypeControl.setValue('boundType'); - expect(service.subscribeRelations(testModel, dcTypeControl)).toHaveSize(1); + let subscriptions = service.subscribeRelations(testModel, dcTypeControl); + expect(subscriptions).toHaveSize(1); }); it('Expect hasMatch to be true (ie. this should be hidden)', () => { From 8cd07de4fc39b61f990e6c3fc892e7d93c945cbd Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 Apr 2022 14:58:52 +1200 Subject: [PATCH 40/87] [TLC-254] Remove chaff from test --- .../ds-dynamic-type-bind-relation.service.spec.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts index 1a1f27c0b4..f8bc7ea886 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts @@ -1,14 +1,12 @@ -import {inject, TestBed, waitForAsync} from '@angular/core/testing'; +import {inject, TestBed} from '@angular/core/testing'; import { DynamicFormControlRelation, - DynamicFormControlMatcher, DynamicFormRelationService, MATCH_VISIBLE, OR_OPERATOR, HIDDEN_MATCHER, - DISABLED_MATCHER, - REQUIRED_MATCHER, HIDDEN_MATCHER_PROVIDER, REQUIRED_MATCHER_PROVIDER, DISABLED_MATCHER_PROVIDER, + HIDDEN_MATCHER_PROVIDER, REQUIRED_MATCHER_PROVIDER, DISABLED_MATCHER_PROVIDER, } from '@ng-dynamic-forms/core'; import { @@ -24,7 +22,6 @@ import {Injector} from '@angular/core'; describe('DSDynamicTypeBindRelationService test suite', () => { let service: DsDynamicTypeBindRelationService; let dynamicFormRelationService: DynamicFormRelationService; - let dynamicFormControlMatchers: DynamicFormControlMatcher[]; let injector: Injector; beforeEach(() => { @@ -34,7 +31,6 @@ describe('DSDynamicTypeBindRelationService test suite', () => { { provide: FormBuilderService, useValue: getMockFormBuilderService() }, { provide: DsDynamicTypeBindRelationService, useClass: DsDynamicTypeBindRelationService }, { provide: DynamicFormRelationService }, - { provide: Injector }, DISABLED_MATCHER_PROVIDER, HIDDEN_MATCHER_PROVIDER, REQUIRED_MATCHER_PROVIDER ] }).compileComponents().then(); @@ -43,13 +39,9 @@ describe('DSDynamicTypeBindRelationService test suite', () => { beforeEach(inject([DsDynamicTypeBindRelationService, DynamicFormRelationService], (relationService: DsDynamicTypeBindRelationService, formRelationService: DynamicFormRelationService, - mockInjector: Injector, ) => { service = relationService; dynamicFormRelationService = formRelationService; - dynamicFormControlMatchers = []; - injector = mockInjector; - dynamicFormControlMatchers = [HIDDEN_MATCHER, REQUIRED_MATCHER, DISABLED_MATCHER]; })); describe('Test getTypeBindValue method', () => { From aa78a2991cf7185f6776b2096e81ff62311ec62d Mon Sep 17 00:00:00 2001 From: Rezart Vata Date: Wed, 27 Apr 2022 20:18:33 +0200 Subject: [PATCH 41/87] CST-5253] Finished functionalities --- src/app/app-routing-paths.ts | 6 +++ .../resolver/submission-object.resolver.ts | 43 +++++++++++++++++++ .../claimed-task-actions.component.html | 26 +++++------ .../pool-task-actions.component.html | 19 +++++--- .../pool-task/pool-task-actions.component.ts | 28 +++++++----- .../workspaceitem-actions.component.html | 38 +++++++++------- .../workspaceitem-actions.component.ts | 20 ++++++--- .../item-from-workflow.resolver.ts | 30 ++----------- .../item-from-workspace.resolver.ts | 21 +++++++++ .../workspaceitems-edit-page-routing-paths.ts | 8 ++++ ...workspaceitems-edit-page-routing.module.ts | 14 +++++- .../workspaceitems-edit-page.module.ts | 4 +- src/assets/i18n/en.json5 | 7 +++ 13 files changed, 183 insertions(+), 81 deletions(-) create mode 100644 src/app/core/submission/resolver/submission-object.resolver.ts create mode 100644 src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts create mode 100644 src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing-paths.ts diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 57767b6f3e..6524edef77 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -70,6 +70,12 @@ export function getWorkflowItemModuleRoute() { return `/${WORKFLOW_ITEM_MODULE_PATH}`; } +export const WORKSPACE_ITEM_MODULE_PATH = 'workspaceitems'; + +export function getWorkspaceItemModuleRoute() { + return `/${WORKSPACE_ITEM_MODULE_PATH}`; +} + export function getDSORoute(dso: DSpaceObject): string { if (hasValue(dso)) { switch ((dso as any).type) { diff --git a/src/app/core/submission/resolver/submission-object.resolver.ts b/src/app/core/submission/resolver/submission-object.resolver.ts new file mode 100644 index 0000000000..32f6c544e2 --- /dev/null +++ b/src/app/core/submission/resolver/submission-object.resolver.ts @@ -0,0 +1,43 @@ +import { DSpaceObject } from './../../shared/dspace-object.model'; +import { followLink } from './../../../shared/utils/follow-link-config.model'; +import { ChildHALResource } from './../../shared/child-hal-resource.model'; +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { switchMap } from 'rxjs/operators'; +import { DataService } from '../../data/data.service'; +import { RemoteData } from '../../data/remote-data'; +import { getFirstCompletedRemoteData } from '../../shared/operators'; + +/** + * This class represents a resolver that requests a specific item before the route is activated + */ +@Injectable() +export class SubmissionObjectResolver implements Resolve> { + constructor( + protected dataService: DataService, + protected store: Store + ) { + } + + /** + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + const itemRD$ = this.dataService.findById(route.params.id, + true, + false, + followLink('item'), + ).pipe( + getFirstCompletedRemoteData(), + switchMap((wfiRD: RemoteData) => wfiRD.payload.item as Observable>), + getFirstCompletedRemoteData() + ); + return itemRD$; + } +} diff --git a/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html b/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html index 4ad6665cf8..0b2398791e 100644 --- a/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html +++ b/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html @@ -1,22 +1,18 @@
- + - - - + + + - +
-
+ \ No newline at end of file diff --git a/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.html b/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.html index 214f85ed5b..c2f0158c12 100644 --- a/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.html +++ b/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.html @@ -1,8 +1,13 @@ - + \ No newline at end of file diff --git a/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.ts b/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.ts index 92086ac817..45f51a5d4a 100644 --- a/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.ts +++ b/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.ts @@ -2,7 +2,7 @@ import { Component, Injector, Input, OnDestroy } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs'; -import {filter, map, switchMap, take} from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; @@ -19,6 +19,7 @@ import { Item } from '../../../core/shared/item.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { MyDSpaceReloadableActionsComponent } from '../mydspace-reloadable-actions'; import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response'; +import { getWorkflowItemViewRoute } from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths'; /** * This component represents mydspace actions related to PoolTask object. @@ -58,12 +59,12 @@ export class PoolTaskActionsComponent extends MyDSpaceReloadableActionsComponent * @param {RequestService} requestService */ constructor(protected injector: Injector, - protected router: Router, - protected notificationsService: NotificationsService, - protected claimedTaskService: ClaimedTaskDataService, - protected translate: TranslateService, - protected searchService: SearchService, - protected requestService: RequestService) { + protected router: Router, + protected notificationsService: NotificationsService, + protected claimedTaskService: ClaimedTaskDataService, + protected translate: TranslateService, + protected searchService: SearchService, + protected requestService: RequestService) { super(PoolTask.type, injector, router, notificationsService, translate, searchService, requestService); } @@ -91,7 +92,7 @@ export class PoolTaskActionsComponent extends MyDSpaceReloadableActionsComponent return this.objectDataService.getPoolTaskEndpointById(this.object.id) .pipe(switchMap((poolTaskHref) => { return this.claimedTaskService.claimTask(this.object.id, poolTaskHref); - })); + })); } reloadObjectExecution(): Observable | DSpaceObject> { @@ -107,12 +108,19 @@ export class PoolTaskActionsComponent extends MyDSpaceReloadableActionsComponent switchMap((workflowItem: WorkflowItem) => workflowItem.item.pipe(getFirstSucceededRemoteDataPayload()) )) .subscribe((item: Item) => { - this.itemUuid = item.uuid; - }); + this.itemUuid = item.uuid; + }); } ngOnDestroy() { this.subs.forEach((sub) => sub.unsubscribe()); } + /** + * Get the workflowitem view route. + */ + getWorkflowItemViewRoute(workflowitem: WorkflowItem): string { + return getWorkflowItemViewRoute(workflowitem?.id); + } + } diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html index a43c9f8d17..8b6ad87ca2 100644 --- a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html @@ -1,22 +1,28 @@
- + + + + {{'submission.workflow.generic.edit' | translate}} -
+ - + \ No newline at end of file diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts index a25ce335e3..a6d30728ac 100644 --- a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts @@ -14,6 +14,7 @@ import { SearchService } from '../../../core/shared/search/search.service'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { NoContent } from '../../../core/shared/NoContent.model'; +import { getWorkspaceItemViewRoute } from '../../../workspaceitems-edit-page/workspaceitems-edit-page-routing-paths'; /** * This component represents actions related to WorkspaceItem object. @@ -48,12 +49,12 @@ export class WorkspaceitemActionsComponent extends MyDSpaceActionsComponent> { +export class ItemFromWorkflowResolver extends SubmissionObjectResolver implements Resolve> { constructor( private workflowItemService: WorkflowItemDataService, protected store: Store ) { + super(workflowItemService, store); } - /** - * Method for resolving an item based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const itemRD$ = this.workflowItemService.findById(route.params.id, - true, - false, - followLink('item'), - ).pipe( - getFirstCompletedRemoteData(), - switchMap((wfiRD: RemoteData) => wfiRD.payload.item as Observable>), - getFirstCompletedRemoteData() - ); - return itemRD$; - } } diff --git a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts new file mode 100644 index 0000000000..60e1fe6a87 --- /dev/null +++ b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { RemoteData } from '../core/data/remote-data'; +import { Item } from '../core/shared/item.model'; +import { Store } from '@ngrx/store'; +import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; + +/** + * This class represents a resolver that requests a specific item before the route is activated + */ +@Injectable() +export class ItemFromWorkspaceResolver extends SubmissionObjectResolver implements Resolve> { + constructor( + private workspaceItemService: WorkspaceitemDataService, + protected store: Store + ) { + super(workspaceItemService, store); + } + +} diff --git a/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing-paths.ts b/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing-paths.ts new file mode 100644 index 0000000000..74917b4392 --- /dev/null +++ b/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing-paths.ts @@ -0,0 +1,8 @@ +import { getWorkspaceItemModuleRoute } from '../app-routing-paths'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; + +export function getWorkspaceItemViewRoute(wfiId: string) { + return new URLCombiner(getWorkspaceItemModuleRoute(), wfiId, WORKSPACE_ITEM_VIEW_PATH).toString(); +} + +export const WORKSPACE_ITEM_VIEW_PATH = 'view'; diff --git a/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts b/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts index 1a58417d0c..10c2f2a1d0 100644 --- a/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts +++ b/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts @@ -4,6 +4,8 @@ import { RouterModule } from '@angular/router'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component'; +import { ItemFromWorkspaceResolver } from './item-from-workspace.resolver'; @NgModule({ imports: [ @@ -17,7 +19,17 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso breadcrumb: I18nBreadcrumbResolver }, data: { title: 'submission.edit.title', breadcrumbKey: 'submission.edit' } - } + }, + { + canActivate: [AuthenticatedGuard], + path: ':id/view', + component: ThemedFullItemPageComponent, + resolve: { + dso: ItemFromWorkspaceResolver, + breadcrumb: I18nBreadcrumbResolver + }, + data: { title: 'workspace-item.view.title', breadcrumbKey: 'workspace-item.view' } + }, ]) ] }) diff --git a/src/app/workspaceitems-edit-page/workspaceitems-edit-page.module.ts b/src/app/workspaceitems-edit-page/workspaceitems-edit-page.module.ts index 65a40f3f7c..83f869881a 100644 --- a/src/app/workspaceitems-edit-page/workspaceitems-edit-page.module.ts +++ b/src/app/workspaceitems-edit-page/workspaceitems-edit-page.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '../shared/shared.module'; import { WorkspaceitemsEditPageRoutingModule } from './workspaceitems-edit-page-routing.module'; import { SubmissionModule } from '../submission/submission.module'; +import { ItemFromWorkspaceResolver } from './item-from-workspace.resolver'; @NgModule({ imports: [ @@ -11,7 +12,8 @@ import { SubmissionModule } from '../submission/submission.module'; SharedModule, SubmissionModule, ], - declarations: [] + declarations: [], + providers: [ItemFromWorkspaceResolver] }) /** * This module handles all modules that need to access the workspaceitems edit page. diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index a7ce942e7d..b2c8aaf482 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -4054,6 +4054,10 @@ "submission.workflow.tasks.pool.show-detail": "Show detail", + "submission.workspace.generic.view": "View", + + "submission.workspace.generic.view-help": "Select this option to view the item's metadata.", + "thumbnail.default.alt": "Thumbnail Image", @@ -4160,6 +4164,9 @@ "workflow-item.view.breadcrumbs": "Workflow View", + "workspace-item.view.breadcrumbs": "Workspace View", + + "workspace-item.view.title": "Workspace View", "idle-modal.header": "Session will expire soon", From b83e87dd4ee5dce2c55b404f2dcb651f4f3fef31 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 28 Apr 2022 10:38:45 +0530 Subject: [PATCH 42/87] [CST-5676] Bitstream Breadcrumb Resolver --- .../bitstream-breadcrumb.resolver.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts index 8d658f7d6e..67c8c092c3 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { Bitstream } from '../shared/bitstream.model'; import { BitstreamDataService } from '../data/bitstream-data.service'; import { BITSTREAM_PAGE_LINKS_TO_FOLLOW, BUNDLE_PAGE_LINKS_TO_FOLLOW } from 'src/app/bitstream-page/bitstream-page.resolver'; @@ -23,9 +23,13 @@ import { ITEM_PAGE_LINKS_TO_FOLLOW } from 'src/app/item-page/item.resolver'; @Injectable({ providedIn: 'root' }) -export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: BitstreamDataService, protected itemService: ItemDataService) { - super(breadcrumbService, dataService); +export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { + constructor( + protected breadcrumbService: DSOBreadcrumbsService, + protected dataService: BitstreamDataService, + protected itemService: ItemDataService + ) { + super(breadcrumbService, itemService); } /** @@ -34,7 +38,7 @@ export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver> { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { const uuid = route.params.id; return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe( getFirstCompletedRemoteData(), @@ -45,12 +49,6 @@ export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { - this.itemService.findById(res.uuid, true, false, ...this.bfollowLinks).pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload() - ).subscribe(bres => { - console.log(bres); - }); const url = res._links.item.href; return {provider: this.breadcrumbService, key: of(res), url: url}; }); @@ -67,7 +65,7 @@ export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver[] { - return BITSTREAM_PAGE_LINKS_TO_FOLLOW; + return [followLink('bundle', followLink('item'))]; } get bfollowLinks(): FollowLinkConfig[] { From b8904f5fe1de67d989a192de11b91956fc2bb645 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 28 Apr 2022 16:35:42 +0530 Subject: [PATCH 43/87] [CST-5676] Resolver Changes Item Undefined --- .../bitstream-breadcrumb.resolver.ts | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts index 67c8c092c3..ef23a2819b 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts @@ -8,7 +8,7 @@ import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable, of } from 'rxjs'; import { BreadcrumbConfig } from 'src/app/breadcrumbs/breadcrumb/breadcrumb-config.model'; import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators'; -import { map } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { hasValue } from 'src/app/shared/empty.util'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { BundleDataService } from '../data/bundle-data.service'; @@ -16,6 +16,7 @@ import { Bundle } from '../shared/bundle.model'; import { ItemDataService } from '../data/item-data.service'; import { Item } from '../shared/item.model'; import { ITEM_PAGE_LINKS_TO_FOLLOW } from 'src/app/item-page/item.resolver'; +import { DSpaceObject } from '../shared/dspace-object.model'; /** * The class that resolves the BreadcrumbConfig object for an Item @@ -26,10 +27,10 @@ import { ITEM_PAGE_LINKS_TO_FOLLOW } from 'src/app/item-page/item.resolver'; export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { constructor( protected breadcrumbService: DSOBreadcrumbsService, - protected dataService: BitstreamDataService, - protected itemService: ItemDataService + protected bitstreamService: BitstreamDataService, + protected dataService: ItemDataService ) { - super(breadcrumbService, itemService); + super(breadcrumbService, dataService); } /** @@ -40,20 +41,37 @@ export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { const uuid = route.params.id; - return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe( + return this.bitstreamService.findById(uuid, true, false, ...this.bfollowLinks).pipe( getFirstCompletedRemoteData(), getRemoteDataPayload(), - map((object: Bitstream) => { - if (hasValue(object)) { - object.bundle.pipe( + switchMap((bitstream: Bitstream) => { + if (hasValue(bitstream)) { + return bitstream.bundle.pipe( getFirstCompletedRemoteData(), - getRemoteDataPayload() - ).subscribe(res => { - const url = res._links.item.href; - return {provider: this.breadcrumbService, key: of(res), url: url}; - }); + getRemoteDataPayload(), + switchMap((bundle: Bundle) => { + if (hasValue(bundle)) { + return bundle.item.pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + map((item: Item) => { + if (hasValue(item)) { + console.log(item); + const fullPath = state.url; + const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; + return {provider: this.breadcrumbService, key: item, url: url}; + } else { + return undefined; + } + }) + ); + } else { + return of(undefined); + } + }) + ); } else { - return undefined; + return of(undefined); } }) ); @@ -64,11 +82,11 @@ export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { * The self links defined in this list are expected to be requested somewhere in the near future * Requesting them as embeds will limit the number of requests */ - get followLinks(): FollowLinkConfig[] { - return [followLink('bundle', followLink('item'))]; - } - - get bfollowLinks(): FollowLinkConfig[] { + get followLinks(): FollowLinkConfig[] { return ITEM_PAGE_LINKS_TO_FOLLOW; } + + get bfollowLinks(): FollowLinkConfig[] { + return [followLink('bundle', followLink('item'))]; + } } From 393a4a3d4075764c1362a25856b4aba2b78d69d1 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 28 Apr 2022 15:41:25 +0200 Subject: [PATCH 44/87] [CST-5676] Implemented bitstream-breadcrumbs.service --- .../bitstream-page-routing.module.ts | 27 +++--- .../bitstream-page/bitstream-page.resolver.ts | 2 +- .../bitstream-breadcrumb.resolver.ts | 77 ++--------------- .../bitstream-breadcrumbs.service.ts | 85 +++++++++++++++++++ .../breadcrumbs/dso-breadcrumbs.service.ts | 4 +- 5 files changed, 109 insertions(+), 86 deletions(-) create mode 100644 src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts diff --git a/src/app/bitstream-page/bitstream-page-routing.module.ts b/src/app/bitstream-page/bitstream-page-routing.module.ts index 5da9135846..0bdda29ddf 100644 --- a/src/app/bitstream-page/bitstream-page-routing.module.ts +++ b/src/app/bitstream-page/bitstream-page-routing.module.ts @@ -11,6 +11,8 @@ import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/re import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component'; import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; import { BitstreamBreadcrumbResolver } from '../core/breadcrumbs/bitstream-breadcrumb.resolver'; +import { BitstreamBreadcrumbsService } from '../core/breadcrumbs/bitstream-breadcrumbs.service'; +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; const EDIT_BITSTREAM_PATH = ':id/edit'; const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; @@ -26,8 +28,7 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; path: 'handle/:prefix/:suffix/:filename', component: BitstreamDownloadPageComponent, resolve: { - bitstream: LegacyBitstreamUrlResolver, - breadcrumb: BitstreamBreadcrumbResolver + bitstream: LegacyBitstreamUrlResolver }, }, { @@ -35,8 +36,7 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; path: ':prefix/:suffix/:sequence_id/:filename', component: BitstreamDownloadPageComponent, resolve: { - bitstream: LegacyBitstreamUrlResolver, - breadcrumb: BitstreamBreadcrumbResolver + bitstream: LegacyBitstreamUrlResolver }, }, { @@ -44,8 +44,7 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; path: ':id/download', component: BitstreamDownloadPageComponent, resolve: { - bitstream: BitstreamPageResolver, - breadcrumb: BitstreamBreadcrumbResolver + bitstream: BitstreamPageResolver }, }, { @@ -53,7 +52,7 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; component: EditBitstreamPageComponent, resolve: { bitstream: BitstreamPageResolver, - breadcrumb: BitstreamBreadcrumbResolver + breadcrumb: BitstreamBreadcrumbResolver, }, canActivate: [AuthenticatedGuard] }, @@ -64,8 +63,7 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; { path: 'create', resolve: { - resourcePolicyTarget: ResourcePolicyTargetResolver, - breadcrumb: BitstreamBreadcrumbResolver + resourcePolicyTarget: ResourcePolicyTargetResolver }, component: ResourcePolicyCreateComponent, data: { title: 'resource-policies.create.page.title', showBreadcrumbs: true } @@ -73,17 +71,17 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; { path: 'edit', resolve: { - resourcePolicy: ResourcePolicyResolver, - breadcrumb: BitstreamBreadcrumbResolver + breadcrumb: I18nBreadcrumbResolver, + resourcePolicy: ResourcePolicyResolver }, component: ResourcePolicyEditComponent, - data: { title: 'resource-policies.edit.page.title', showBreadcrumbs: true } + data: { breadcrumbKey: 'item.edit', title: 'resource-policies.edit.page.title', showBreadcrumbs: true } }, { path: '', resolve: { bitstream: BitstreamPageResolver, - breadcrumb: BitstreamBreadcrumbResolver + breadcrumb: BitstreamBreadcrumbResolver, }, component: BitstreamAuthorizationsComponent, data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true } @@ -94,7 +92,8 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; ], providers: [ BitstreamPageResolver, - BitstreamBreadcrumbResolver + BitstreamBreadcrumbResolver, + BitstreamBreadcrumbsService ] }) export class BitstreamPageRoutingModule { diff --git a/src/app/bitstream-page/bitstream-page.resolver.ts b/src/app/bitstream-page/bitstream-page.resolver.ts index 5f41cafdeb..4f4f16b24d 100644 --- a/src/app/bitstream-page/bitstream-page.resolver.ts +++ b/src/app/bitstream-page/bitstream-page.resolver.ts @@ -17,7 +17,7 @@ import { Bundle } from '../core/shared/bundle.model'; followLink('parentCommunity', {}, followLink('parentCommunity')) ), - followLink('bundle'), + followLink('bundle', {}, followLink('item')), followLink('thumbnail') ]; diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts index ef23a2819b..b2ddade682 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts @@ -1,22 +1,11 @@ import { Injectable } from '@angular/core'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; + +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { Bitstream } from '../shared/bitstream.model'; import { BitstreamDataService } from '../data/bitstream-data.service'; -import { BITSTREAM_PAGE_LINKS_TO_FOLLOW, BUNDLE_PAGE_LINKS_TO_FOLLOW } from 'src/app/bitstream-page/bitstream-page.resolver'; -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { Observable, of } from 'rxjs'; -import { BreadcrumbConfig } from 'src/app/breadcrumbs/breadcrumb/breadcrumb-config.model'; -import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators'; -import { map, switchMap } from 'rxjs/operators'; -import { hasValue } from 'src/app/shared/empty.util'; +import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; -import { BundleDataService } from '../data/bundle-data.service'; -import { Bundle } from '../shared/bundle.model'; -import { ItemDataService } from '../data/item-data.service'; -import { Item } from '../shared/item.model'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from 'src/app/item-page/item.resolver'; -import { DSpaceObject } from '../shared/dspace-object.model'; +import { BitstreamBreadcrumbsService } from './bitstream-breadcrumbs.service'; /** * The class that resolves the BreadcrumbConfig object for an Item @@ -24,69 +13,19 @@ import { DSpaceObject } from '../shared/dspace-object.model'; @Injectable({ providedIn: 'root' }) -export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { +export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { constructor( - protected breadcrumbService: DSOBreadcrumbsService, - protected bitstreamService: BitstreamDataService, - protected dataService: ItemDataService - ) { + protected breadcrumbService: BitstreamBreadcrumbsService, protected dataService: BitstreamDataService) { super(breadcrumbService, dataService); } - /** - * Method for resolving a breadcrumb config object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BitstreamBreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const uuid = route.params.id; - return this.bitstreamService.findById(uuid, true, false, ...this.bfollowLinks).pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload(), - switchMap((bitstream: Bitstream) => { - if (hasValue(bitstream)) { - return bitstream.bundle.pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload(), - switchMap((bundle: Bundle) => { - if (hasValue(bundle)) { - return bundle.item.pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload(), - map((item: Item) => { - if (hasValue(item)) { - console.log(item); - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; - return {provider: this.breadcrumbService, key: item, url: url}; - } else { - return undefined; - } - }) - ); - } else { - return of(undefined); - } - }) - ); - } else { - return of(undefined); - } - }) - ); - } - /** * Method that returns the follow links to already resolve * The self links defined in this list are expected to be requested somewhere in the near future * Requesting them as embeds will limit the number of requests */ - get followLinks(): FollowLinkConfig[] { - return ITEM_PAGE_LINKS_TO_FOLLOW; + get followLinks(): FollowLinkConfig[] { + return BITSTREAM_PAGE_LINKS_TO_FOLLOW; } - get bfollowLinks(): FollowLinkConfig[] { - return [followLink('bundle', followLink('item'))]; - } } diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts b/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts new file mode 100644 index 0000000000..333886ed3d --- /dev/null +++ b/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of as observableOf } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { DSONameService } from './dso-name.service'; +import { ChildHALResource } from '../shared/child-hal-resource.model'; +import { LinkService } from '../cache/builders/link.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { RemoteData } from '../data/remote-data'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { getDSORoute } from '../../app-routing-paths'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { BitstreamDataService } from '../data/bitstream-data.service'; +import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators'; +import { Bitstream } from '../shared/bitstream.model'; +import { Bundle } from '../shared/bundle.model'; +import { Item } from '../shared/item.model'; +import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; + +/** + * Service to calculate DSpaceObject breadcrumbs for a single part of the route + */ +@Injectable({ + providedIn: 'root' +}) +export class BitstreamBreadcrumbsService extends DSOBreadcrumbsService { + constructor( + protected bitstreamService: BitstreamDataService, + protected linkService: LinkService, + protected dsoNameService: DSONameService + ) { + super(linkService, dsoNameService); + } + + /** + * Method to recursively calculate the breadcrumbs + * This method returns the name and url of the key and all its parent DSOs recursively, top down + * @param key The key (a DSpaceObject) used to resolve the breadcrumb + * @param url The url to use as a link for this breadcrumb + */ + getBreadcrumbs(key: ChildHALResource & DSpaceObject, url: string): Observable { + const label = this.dsoNameService.getName(key); + const crumb = new Breadcrumb(label, url); + + return this.getOwningItem(key.uuid).pipe( + switchMap((parentRD: RemoteData) => { + if (isNotEmpty(parentRD) && hasValue(parentRD.payload)) { + const parent = parentRD.payload; + return super.getBreadcrumbs(parent, getDSORoute(parent)); + } + return observableOf([]); + + }), + map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb]) + ); + } + + getOwningItem(uuid: string): Observable> { + return this.bitstreamService.findById(uuid, true, true, ...BITSTREAM_PAGE_LINKS_TO_FOLLOW).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + switchMap((bitstream: Bitstream) => { + if (hasValue(bitstream)) { + return bitstream.bundle.pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + switchMap((bundle: Bundle) => { + if (hasValue(bundle)) { + return bundle.item.pipe( + getFirstCompletedRemoteData(), + ); + } else { + return observableOf(undefined); + } + }) + ); + } else { + return observableOf(undefined); + } + }) + ); + } +} diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index 23fff18537..a5884ca3c9 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -20,8 +20,8 @@ import { getDSORoute } from '../../app-routing-paths'; }) export class DSOBreadcrumbsService implements BreadcrumbsProviderService { constructor( - private linkService: LinkService, - private dsoNameService: DSONameService + protected linkService: LinkService, + protected dsoNameService: DSONameService ) { } From 3d206165b20303aa56fd2eeadf44451942375d61 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 28 Apr 2022 15:53:48 +0200 Subject: [PATCH 45/87] [CST-5676] Fix bitstream's follow links --- .../bitstream-page/bitstream-page.resolver.ts | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/app/bitstream-page/bitstream-page.resolver.ts b/src/app/bitstream-page/bitstream-page.resolver.ts index 4f4f16b24d..be92041dfc 100644 --- a/src/app/bitstream-page/bitstream-page.resolver.ts +++ b/src/app/bitstream-page/bitstream-page.resolver.ts @@ -6,27 +6,14 @@ import { Bitstream } from '../core/shared/bitstream.model'; import { BitstreamDataService } from '../core/data/bitstream-data.service'; import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; -import { Bundle } from '../core/shared/bundle.model'; /** * The self links defined in this list are expected to be requested somewhere in the near future * Requesting them as embeds will limit the number of requests */ export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('format', {}, - followLink('parentCommunity', {}, - followLink('parentCommunity')) - ), followLink('bundle', {}, followLink('item')), - followLink('thumbnail') -]; - -export const BUNDLE_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('bitstreams', {}, - followLink('parentCommunity', {}, - followLink('parentCommunity')) - ), - followLink('primaryBitstream') + followLink('format') ]; /** @@ -56,9 +43,6 @@ export class BitstreamPageResolver implements Resolve> { * Requesting them as embeds will limit the number of requests */ get followLinks(): FollowLinkConfig[] { - return [ - followLink('bundle', {}, followLink('item')), - followLink('format') - ]; + return BITSTREAM_PAGE_LINKS_TO_FOLLOW; } } From 64f3af7a1b34a63d7e0d57b5e9c3011b12056a9e Mon Sep 17 00:00:00 2001 From: Rezart Vata Date: Thu, 28 Apr 2022 18:35:04 +0200 Subject: [PATCH 46/87] [CST-5253] Added unit testing --- .../item-from-workspace.resolver.spec.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts diff --git a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts new file mode 100644 index 0000000000..c14344d70d --- /dev/null +++ b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts @@ -0,0 +1,36 @@ +import { first } from 'rxjs/operators'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { ItemFromWorkspaceResolver } from './item-from-workspace.resolver'; + +describe('ItemFromWorkspaceResolver', () => { + describe('resolve', () => { + let resolver: ItemFromWorkspaceResolver; + let wfiService: WorkspaceitemDataService; + const uuid = '1234-65487-12354-1235'; + const itemUuid = '8888-8888-8888-8888'; + const wfi = { + id: uuid, + item: createSuccessfulRemoteDataObject$({ id: itemUuid }) + }; + + + beforeEach(() => { + wfiService = { + findById: (id: string) => createSuccessfulRemoteDataObject$(wfi) + } as any; + resolver = new ItemFromWorkspaceResolver(wfiService, null); + }); + + it('should resolve a an item from from the workflow item with the correct id', (done) => { + resolver.resolve({ params: { id: uuid } } as any, undefined) + .pipe(first()) + .subscribe( + (resolved) => { + expect(resolved.payload.id).toEqual(itemUuid); + done(); + } + ); + }); + }); +}); From d32566beda258a5e0d822ae2806b86ff965cfad6 Mon Sep 17 00:00:00 2001 From: Michael Spalti Date: Thu, 28 Apr 2022 12:38:35 -0700 Subject: [PATCH 47/87] Mirador webpack build fix. --- src/mirador-viewer/mirador.html | 1 + webpack/webpack.mirador.config.ts | 21 +++++++++------------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/mirador-viewer/mirador.html b/src/mirador-viewer/mirador.html index 6a9547133c..3cd1e16501 100644 --- a/src/mirador-viewer/mirador.html +++ b/src/mirador-viewer/mirador.html @@ -6,5 +6,6 @@
+ diff --git a/webpack/webpack.mirador.config.ts b/webpack/webpack.mirador.config.ts index 3e04ad6b79..c0083ded6e 100644 --- a/webpack/webpack.mirador.config.ts +++ b/webpack/webpack.mirador.config.ts @@ -1,4 +1,4 @@ -const HtmlWebpackPlugin = require('html-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); const path = require('path'); module.exports = { @@ -10,19 +10,16 @@ module.exports = { path: path.resolve(__dirname, '..' , 'dist/iiif/mirador'), filename: '[name].js' }, - module: { - rules: [ - { - test: /\.html$/i, - loader: 'html-loader', - }, - ], - }, devServer: { contentBase: '../dist/iiif/mirador', }, - plugins: [new HtmlWebpackPlugin({ - filename: 'index.html', - template: './src/mirador-viewer/mirador.html' + resolve: { + fallback: { + url: false + }}, + plugins: [new CopyWebpackPlugin({ + patterns: [ + {from: './src/mirador-viewer/mirador.html', to: './index.html'} + ] })] }; From 2b7ed5c25800662fb3412f074ab2e337bf8d7183 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 29 Apr 2022 09:15:20 +0200 Subject: [PATCH 48/87] [CST-5253] Add view item button for workflow items --- .../workflowitem/workflowitem-actions.component.html | 5 +++++ .../workflowitem/workflowitem-actions.component.spec.ts | 7 +++++++ .../workflowitem/workflowitem-actions.component.ts | 8 ++++++++ 3 files changed, 20 insertions(+) diff --git a/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.html b/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.html index e69de29bb2..f6e5fecb50 100644 --- a/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.html +++ b/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.html @@ -0,0 +1,5 @@ + diff --git a/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.spec.ts b/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.spec.ts index 046eb2f018..79aece892e 100644 --- a/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.spec.ts +++ b/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.spec.ts @@ -18,6 +18,7 @@ import { getMockRequestService } from '../../mocks/request.service.mock'; import { RequestService } from '../../../core/data/request.service'; import { getMockSearchService } from '../../mocks/search-service.mock'; import { SearchService } from '../../../core/shared/search/search.service'; +import { By } from '@angular/platform-browser'; let component: WorkflowitemActionsComponent; let fixture: ComponentFixture; @@ -105,4 +106,10 @@ describe('WorkflowitemActionsComponent', () => { expect(component.object).toEqual(mockObject); }); + it('should display view button', () => { + const btn = fixture.debugElement.query(By.css('button [data-test="view-btn"]')); + + expect(btn).toBeDefined(); + }); + }); diff --git a/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.ts b/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.ts index 62a23ba66e..3587356642 100644 --- a/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.ts +++ b/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.ts @@ -9,6 +9,7 @@ import { WorkflowItemDataService } from '../../../core/submission/workflowitem-d import { NotificationsService } from '../../notifications/notifications.service'; import { RequestService } from '../../../core/data/request.service'; import { SearchService } from '../../../core/shared/search/search.service'; +import { getWorkflowItemViewRoute } from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths'; /** * This component represents actions related to WorkflowItem object. @@ -44,6 +45,13 @@ export class WorkflowitemActionsComponent extends MyDSpaceActionsComponent Date: Fri, 29 Apr 2022 09:16:03 +0200 Subject: [PATCH 49/87] [CST-5253] Fix detail visualization for archived item in the mydspace result list --- .../item-search-result-detail-element.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.ts index 7e611ec3c8..27a94b0cf5 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.ts @@ -3,9 +3,12 @@ import { Component } from '@angular/core'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { Item } from '../../../../core/shared/item.model'; import { SearchResultDetailElementComponent } from '../search-result-detail-element.component'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +import { + MyDspaceItemStatusType +} from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import { Context } from '../../../../core/shared/context.model'; /** * This component renders item object for the search result in the detail view. @@ -16,7 +19,8 @@ import { ItemSearchResult } from '../../../object-collection/shared/item-search- templateUrl: './item-search-result-detail-element.component.html' }) -@listableObjectComponent(Item, ViewMode.DetailedListElement) +@listableObjectComponent(ItemSearchResult, ViewMode.DetailedListElement, Context.Workspace) +@listableObjectComponent(ItemSearchResult, ViewMode.DetailedListElement, Context.Workflow) export class ItemSearchResultDetailElementComponent extends SearchResultDetailElementComponent { /** From d3ef3d30790a4ddb4a7ccf1ac5cf5e713c4e4e35 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 29 Apr 2022 09:17:17 +0200 Subject: [PATCH 50/87] [CST-5253] Change full-item-page.component in order to display also workspace items --- .../full/full-item-page.component.html | 28 ++++++++++--------- .../full/full-item-page.component.spec.ts | 4 +-- .../full/full-item-page.component.ts | 8 +++--- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/app/item-page/full/full-item-page.component.html b/src/app/item-page/full/full-item-page.component.html index 7cc8ff92c4..042be3d8ce 100644 --- a/src/app/item-page/full/full-item-page.component.html +++ b/src/app/item-page/full/full-item-page.component.html @@ -12,26 +12,28 @@ [tooltipMsg]="'item.page.edit'">
-