+ class="btn btn-outline-primary btn-sm" role="button" tabindex="0" data-test="show-more-button">
{{ 'communityList.showMore' | translate }}
diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts
index 03b6bc11910..802d61d169c 100644
--- a/src/app/core/auth/auth.actions.ts
+++ b/src/app/core/auth/auth.actions.ts
@@ -69,8 +69,16 @@ export class AuthenticatedAction implements Action {
public type: string = AuthActionTypes.AUTHENTICATED;
payload: AuthTokenInfo;
- constructor(token: AuthTokenInfo) {
+ /**
+ * Whether we should consider the given authentication info final.
+ * If the backend restarted we may have a token that hasn't expired yet, but it will be invalid anyway.
+ * In this case we'll have to check twice.
+ */
+ checkAgain: boolean;
+
+ constructor(token: AuthTokenInfo, checkAgain = false) {
this.payload = token;
+ this.checkAgain = checkAgain;
}
}
diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts
index a423455594a..fe27f4118d0 100644
--- a/src/app/core/auth/auth.effects.spec.ts
+++ b/src/app/core/auth/auth.effects.spec.ts
@@ -156,7 +156,7 @@ describe('AuthEffects', () => {
describe('when token is valid', () => {
it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => {
- actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATED, payload: token } });
+ actions = hot('--a-', { a: new AuthenticatedAction(token) });
const expected = cold('--b-', { b: new AuthenticatedSuccessAction(true, token, EPersonMock._links.self.href) });
@@ -164,17 +164,29 @@ describe('AuthEffects', () => {
});
});
- describe('when token is not valid', () => {
+ describe('when token is expired', () => {
it('should return a AUTHENTICATED_ERROR action in response to a AUTHENTICATED action', () => {
spyOn((authEffects as any).authService, 'authenticatedUser').and.returnValue(observableThrow(new Error('Message Error test')));
- actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATED, payload: token } });
+ actions = hot('--a-', { a: new AuthenticatedAction(token) });
const expected = cold('--b-', { b: new AuthenticatedErrorAction(new Error('Message Error test')) });
expect(authEffects.authenticated$).toBeObservable(expected);
});
});
+
+ describe('when token is not valid but also not expired (~ cookie)', () => {
+ it('should return a AUTHENTICATED_ERROR action in response to a AUTHENTICATED action', () => {
+ spyOn((authEffects as any).authService, 'authenticatedUser').and.returnValue(observableThrow(new Error('Message Error test')));
+
+ actions = hot('--a-', { a: new AuthenticatedAction(token, true) });
+
+ const expected = cold('--b-', { b: new CheckAuthenticationTokenCookieAction() });
+
+ expect(authEffects.authenticated$).toBeObservable(expected);
+ });
+ });
});
describe('authenticatedSuccess$', () => {
@@ -210,7 +222,7 @@ describe('AuthEffects', () => {
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN } });
- const expected = cold('--b-', { b: new AuthenticatedAction(token) });
+ const expected = cold('--b-', { b: new AuthenticatedAction(token, true) });
expect(authEffects.checkToken$).toBeObservable(expected);
});
diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts
index 2919a40fa85..148a5d7dfc7 100644
--- a/src/app/core/auth/auth.effects.ts
+++ b/src/app/core/auth/auth.effects.ts
@@ -121,7 +121,13 @@ export class AuthEffects {
switchMap((action: AuthenticatedAction) => {
return this.authService.authenticatedUser(action.payload).pipe(
map((userHref: string) => new AuthenticatedSuccessAction((userHref !== null), action.payload, userHref)),
- catchError((error: unknown) => errorToAuthAction$(AuthenticatedErrorAction, error)),
+ catchError((error: unknown) => {
+ if (action.checkAgain) {
+ return observableOf(new CheckAuthenticationTokenCookieAction());
+ } else {
+ return errorToAuthAction$(AuthenticatedErrorAction, error);
+ }
+ }),
);
}),
));
@@ -176,7 +182,7 @@ export class AuthEffects {
public checkToken$: Observable
= createEffect(() => this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN),
switchMap(() => {
return this.authService.hasValidAuthenticationToken().pipe(
- map((token: AuthTokenInfo) => new AuthenticatedAction(token)),
+ map((token: AuthTokenInfo) => new AuthenticatedAction(token, true)),
catchError((error: unknown) => observableOf(new CheckAuthenticationTokenCookieAction())),
);
}),
diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts
index ee6a5677da0..24c3aac8bbf 100644
--- a/src/app/core/data/collection-data.service.ts
+++ b/src/app/core/data/collection-data.service.ts
@@ -66,7 +66,47 @@ export class CollectionDataService extends ComColDataService {
}
/**
- * Get all collections the user is authorized to submit to
+ * Get all collections the user is admin of
+ *
+ * @param query limit the returned collection to those with metadata values
+ * matching the query terms.
+ * @param options The [[FindListOptions]] object
+ * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
+ * no valid cached version. Defaults to true
+ * @param reRequestOnStale Whether or not the request should automatically be re-
+ * requested after the response becomes stale
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
+ * @return Observable>>
+ * collection list
+ */
+ getAdminAuthorizedCollection(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ const searchHref = 'findAdminAuthorized';
+ return this.getAuthorizedCollection(query, options, useCachedVersionIfAvailable, reRequestOnStale, searchHref, ...linksToFollow);
+ }
+
+ /**
+ * Get all collections the user is authorized to edit
+ *
+ * @param query limit the returned collection to those with metadata values
+ * matching the query terms.
+ * @param options The [[FindListOptions]] object
+ * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
+ * no valid cached version. Defaults to true
+ * @param reRequestOnStale Whether or not the request should automatically be re-
+ * requested after the response becomes stale
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
+ * @return Observable>>
+ * collection list
+ */
+ getEditAuthorizedCollection(query: string,options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ const searchHref = 'findEditAuthorized';
+ return this.getAuthorizedCollection(query, options, useCachedVersionIfAvailable, reRequestOnStale, searchHref, ...linksToFollow);
+ }
+
+ /**
+ * Get all collections the user is authorized to submit
*
* @param query limit the returned collection to those with metadata values
* matching the query terms.
@@ -77,7 +117,27 @@ export class CollectionDataService extends ComColDataService {
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
+ * @return Observable>>
+ * collection list
+ */
+ getSubmitAuthorizedCollection(query: string,options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ const searchHref = 'findSubmitAuthorized';
+ return this.getAuthorizedCollection(query, options, useCachedVersionIfAvailable, reRequestOnStale, searchHref, ...linksToFollow);
+ }
+
+ /**
+ * Get all collections the user is authorized to perform a specific action on
+ *
+ * @param query limit the returned collection to those with metadata values
+ * matching the query terms.
+ * @param options The [[FindListOptions]] object
+ * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
+ * no valid cached version. Defaults to true
+ * @param reRequestOnStale Whether or not the request should automatically be re-
+ * requested after the response becomes stale
* @param searchHref The backend search endpoint to use (default to submit)
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
* @return Observable>>
* collection list
*/
diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts
index ba4feedbb55..7a216afd5e6 100644
--- a/src/app/core/data/community-data.service.ts
+++ b/src/app/core/data/community-data.service.ts
@@ -40,6 +40,66 @@ export class CommunityDataService extends ComColDataService {
super('communities', requestService, rdbService, objectCache, halService, comparator, notificationsService, bitstreamDataService);
}
+ /**
+ * Get all communities the user is admin of
+ *
+ * @param query limit the returned collection to those with metadata values
+ * matching the query terms.
+ * @param options The [[FindListOptions]] object
+ * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
+ * no valid cached version. Defaults to true
+ * @param reRequestOnStale Whether or not the request should automatically be re-
+ * requested after the response becomes stale
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
+ * @return Observable>>
+ * community list
+ */
+ getAdminAuthorizedCommunity(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ const searchHref = 'findAdminAuthorized';
+ return this.getAuthorizedCommunity(query, options, useCachedVersionIfAvailable, reRequestOnStale, searchHref, ...linksToFollow);
+ }
+
+ /**
+ * Get all communities the user is authorized to add a new subcommunity or collection to
+ *
+ * @param query limit the returned collection to those with metadata values
+ * matching the query terms.
+ * @param options The [[FindListOptions]] object
+ * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
+ * no valid cached version. Defaults to true
+ * @param reRequestOnStale Whether or not the request should automatically be re-
+ * requested after the response becomes stale
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
+ * @return Observable>>
+ * community list
+ */
+ getAddAuthorizedCommunity(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ const searchHref = 'findAddAuthorized';
+ return this.getAuthorizedCommunity(query, options, useCachedVersionIfAvailable, reRequestOnStale, searchHref, ...linksToFollow);
+ }
+
+ /**
+ * Get all communities the user is authorized to edit
+ *
+ * @param query limit the returned collection to those with metadata values
+ * matching the query terms.
+ * @param options The [[FindListOptions]] object
+ * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
+ * no valid cached version. Defaults to true
+ * @param reRequestOnStale Whether or not the request should automatically be re-
+ * requested after the response becomes stale
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
+ * @return Observable>>
+ * community list
+ */
+ getEditAuthorizedCommunity(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ const searchHref = 'findEditAuthorized';
+ return this.getAuthorizedCommunity(query, options, useCachedVersionIfAvailable, reRequestOnStale, searchHref, ...linksToFollow);
+ }
+
/**
* Get all communities the user is authorized to submit to
*
@@ -50,13 +110,13 @@ export class CommunityDataService extends ComColDataService {
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
+ * @param searchHref The search endpoint to use, defaults to 'findAdminAuthorized'
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return Observable>>
* community list
*/
- getAuthorizedCommunity(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> {
- const searchHref = 'findAdminAuthorized';
+ getAuthorizedCommunity(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, searchHref: string = 'findAdminAuthorized', ...linksToFollow: FollowLinkConfig[]): Observable>> {
options = Object.assign({}, options, {
searchParams: [new RequestParam('query', query)],
});
diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts
index 2fdad85b482..5b5923080c4 100644
--- a/src/app/core/data/item-data.service.ts
+++ b/src/app/core/data/item-data.service.ts
@@ -18,6 +18,7 @@ import {
switchMap,
take,
} from 'rxjs/operators';
+import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model';
import {
hasValue,
@@ -57,6 +58,7 @@ import {
PatchData,
PatchDataImpl,
} from './base/patch-data';
+import { SearchDataImpl } from './base/search-data';
import { BundleDataService } from './bundle-data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { FindListOptions } from './find-list-options.model';
@@ -83,6 +85,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService-
private createData: CreateData
- ;
private patchData: PatchData
- ;
private deleteData: DeleteData
- ;
+ private searchData: SearchDataImpl
- ;
protected constructor(
protected linkPath,
@@ -101,6 +104,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService
-
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
this.patchData = new PatchDataImpl
- (this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint);
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
+ this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}
/**
@@ -361,6 +365,26 @@ export abstract class BaseItemDataService extends IdentifiableDataService
-
);
}
+ /**
+ * Find the list of items for which the current user has editing rights.
+ *
+ * @param query limit the returned collection to those with metadata values
+ * matching the query terms
+ * @param options The [[FindListOptions]] object
+ * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
+ * no valid cached version. Defaults to true
+ * @param reRequestOnStale Whether or not the request should automatically be re-
+ * requested after the response becomes stale
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
+ * @return Observable
>>
+ * item list
+ */
+ public findEditAuthorized(query: string, options: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig- []): Observable
>> {
+ options = { ...options, searchParams: [new RequestParam('query', query)] };
+ return this.searchBy('findEditAuthorized', options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
+ }
+
/**
* Invalidate the cache of the item
* @param itemUUID
@@ -377,6 +401,24 @@ export abstract class BaseItemDataService extends IdentifiableDataService-
this.patchData.commitUpdates(method);
}
+ /**
+ * Make a new FindListRequest with given search method
+ *
+ * @param searchMethod The search method for the object
+ * @param options The [[FindListOptions]] object
+ * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
+ * no valid cached version. Defaults to true
+ * @param reRequestOnStale Whether or not the request should automatically be re-
+ * requested after the response becomes stale
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
+ * @return {Observable
>}
+ * Return an observable that emits response from the server
+ */
+ public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig- []): Observable
>> {
+ return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
+ }
+
/**
* Send a patch request for a specified object
* @param {T} object The object to send a patch request for
diff --git a/src/app/core/locale/locale.interceptor.spec.ts b/src/app/core/locale/locale.interceptor.spec.ts
index dcc4b7e257c..8806f937c01 100644
--- a/src/app/core/locale/locale.interceptor.spec.ts
+++ b/src/app/core/locale/locale.interceptor.spec.ts
@@ -18,11 +18,12 @@ describe(`LocaleInterceptor`, () => {
let localeService: any;
const languageList = ['en;q=1', 'it;q=0.9', 'de;q=0.8', 'fr;q=0.7'];
+ const rootHref = 'https://sandbox.dspace.org/server/api';
- const mockLocaleService = jasmine.createSpyObj('LocaleService', {
- getCurrentLanguageCode: jasmine.createSpy('getCurrentLanguageCode'),
- getLanguageCodeList: of(languageList),
- });
+ const mockLocaleService = jasmine.createSpyObj('LocaleService', [
+ 'getCurrentLanguageCode',
+ 'getLanguageCodeList',
+ ]);
const mockHalEndpointService = {
getRootHref: jasmine.createSpy('getRootHref'),
@@ -48,6 +49,8 @@ describe(`LocaleInterceptor`, () => {
localeService = TestBed.inject(LocaleService);
localeService.getCurrentLanguageCode.and.returnValue(of('en'));
+ localeService.getLanguageCodeList.and.returnValue(of(languageList));
+ mockHalEndpointService.getRootHref.and.returnValue(rootHref);
});
describe('', () => {
@@ -76,6 +79,29 @@ describe(`LocaleInterceptor`, () => {
const lang = httpRequest.request.headers.get('Accept-Language');
expect(lang).toBeDefined();
expect(lang).toBe(languageList.toString());
+ expect(localeService.getLanguageCodeList).toHaveBeenCalledWith(false);
+ });
+
+ it('should ignore EPerson settings for root endpoint requests', () => {
+ service.request(RestRequestMethod.GET, rootHref).subscribe((response) => {
+ expect(response).toBeTruthy();
+ });
+
+ httpMock.expectOne(rootHref);
+
+ expect(localeService.getLanguageCodeList).toHaveBeenCalledWith(true);
+ });
+
+ it('should ignore EPerson settings for eperson endpoint requests', () => {
+ const epersonHref = `${rootHref}/eperson/epersons/1234`;
+
+ service.request(RestRequestMethod.GET, epersonHref).subscribe((response) => {
+ expect(response).toBeTruthy();
+ });
+
+ httpMock.expectOne(epersonHref);
+
+ expect(localeService.getLanguageCodeList).toHaveBeenCalledWith(true);
});
});
diff --git a/src/app/core/locale/locale.interceptor.ts b/src/app/core/locale/locale.interceptor.ts
index a415ab8c514..f005b333b12 100644
--- a/src/app/core/locale/locale.interceptor.ts
+++ b/src/app/core/locale/locale.interceptor.ts
@@ -31,7 +31,9 @@ export class LocaleInterceptor implements HttpInterceptor {
*/
intercept(req: HttpRequest, next: HttpHandler): Observable> {
let newReq: HttpRequest;
- return this.localeService.getLanguageCodeList(req.url === this.halEndpointService.getRootHref())
+ const ignoreEPersonSettings: boolean = this.shouldIgnoreEPersonSettings(req.url);
+
+ return this.localeService.getLanguageCodeList(ignoreEPersonSettings)
.pipe(
take(1),
scan((acc: any, value: any) => [...acc, value], []),
@@ -45,4 +47,12 @@ export class LocaleInterceptor implements HttpInterceptor {
return next.handle(newReq);
}));
}
+
+ /**
+ * Avoid recursive EPerson language lookup for requests that are needed to resolve EPerson itself.
+ */
+ private shouldIgnoreEPersonSettings(url: string): boolean {
+ const rootHref = this.halEndpointService.getRootHref();
+ return url === rootHref || url.startsWith(`${rootHref}/eperson/epersons`);
+ }
}
diff --git a/src/app/core/metadata/head-tag.service.spec.ts b/src/app/core/metadata/head-tag.service.spec.ts
index da4fa97ee98..96723fe6a96 100644
--- a/src/app/core/metadata/head-tag.service.spec.ts
+++ b/src/app/core/metadata/head-tag.service.spec.ts
@@ -97,7 +97,7 @@ describe('HeadTagService', () => {
},
} as any as Router;
hardRedirectService = jasmine.createSpyObj( {
- getCurrentOrigin: 'https://request.org',
+ getBaseUrl: 'https://request.org',
});
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true),
diff --git a/src/app/core/metadata/head-tag.service.ts b/src/app/core/metadata/head-tag.service.ts
index efe585733b0..801a95c0fe5 100644
--- a/src/app/core/metadata/head-tag.service.ts
+++ b/src/app/core/metadata/head-tag.service.ts
@@ -324,7 +324,7 @@ export class HeadTagService {
if (this.currentObject.value instanceof Item) {
let url = this.getMetaTagValue('dc.identifier.uri');
if (hasNoValue(url)) {
- url = new URLCombiner(this.hardRedirectService.getCurrentOrigin(), this.router.url).toString();
+ url = new URLCombiner(this.hardRedirectService.getBaseUrl(), this.router.url).toString();
}
this.addMetaTag('citation_abstract_html_url', url);
}
@@ -407,7 +407,7 @@ export class HeadTagService {
// Use the found link to set the tag
this.addMetaTag(
'citation_pdf_url',
- new URLCombiner(this.hardRedirectService.getCurrentOrigin(), link).toString(),
+ new URLCombiner(this.hardRedirectService.getBaseUrl(), link).toString(),
);
});
}
diff --git a/src/app/core/resource-policy/models/action-type.model.ts b/src/app/core/resource-policy/models/action-type.model.ts
index 93c69c37052..69da5b37607 100644
--- a/src/app/core/resource-policy/models/action-type.model.ts
+++ b/src/app/core/resource-policy/models/action-type.model.ts
@@ -15,7 +15,7 @@ export enum ActionType {
/**
* Action of deleting something
*/
- DELETE = 'DELETE',
+ DELETE = 'OBSOLETE (DELETE)',
/**
* Action of adding something to a container
diff --git a/src/app/core/services/browser-hard-redirect.service.spec.ts b/src/app/core/services/browser-hard-redirect.service.spec.ts
index 859bfdc8aa8..4d4e98b71e1 100644
--- a/src/app/core/services/browser-hard-redirect.service.spec.ts
+++ b/src/app/core/services/browser-hard-redirect.service.spec.ts
@@ -1,11 +1,13 @@
import { TestBed } from '@angular/core/testing';
+import { environment } from '../../../environments/environment';
import { BrowserHardRedirectService } from './browser-hard-redirect.service';
describe('BrowserHardRedirectService', () => {
let origin: string;
let mockLocation: Location;
let service: BrowserHardRedirectService;
+ let originalBaseUrl;
beforeEach(() => {
origin = 'https://test-host.com:4000';
@@ -20,11 +22,22 @@ describe('BrowserHardRedirectService', () => {
} as Location;
spyOn(mockLocation, 'replace');
+ // Store original environment variable to restore after tests
+ originalBaseUrl = environment.ui.baseUrl;
+
+ // Set environment variable to match our mock location origin for testing
+ environment.ui.baseUrl = origin;
+
service = new BrowserHardRedirectService(mockLocation);
TestBed.configureTestingModule({});
});
+ afterEach(() => {
+ // Restore original environment variable after tests
+ environment.ui.baseUrl = originalBaseUrl;
+ });
+
it('should be created', () => {
expect(service).toBeTruthy();
});
@@ -52,7 +65,7 @@ describe('BrowserHardRedirectService', () => {
describe('when requesting the origin', () => {
it('should return the location origin', () => {
- expect(service.getCurrentOrigin()).toEqual(origin);
+ expect(service.getBaseUrl()).toEqual(origin);
});
});
diff --git a/src/app/core/services/browser-hard-redirect.service.ts b/src/app/core/services/browser-hard-redirect.service.ts
index c386e82f2e9..a85b0cf1370 100644
--- a/src/app/core/services/browser-hard-redirect.service.ts
+++ b/src/app/core/services/browser-hard-redirect.service.ts
@@ -4,6 +4,7 @@ import {
InjectionToken,
} from '@angular/core';
+import { environment } from '../../../environments/environment';
import { HardRedirectService } from './hard-redirect.service';
export const LocationToken = new InjectionToken('Location');
@@ -41,12 +42,11 @@ export class BrowserHardRedirectService extends HardRedirectService {
}
/**
- * Get the origin of the current URL
+ * Get the base public URL of our application.
+ * This is used as the base URL for redirects, and should be in the format of
* i.e. "://" [ ":" ]
- * e.g. if the URL is https://demo.dspace.org/search?query=test,
- * the origin would be https://demo.dspace.org
*/
- getCurrentOrigin(): string {
- return this.location.origin;
+ getBaseUrl(): string {
+ return environment.ui.baseUrl;
}
}
diff --git a/src/app/core/services/browser.referrer.service.spec.ts b/src/app/core/services/browser.referrer.service.spec.ts
index 90ce43c995b..f4aaca93e2c 100644
--- a/src/app/core/services/browser.referrer.service.spec.ts
+++ b/src/app/core/services/browser.referrer.service.spec.ts
@@ -16,7 +16,7 @@ describe(`BrowserReferrerService`, () => {
service = new BrowserReferrerService(
{ referrer: documentReferrer },
routeService,
- { getCurrentOrigin: () => origin } as any,
+ { getBaseUrl: () => origin } as any,
);
});
diff --git a/src/app/core/services/browser.referrer.service.ts b/src/app/core/services/browser.referrer.service.ts
index fd2d3d5efd3..c0d980388c5 100644
--- a/src/app/core/services/browser.referrer.service.ts
+++ b/src/app/core/services/browser.referrer.service.ts
@@ -50,7 +50,7 @@ export class BrowserReferrerService extends ReferrerService {
const reversedHistory = [...history].reverse();
// and find the first URL that differs from the current one
const prevUrl = reversedHistory.find((url: string) => url !== currentURL);
- return new URLCombiner(this.hardRedirectService.getCurrentOrigin(), prevUrl).toString();
+ return new URLCombiner(this.hardRedirectService.getBaseUrl(), prevUrl).toString();
}
}),
);
diff --git a/src/app/core/services/hard-redirect.service.ts b/src/app/core/services/hard-redirect.service.ts
index e6104cefb9c..023b84a8af3 100644
--- a/src/app/core/services/hard-redirect.service.ts
+++ b/src/app/core/services/hard-redirect.service.ts
@@ -23,10 +23,9 @@ export abstract class HardRedirectService {
abstract getCurrentRoute(): string;
/**
- * Get the origin of the current URL
+ * Get the base public URL of our application.
+ * This is used as the base URL for redirects, and should be in the format of
* i.e. "://" [ ":" ]
- * e.g. if the URL is https://demo.dspace.org/search?query=test,
- * the origin would be https://demo.dspace.org
*/
- abstract getCurrentOrigin(): string;
+ abstract getBaseUrl(): string;
}
diff --git a/src/app/core/services/server-hard-redirect.service.spec.ts b/src/app/core/services/server-hard-redirect.service.spec.ts
index a904a8e66cf..df99c37e59c 100644
--- a/src/app/core/services/server-hard-redirect.service.spec.ts
+++ b/src/app/core/services/server-hard-redirect.service.spec.ts
@@ -1,6 +1,7 @@
import { TestBed } from '@angular/core/testing';
-import { environment } from '../../../environments/environment.test';
+import { AppConfig } from '../../../config/app-config.interface';
+import { environment } from '../../../environments/environment';
import { ServerHardRedirectService } from './server-hard-redirect.service';
describe('ServerHardRedirectService', () => {
@@ -8,8 +9,20 @@ describe('ServerHardRedirectService', () => {
const mockRequest = jasmine.createSpyObj(['get']);
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
- let service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse);
+ const envConfig = {
+ rest: {
+ ssl: true,
+ host: 'rest.com',
+ port: 443,
+ // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
+ nameSpace: '/api',
+ baseUrl: 'https://rest.com/server',
+ },
+ } as AppConfig;
+
+ let service: ServerHardRedirectService = new ServerHardRedirectService(envConfig, mockRequest, mockResponse);
const origin = 'https://test-host.com:4000';
+ let originalBaseUrl;
beforeEach(() => {
mockRequest.protocol = 'https';
@@ -17,9 +30,20 @@ describe('ServerHardRedirectService', () => {
host: 'test-host.com:4000',
};
+ // Store original environment variable to restore after tests
+ originalBaseUrl = environment.ui.baseUrl;
+
+ // Set environment variable to match our mock location origin for testing
+ environment.ui.baseUrl = origin;
+
TestBed.configureTestingModule({});
});
+ afterEach(() => {
+ // Restore original environment variable after tests
+ environment.ui.baseUrl = originalBaseUrl;
+ });
+
it('should be created', () => {
expect(service).toBeTruthy();
});
@@ -65,14 +89,14 @@ describe('ServerHardRedirectService', () => {
describe('when requesting the origin', () => {
it('should return the location origin', () => {
- expect(service.getCurrentOrigin()).toEqual(origin);
+ expect(service.getBaseUrl()).toEqual(origin);
});
});
describe('when SSR base url is set', () => {
const redirect = 'https://private-url:4000/server/api/bitstreams/uuid';
const replacedUrl = 'https://public-url/server/api/bitstreams/uuid';
- const environmentWithSSRUrl: any = { ...environment, ...{ ...environment.rest, rest: {
+ const environmentWithSSRUrl: any = { ...envConfig, ...{ ...envConfig.rest, rest: {
ssrBaseUrl: 'https://private-url:4000/server',
baseUrl: 'https://public-url/server',
} } };
diff --git a/src/app/core/services/server-hard-redirect.service.ts b/src/app/core/services/server-hard-redirect.service.ts
index 1592d9bf1cf..b061c53423e 100644
--- a/src/app/core/services/server-hard-redirect.service.ts
+++ b/src/app/core/services/server-hard-redirect.service.ts
@@ -11,6 +11,7 @@ import {
APP_CONFIG,
AppConfig,
} from '../../../config/app-config.interface';
+import { environment } from '../../../environments/environment';
import {
REQUEST,
RESPONSE,
@@ -18,6 +19,7 @@ import {
import { isNotEmpty } from '../../shared/empty.util';
import { HardRedirectService } from './hard-redirect.service';
+
/**
* Service for performing hard redirects within the server app module
*/
@@ -88,12 +90,11 @@ export class ServerHardRedirectService extends HardRedirectService {
}
/**
- * Get the origin of the current URL
+ * Get the base public URL of our application.
+ * This is used as the base URL for redirects, and should be in the format of
* i.e. "://" [ ":" ]
- * e.g. if the URL is https://demo.dspace.org/search?query=test,
- * the origin would be https://demo.dspace.org
*/
- getCurrentOrigin(): string {
- return this.req.protocol + '://' + this.req.headers.host;
+ getBaseUrl(): string {
+ return environment.ui.baseUrl;
}
}
diff --git a/src/app/core/submission/models/sherpa-policies-details.model.ts b/src/app/core/submission/models/opf-policies-details.model.ts
similarity index 84%
rename from src/app/core/submission/models/sherpa-policies-details.model.ts
rename to src/app/core/submission/models/opf-policies-details.model.ts
index af4d4a58907..19450477278 100644
--- a/src/app/core/submission/models/sherpa-policies-details.model.ts
+++ b/src/app/core/submission/models/opf-policies-details.model.ts
@@ -1,25 +1,25 @@
/**
* An interface to represent an access condition.
*/
-export class SherpaPoliciesDetailsObject {
+export class JiscOpfPoliciesDetailsObject {
/**
- * The sherpa policies error
+ * The Jisc Open Policy Finder policies error
*/
error: boolean;
/**
- * The sherpa policies journal details
+ * The Jisc Open Policy Finder policies journal details
*/
journals: Journal[];
/**
- * The sherpa policies message
+ * The Jisc Open Policy Finder policies message
*/
message: string;
/**
- * The sherpa policies metadata
+ * The Jisc Open Policy Finder policies metadata
*/
metadata: Metadata;
diff --git a/src/app/core/submission/models/workspaceitem-section-opf-policies.model.ts b/src/app/core/submission/models/workspaceitem-section-opf-policies.model.ts
new file mode 100644
index 00000000000..c435d927ef2
--- /dev/null
+++ b/src/app/core/submission/models/workspaceitem-section-opf-policies.model.ts
@@ -0,0 +1,22 @@
+import { JiscOpfPoliciesDetailsObject } from './opf-policies-details.model';
+
+/**
+ * An interface to represent the submission's item accesses condition.
+ */
+export interface WorkspaceitemSectionJiscOpfPoliciesObject {
+
+ /**
+ * The access condition id
+ */
+ id: string;
+
+ /**
+ * The Jisc Open Policy Finder policies retrievalTime
+ */
+ retrievalTime: string;
+
+ /**
+ * The Jisc Open Policy Finder policies details
+ */
+ opfResponse: JiscOpfPoliciesDetailsObject;
+}
diff --git a/src/app/core/submission/models/workspaceitem-section-sherpa-policies.model.ts b/src/app/core/submission/models/workspaceitem-section-sherpa-policies.model.ts
deleted file mode 100644
index c57beadbb9b..00000000000
--- a/src/app/core/submission/models/workspaceitem-section-sherpa-policies.model.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { SherpaPoliciesDetailsObject } from './sherpa-policies-details.model';
-
-/**
- * An interface to represent the submission's item accesses condition.
- */
-export interface WorkspaceitemSectionSherpaPoliciesObject {
-
- /**
- * The access condition id
- */
- id: string;
-
- /**
- * The sherpa policies retrievalTime
- */
- retrievalTime: string;
-
- /**
- * The sherpa policies details
- */
- sherpaResponse: SherpaPoliciesDetailsObject;
-}
diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts
index 46a0590977f..4ed0f0a6d54 100644
--- a/src/app/core/submission/models/workspaceitem-sections.model.ts
+++ b/src/app/core/submission/models/workspaceitem-sections.model.ts
@@ -4,7 +4,7 @@ import { WorkspaceitemSectionDuplicatesObject } from './workspaceitem-section-du
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
import { WorkspaceitemSectionIdentifiersObject } from './workspaceitem-section-identifiers.model';
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
-import { WorkspaceitemSectionSherpaPoliciesObject } from './workspaceitem-section-sherpa-policies.model';
+import { WorkspaceitemSectionJiscOpfPoliciesObject } from './workspaceitem-section-opf-policies.model';
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
/**
@@ -24,7 +24,7 @@ export type WorkspaceitemSectionDataType
| WorkspaceitemSectionLicenseObject
| WorkspaceitemSectionCcLicenseObject
| WorkspaceitemSectionAccessesObject
- | WorkspaceitemSectionSherpaPoliciesObject
+ | WorkspaceitemSectionJiscOpfPoliciesObject
| WorkspaceitemSectionIdentifiersObject
| WorkspaceitemSectionDuplicatesObject
| string;
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html
index 22627826874..6e540d51a4d 100644
--- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html
+++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html
@@ -102,7 +102,7 @@
cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [dsBtnDisabled]="disabled"
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}">
-
+
diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/app/entity-groups/research-entities/item-pages/person/person.component.html
index 82d42e6857c..20b591a35ef 100644
--- a/src/app/entity-groups/research-entities/item-pages/person/person.component.html
+++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.html
@@ -49,6 +49,9 @@
[fields]="['dc.title']"
[label]="'person.page.name'">
+
- {{'item.qa-event-notification.check.notification-info' | translate : {num: source.totalEvents } }}
+ @if (source.totalEvents === 1) {
+ {{'item.qa-event-notification.check.notification-info.singular' | translate : {num: source.totalEvents } }}
+ } @else {
+ {{'item.qa-event-notification.check.notification-info.plural' | translate : {num: source.totalEvents } }}
+ }
{
{ provide: CookieService, useValue: new CookieServiceMock() },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
{ provide: EntityTypeDataService, useValue: getMockEntityTypeService() },
+ { provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
diff --git a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.html b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.html
index e44d2c6e489..a9294595051 100644
--- a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.html
+++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.html
@@ -14,7 +14,11 @@
- {{ "mydspace.qa-event-notification.check.notification-info" | translate : { num: source.totalEvents } }}
+ @if (source.totalEvents === 1) {
+ {{ "mydspace.qa-event-notification.check.notification-info.singular" | translate : { num: source.totalEvents } }}
+ } @else {
+ {{ "mydspace.qa-event-notification.check.notification-info.plural" | translate : { num: source.totalEvents } }}
+ }
{
const validationError = fixture.debugElement.query(By.css('.validation-error'));
expect(validationError).toBeFalsy();
}));
+
+ it('should load more scripts when scrolled to the bottom', fakeAsync(() => {
+ spyOn(component, 'loadScripts');
+ const event = {
+ target: {
+ scrollTop: 100,
+ clientHeight: 200,
+ scrollHeight: 300,
+ },
+ };
+
+ component.onScroll(event);
+ tick();
+
+ expect(component.loadScripts).toHaveBeenCalled();
+ }));
+
+ it('should load more scripts when scrolled almost to the bottom', fakeAsync(() => {
+ spyOn(component, 'loadScripts');
+ const event = {
+ target: {
+ scrollTop: 99,
+ clientHeight: 200,
+ scrollHeight: 300,
+ },
+ };
+
+ component.onScroll(event);
+ tick();
+
+ expect(component.loadScripts).toHaveBeenCalled();
+ }));
+
+ it('should not load more scripts if already loading', fakeAsync(() => {
+ spyOn(component, 'loadScripts');
+ component.isLoading$.next(true);
+ const event = {
+ target: {
+ scrollTop: 100,
+ clientHeight: 200,
+ scrollHeight: 300,
+ },
+ };
+
+ component.onScroll(event);
+ tick();
+
+ expect(component.loadScripts).not.toHaveBeenCalled();
+ }));
+
+ it('should not load more scripts if it is the last page', fakeAsync(() => {
+ spyOn(component, 'loadScripts');
+ (component as any)._isLastPage = true;
+ const event = {
+ target: {
+ scrollTop: 100,
+ clientHeight: 200,
+ scrollHeight: 300,
+ },
+ };
+
+ component.onScroll(event);
+ tick();
+
+ expect(component.loadScripts).not.toHaveBeenCalled();
+ }));
+
+ it('should not load more scripts if not scrolled to the bottom', fakeAsync(() => {
+ spyOn(component, 'loadScripts');
+ const event = {
+ target: {
+ scrollTop: 50,
+ clientHeight: 200,
+ scrollHeight: 300,
+ },
+ };
+
+ component.onScroll(event);
+ tick();
+
+ expect(component.loadScripts).not.toHaveBeenCalled();
+ }));
});
diff --git a/src/app/process-page/form/scripts-select/scripts-select.component.ts b/src/app/process-page/form/scripts-select/scripts-select.component.ts
index 9eccb7ceffc..3af3760eb2b 100644
--- a/src/app/process-page/form/scripts-select/scripts-select.component.ts
+++ b/src/app/process-page/form/scripts-select/scripts-select.component.ts
@@ -128,7 +128,9 @@ export class ScriptsSelectComponent implements OnInit, OnDestroy {
* @param event The scroll event
*/
onScroll(event: any) {
- if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) {
+ // offset to fix issues with zooming in or out in the browser
+ const offset = 5;
+ if (event.target.scrollTop + event.target.clientHeight + offset >= event.target.scrollHeight) {
if (!this.isLoading$.value && !this._isLastPage) {
this.scriptOptions.currentPage++;
this.loadScripts();
diff --git a/src/app/shared/btn-disabled.directive.ts b/src/app/shared/btn-disabled.directive.ts
index bbc21fef7f8..525ba00c579 100644
--- a/src/app/shared/btn-disabled.directive.ts
+++ b/src/app/shared/btn-disabled.directive.ts
@@ -54,7 +54,7 @@ export class BtnDisabledDirective {
*/
@HostListener('keydown', ['$event'])
handleKeydown(event: KeyboardEvent) {
- if (this.isDisabled && (event.key === 'Enter' || event.key === 'Space')) {
+ if (this.isDisabled && (event.key === 'Enter' || event.key === ' ')) {
event.preventDefault();
event.stopImmediatePropagation();
}
diff --git a/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.scss b/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.scss
index db6e7daecfd..0323e8cfae5 100644
--- a/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.scss
+++ b/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.scss
@@ -1,4 +1,6 @@
img {
max-width: var(--ds-comcol-logo-max-width);
max-height: var(--ds-comcol-logo-max-height);
+ object-fit: contain;
+ object-position: left center;
}
diff --git a/src/app/shared/disabled-directive.spec.ts b/src/app/shared/disabled-directive.spec.ts
index 96f760fc466..37a09e8560a 100644
--- a/src/app/shared/disabled-directive.spec.ts
+++ b/src/app/shared/disabled-directive.spec.ts
@@ -76,7 +76,7 @@ describe('DisabledDirective', () => {
button.nativeElement.addEventListener('keydown', () => keydownHandled = true);
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
- const spaceEvent = new KeyboardEvent('keydown', { key: 'Space' });
+ const spaceEvent = new KeyboardEvent('keydown', { key: ' ' });
button.nativeElement.dispatchEvent(enterEvent);
button.nativeElement.dispatchEvent(spaceEvent);
@@ -94,7 +94,7 @@ describe('DisabledDirective', () => {
button.nativeElement.click();
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
- const spaceEvent = new KeyboardEvent('keydown', { key: 'Space' });
+ const spaceEvent = new KeyboardEvent('keydown', { key: ' ' });
button.nativeElement.dispatchEvent(enterEvent);
button.nativeElement.dispatchEvent(spaceEvent);
diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts
index 4a40f90f013..f2cad69ab33 100644
--- a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts
+++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts
@@ -8,6 +8,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { CollectionDataService } from '../../../../core/data/collection-data.service';
+import { ActionType } from '../../../../core/resource-policy/models/action-type.model';
import { Collection } from '../../../../core/shared/collection.model';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { SearchService } from '../../../../core/shared/search/search.service';
@@ -34,6 +35,9 @@ describe('AuthorizedCollectionSelectorComponent', () => {
});
collectionService = jasmine.createSpyObj('collectionService', {
getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])),
+ getSubmitAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])),
+ getAdminAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])),
+ getEditAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])),
getAuthorizedCollectionByEntityType: createSuccessfulRemoteDataObject$(createPaginatedList([collection])),
});
notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
@@ -60,10 +64,41 @@ describe('AuthorizedCollectionSelectorComponent', () => {
});
describe('search', () => {
- describe('when has no entity type', () => {
- it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => {
+ describe('when action type is ADD', () => {
+ describe('when has no entity type', () => {
+ it('should call getSubmitAuthorizedCollection and return the authorized collection in a SearchResult', (done) => {
+ component.action = ActionType.ADD;
+ fixture.detectChanges();
+ component.search('', 1).subscribe((resultRD) => {
+ expect(collectionService.getSubmitAuthorizedCollection).toHaveBeenCalled();
+ expect(resultRD.payload.page.length).toEqual(1);
+ expect(resultRD.payload.page[0].indexableObject).toEqual(collection);
+ done();
+ });
+ });
+ });
+
+ describe('when has entity type', () => {
+ it('should call getAuthorizedCollectionByEntityType and return the authorized collection in a SearchResult', (done) => {
+ component.entityType = 'test';
+ component.action = ActionType.ADD;
+ fixture.detectChanges();
+ component.search('', 1).subscribe((resultRD) => {
+ expect(collectionService.getAuthorizedCollectionByEntityType).toHaveBeenCalled();
+ expect(resultRD.payload.page.length).toEqual(1);
+ expect(resultRD.payload.page[0].indexableObject).toEqual(collection);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('when action type is WRITE', () => {
+ it('should call getEditAuthorizedCollection', (done) => {
+ component.action = ActionType.WRITE;
+ fixture.detectChanges();
component.search('', 1).subscribe((resultRD) => {
- expect(collectionService.getAuthorizedCollection).toHaveBeenCalled();
+ expect(collectionService.getEditAuthorizedCollection).toHaveBeenCalled();
expect(resultRD.payload.page.length).toEqual(1);
expect(resultRD.payload.page[0].indexableObject).toEqual(collection);
done();
@@ -71,12 +106,10 @@ describe('AuthorizedCollectionSelectorComponent', () => {
});
});
- describe('when has entity type', () => {
- it('should call getAuthorizedCollectionByEntityType and return the authorized collection in a SearchResult', (done) => {
- component.entityType = 'test';
- fixture.detectChanges();
+ describe('when action is not provided', () => {
+ it('should call getAdminAuthorizedCollection', (done) => {
component.search('', 1).subscribe((resultRD) => {
- expect(collectionService.getAuthorizedCollectionByEntityType).toHaveBeenCalled();
+ expect(collectionService.getAdminAuthorizedCollection).toHaveBeenCalled();
expect(resultRD.payload.page.length).toEqual(1);
expect(resultRD.payload.page[0].indexableObject).toEqual(collection);
done();
diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts
index 29f6422952d..ee74f7a7ee7 100644
--- a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts
+++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts
@@ -19,6 +19,7 @@ import {
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
+import { ActionType } from 'src/app/core/resource-policy/models/action-type.model';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { CollectionDataService } from '../../../../core/data/collection-data.service';
@@ -59,10 +60,15 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
@Input() entityType: string;
/**
- * Search endpoint to use for finding authorized collections.
- * Defaults to 'findSubmitAuthorized', but can be overridden (e.g. to 'findAdminAuthorized')
+ * Optional search endpoint override for finding authorized collections.
+ * When set, it takes precedence over the endpoint selected by the action type.
*/
- @Input() searchHref = 'findSubmitAuthorized';
+ @Input() searchHref: string;
+
+ /**
+ * The action type to determine which authorized collections to fetch, defaults to ADMIN
+ */
+ @Input() action: ActionType = ActionType.ADMIN;
constructor(
protected searchService: SearchService,
@@ -94,15 +100,27 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
elementsPerPage: this.defaultPagination.pageSize,
};
- if (this.entityType) {
+ if (this.searchHref) {
searchListService$ = this.collectionDataService
- .getAuthorizedCollectionByEntityType(
- query,
- this.entityType,
- findOptions);
+ .getAuthorizedCollection(query, findOptions, useCache, false, this.searchHref, followLink('parentCommunity'));
+ } else if (this.action === ActionType.WRITE) {
+ searchListService$ = this.collectionDataService
+ .getEditAuthorizedCollection(query, findOptions, useCache, false, followLink('parentCommunity'));
+ } else if (this.action === ActionType.ADD) {
+ if (this.entityType) {
+ searchListService$ = this.collectionDataService
+ .getAuthorizedCollectionByEntityType(
+ query,
+ this.entityType,
+ findOptions);
+ } else {
+ searchListService$ = this.collectionDataService
+ .getSubmitAuthorizedCollection(query, findOptions, useCache, false, followLink('parentCommunity'));
+ }
} else {
+ // By default, search for admin authorized collections
searchListService$ = this.collectionDataService
- .getAuthorizedCollection(query, findOptions, useCache, false, this.searchHref, followLink('parentCommunity'));
+ .getAdminAuthorizedCollection(query, findOptions, useCache, false, followLink('parentCommunity'));
}
return searchListService$.pipe(
getFirstCompletedRemoteData(),
diff --git a/src/app/shared/dso-selector/dso-selector/authorized-community-selector/authorized-community-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/authorized-community-selector/authorized-community-selector.component.spec.ts
index d11291c7568..3e8969468f4 100644
--- a/src/app/shared/dso-selector/dso-selector/authorized-community-selector/authorized-community-selector.component.spec.ts
+++ b/src/app/shared/dso-selector/dso-selector/authorized-community-selector/authorized-community-selector.component.spec.ts
@@ -6,6 +6,7 @@ import {
} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
+import { ActionType } from 'src/app/core/resource-policy/models/action-type.model';
import { CommunityDataService } from '../../../../core/data/community-data.service';
import { Community } from '../../../../core/shared/community.model';
@@ -33,7 +34,9 @@ describe('AuthorizedCommunitySelectorComponent', () => {
id: 'authorized-community',
});
communityService = jasmine.createSpyObj('communityService', {
- getAuthorizedCommunity: createSuccessfulRemoteDataObject$(createPaginatedList([community])),
+ getAddAuthorizedCommunity: createSuccessfulRemoteDataObject$(createPaginatedList([community])),
+ getEditAuthorizedCommunity: createSuccessfulRemoteDataObject$(createPaginatedList([community])),
+ getAdminAuthorizedCommunity: createSuccessfulRemoteDataObject$(createPaginatedList([community])),
});
notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
TestBed.configureTestingModule({
@@ -59,12 +62,38 @@ describe('AuthorizedCommunitySelectorComponent', () => {
});
describe('search', () => {
- it('should call getAuthorizedCommunity and return the authorized community in a SearchResult', (done) => {
- component.search('', 1).subscribe((resultRD) => {
- expect(communityService.getAuthorizedCommunity).toHaveBeenCalled();
- expect(resultRD.payload.page.length).toEqual(1);
- expect(resultRD.payload.page[0].indexableObject).toEqual(community);
- done();
+ describe('when action type is ADD', () => {
+ it('should call getAddAuthorizedCommunity and return the authorized community in a SearchResult', (done) => {
+ component.action = ActionType.ADD;
+ fixture.detectChanges();
+ component.search('', 1).subscribe((resultRD) => {
+ expect(communityService.getAddAuthorizedCommunity).toHaveBeenCalled();
+ expect(resultRD.payload.page.length).toEqual(1);
+ expect(resultRD.payload.page[0].indexableObject).toEqual(community);
+ done();
+ });
+ });
+ });
+ describe('when action type is WRITE', () => {
+ it('should call getEditAuthorizedCommunity and return the authorized community in a SearchResult', (done) => {
+ component.action = ActionType.WRITE;
+ fixture.detectChanges();
+ component.search('', 1).subscribe((resultRD) => {
+ expect(communityService.getEditAuthorizedCommunity).toHaveBeenCalled();
+ expect(resultRD.payload.page.length).toEqual(1);
+ expect(resultRD.payload.page[0].indexableObject).toEqual(community);
+ done();
+ });
+ });
+ });
+ describe('when action type is not provided', () => {
+ it('should call getAdminAuthorizedCommunity and return the authorized community in a SearchResult', (done) => {
+ component.search('', 1).subscribe((resultRD) => {
+ expect(communityService.getAdminAuthorizedCommunity).toHaveBeenCalled();
+ expect(resultRD.payload.page.length).toEqual(1);
+ expect(resultRD.payload.page[0].indexableObject).toEqual(community);
+ done();
+ });
});
});
});
diff --git a/src/app/shared/dso-selector/dso-selector/authorized-community-selector/authorized-community-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-community-selector/authorized-community-selector.component.ts
index f5a6ed752ee..f060ba46eeb 100644
--- a/src/app/shared/dso-selector/dso-selector/authorized-community-selector/authorized-community-selector.component.ts
+++ b/src/app/shared/dso-selector/dso-selector/authorized-community-selector/authorized-community-selector.component.ts
@@ -4,7 +4,10 @@ import {
NgClass,
NgIf,
} from '@angular/common';
-import { Component } from '@angular/core';
+import {
+ Component,
+ Input,
+} from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
@@ -16,6 +19,7 @@ import {
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
+import { ActionType } from 'src/app/core/resource-policy/models/action-type.model';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { CommunityDataService } from '../../../../core/data/community-data.service';
@@ -62,9 +66,11 @@ import { DSOSelectorComponent } from '../dso-selector.component';
* Component rendering a list of communities to select from
*/
export class AuthorizedCommunitySelectorComponent extends DSOSelectorComponent {
+
/**
- * If present this value is used to filter community list by entity type
+ * The action type to determine which authorized communities to fetch
*/
+ @Input() action: ActionType = ActionType.ADMIN;
constructor(
protected searchService: SearchService,
@@ -96,9 +102,17 @@ export class AuthorizedCommunitySelectorComponent extends DSOSelectorComponent {
elementsPerPage: this.defaultPagination.pageSize,
};
- searchListService$ = this.communityDataService
- .getAuthorizedCommunity(query, findOptions, useCache, false, followLink('parentCommunity'));
-
+ if (this.action === ActionType.WRITE) {
+ searchListService$ = this.communityDataService
+ .getEditAuthorizedCommunity(query, findOptions, useCache, false, followLink('parentCommunity'));
+ } else if (this.action === ActionType.ADD) {
+ searchListService$ = this.communityDataService
+ .getAddAuthorizedCommunity(query, findOptions, useCache, false, followLink('parentCommunity'));
+ } else {
+ // By default, search for admin authorized communities
+ searchListService$ = this.communityDataService
+ .getAdminAuthorizedCommunity(query, findOptions, useCache, false, followLink('parentCommunity'));
+ }
return searchListService$.pipe(
getFirstCompletedRemoteData(),
map((rd) => Object.assign(new RemoteData(null, null, null, null), rd, {
diff --git a/src/app/shared/dso-selector/dso-selector/authorized-item-selector/authorized-item-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/authorized-item-selector/authorized-item-selector.component.spec.ts
new file mode 100644
index 00000000000..bd4278215be
--- /dev/null
+++ b/src/app/shared/dso-selector/dso-selector/authorized-item-selector/authorized-item-selector.component.spec.ts
@@ -0,0 +1,71 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import {
+ ComponentFixture,
+ TestBed,
+ waitForAsync,
+} from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { ItemDataService } from 'src/app/core/data/item-data.service';
+import { Item } from 'src/app/core/shared/item.model';
+import { SearchService } from 'src/app/core/shared/search/search.service';
+import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
+import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils';
+import { createPaginatedList } from 'src/app/shared/testing/utils.test';
+
+import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
+import { ThemedLoadingComponent } from '../../../loading/themed-loading.component';
+import { ListableObjectComponentLoaderComponent } from '../../../object-collection/shared/listable-object/listable-object-component-loader.component';
+import { VarDirective } from '../../../utils/var.directive';
+import { AuthorizedItemSelectorComponent } from './authorized-item-selector.component';
+
+describe('AuthorizedItemSelectorComponent', () => {
+ let component: AuthorizedItemSelectorComponent;
+ let fixture: ComponentFixture;
+
+ let itemService;
+ let item;
+
+ let notificationsService: NotificationsService;
+
+ beforeEach(waitForAsync(() => {
+ item = Object.assign(new Item(), {
+ id: 'authorized-item',
+ });
+ itemService = jasmine.createSpyObj('itemService', {
+ findEditAuthorized: createSuccessfulRemoteDataObject$(createPaginatedList([item])),
+ });
+ notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), AuthorizedItemSelectorComponent, VarDirective],
+ providers: [
+ { provide: SearchService, useValue: {} },
+ { provide: ItemDataService, useValue: itemService },
+ { provide: NotificationsService, useValue: notificationsService },
+ ],
+ schemas: [NO_ERRORS_SCHEMA],
+ })
+ .overrideComponent(AuthorizedItemSelectorComponent, {
+ remove: { imports: [ListableObjectComponentLoaderComponent, ThemedLoadingComponent] },
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AuthorizedItemSelectorComponent);
+ component = fixture.componentInstance;
+ component.types = [DSpaceObjectType.ITEM];
+ fixture.detectChanges();
+ });
+
+ describe('search', () => {
+ it('should call findEditAuthorized and return the authorized item in a SearchResult', (done) => {
+ component.search('', 1).subscribe((resultRD) => {
+ expect(itemService.findEditAuthorized).toHaveBeenCalled();
+ expect(resultRD.payload.page.length).toEqual(1);
+ expect(resultRD.payload.page[0].indexableObject).toEqual(item);
+ done();
+ });
+ });
+ });
+});
diff --git a/src/app/shared/dso-selector/dso-selector/authorized-item-selector/authorized-item-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-item-selector/authorized-item-selector.component.ts
new file mode 100644
index 00000000000..f0c5bbf52f4
--- /dev/null
+++ b/src/app/shared/dso-selector/dso-selector/authorized-item-selector/authorized-item-selector.component.ts
@@ -0,0 +1,113 @@
+import {
+ AsyncPipe,
+ NgClass,
+ NgFor,
+ NgIf,
+} from '@angular/common';
+import {
+ Component,
+ Input,
+} from '@angular/core';
+import {
+ FormsModule,
+ ReactiveFormsModule,
+} from '@angular/forms';
+import {
+ TranslateModule,
+ TranslateService,
+} from '@ngx-translate/core';
+import { InfiniteScrollModule } from 'ngx-infinite-scroll';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { ItemDataService } from 'src/app/core/data/item-data.service';
+import { Item } from 'src/app/core/shared/item.model';
+import { SearchService } from 'src/app/core/shared/search/search.service';
+import { hasValue } from 'src/app/shared/empty.util';
+import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
+import { ItemSearchResult } from 'src/app/shared/object-collection/shared/item-search-result.model';
+import { SearchResult } from 'src/app/shared/search/models/search-result.model';
+import { followLink } from 'src/app/shared/utils/follow-link-config.model';
+
+import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
+import { FindListOptions } from '../../../../core/data/find-list-options.model';
+import {
+ buildPaginatedList,
+ PaginatedList,
+} from '../../../../core/data/paginated-list.model';
+import { RemoteData } from '../../../../core/data/remote-data';
+import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
+import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
+import { HoverClassDirective } from '../../../hover-class.directive';
+import { ThemedLoadingComponent } from '../../../loading/themed-loading.component';
+import { ListableObjectComponentLoaderComponent } from '../../../object-collection/shared/listable-object/listable-object-component-loader.component';
+import { SelectorActionType } from '../../modal-wrappers/dso-selector-modal-wrapper.component';
+import { DSOSelectorComponent } from '../dso-selector.component';
+
+@Component({
+ selector: 'ds-authorized-item-selector',
+ styleUrls: ['../dso-selector.component.scss'],
+ templateUrl: '../dso-selector.component.html',
+ standalone: true,
+ imports: [
+ AsyncPipe,
+ FormsModule,
+ HoverClassDirective,
+ InfiniteScrollModule,
+ ListableObjectComponentLoaderComponent,
+ NgClass,
+ ReactiveFormsModule,
+ ThemedLoadingComponent,
+ TranslateModule,
+ NgIf,
+ NgFor,
+ ],
+})
+/**
+ * Component rendering a list of item to select from for editing
+ */
+export class AuthorizedItemSelectorComponent extends DSOSelectorComponent {
+
+ constructor(
+ protected searchService: SearchService,
+ protected itemDataService: ItemDataService,
+ protected notifcationsService: NotificationsService,
+ protected translate: TranslateService,
+ protected dsoNameService: DSONameService,
+ ) {
+ super(searchService, notifcationsService, translate, dsoNameService);
+ }
+
+ @Input() action: SelectorActionType = SelectorActionType.EDIT;
+
+ /**
+ * Get a query to send for retrieving the current DSO
+ */
+ getCurrentDSOQuery(): string {
+ return this.currentDSOId;
+ }
+
+ /**
+ * Perform a search for authorized collections with the current query and page
+ * @param query Query to search objects for
+ * @param page Page to retrieve
+ * @param useCache Whether or not to use the cache
+ */
+ search(query: string, page: number, useCache: boolean = true): Observable>>> {
+ let searchListService$: Observable>> = null;
+ const findOptions: FindListOptions = {
+ currentPage: page,
+ elementsPerPage: this.defaultPagination.pageSize,
+ };
+
+ // By default, search for edit authorized items
+ searchListService$ = this.itemDataService
+ .findEditAuthorized(query, findOptions, useCache, false, followLink('owningCollection'));
+
+ return searchListService$.pipe(
+ getFirstCompletedRemoteData(),
+ map((rd) => Object.assign(new RemoteData(null, null, null, null), rd, {
+ payload: hasValue(rd.payload) ? buildPaginatedList(rd.payload.pageInfo, rd.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))) : null,
+ })),
+ );
+ }
+}
diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.html b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.html
index 4f6cdee1e32..2bcc5455d88 100644
--- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.html
+++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.html
@@ -7,6 +7,7 @@
{{header | translate}}
diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts
index 5159a408bdd..5b6f9f32f60 100644
--- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts
+++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts
@@ -10,6 +10,7 @@ import {
} from '@angular/router';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
+import { ActionType } from 'src/app/core/resource-policy/models/action-type.model';
import { environment } from '../../../../../environments/environment';
import {
@@ -42,6 +43,7 @@ export class CreateCollectionParentSelectorComponent extends DSOSelectorModalWra
objectType = DSpaceObjectType.COLLECTION;
selectorTypes = [DSpaceObjectType.COMMUNITY];
action = SelectorActionType.CREATE;
+ rpActionType = ActionType.ADD;
header = 'dso-selector.create.collection.sub-level';
defaultSort = new SortOptions(environment.comcolSelectionSort.sortField, environment.comcolSelectionSort.sortDirection as SortDirection);
diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html
index 1c195e257cd..6548ff671d8 100644
--- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html
+++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html
@@ -17,6 +17,7 @@