diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0495f51fc2..0b5b3f9d8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: strategy: # Create a matrix of Node versions to test against (in parallel) matrix: - node-version: [10.x, 12.x] + node-version: [12.x, 14.x] # Do NOT exit immediately if one matrix job fails fail-fast: false # These are the actual CI steps to perform per job @@ -80,6 +80,19 @@ jobs: docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli docker container ls + # Wait until the REST API returns a 200 response (or for a max of 30 seconds) + # https://github.com/nev7n/wait_for_response + - name: Wait for DSpace REST Backend to be ready (for e2e tests) + uses: nev7n/wait_for_response@v1 + with: + # We use the 'sites' endpoint to also ensure the database is ready + url: 'http://localhost:8080/server/api/core/sites' + responseCode: 200 + timeout: 30000 + + - name: Get DSpace REST Backend info/properties + run: curl http://localhost:8080/server/api + - name: Run e2e tests (integration tests) run: yarn run e2e:ci diff --git a/README.md b/README.md index 9a7bdbbbdb..3f5351ec82 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ You can find additional information on the DSpace 7 Angular UI on the [wiki](htt Quick start ----------- -**Ensure you're running [Node](https://nodejs.org) `v10.x` or `v12.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`** +**Ensure you're running [Node](https://nodejs.org) `v12.x` or `v14.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`** ```bash # clone the repo @@ -65,7 +65,7 @@ Requirements ------------ - [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com) -- Ensure you're running node `v10.x` or `v12.x` and yarn >= `v1.x` +- Ensure you're running node `v12.x` or `v14.x` and yarn >= `v1.x` If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS. @@ -339,7 +339,6 @@ dspace-angular ├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration ├── typedoc.json * TYPEDOC configuration ├── webpack * Webpack (https://webpack.github.io/) config directory -│   ├── helpers.js * │   ├── webpack.aot.js * Webpack (https://webpack.github.io/) config for AoT build │   ├── webpack.client.js * Webpack (https://webpack.github.io/) config for client build │   ├── webpack.common.js * diff --git a/angular.json b/angular.json index 8555fe9c2d..19cbe94be6 100644 --- a/angular.json +++ b/angular.json @@ -17,6 +17,7 @@ "build": { "builder": "@angular-builders/custom-webpack:browser", "options": { + "extractCss": true, "preserveSymlinks": true, "customWebpackConfig": { "path": "./webpack/webpack.browser.ts", @@ -46,7 +47,16 @@ "src/robots.txt" ], "styles": [ - "src/styles.scss" + { + "input": "src/styles/base-theme.scss", + "inject": false, + "bundleName": "base-theme" + }, + { + "input": "src/themes/custom/styles/theme.scss", + "inject": false, + "bundleName": "custom-theme" + } ], "scripts": [] }, @@ -116,7 +126,11 @@ "src/assets" ], "styles": [ - "src/styles.scss" + { + "input": "src/styles/base-theme.scss", + "inject": false, + "bundleName": "base-theme" + } ], "scripts": [] } diff --git a/package.json b/package.json index aa8004db9f..8c416ee4e1 100644 --- a/package.json +++ b/package.json @@ -20,16 +20,17 @@ "serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts", "start:dev": "npm-run-all --parallel config:dev:watch serve", "start:prod": "yarn run build:prod && yarn run serve:ssr", + "analyze": "webpack-bundle-analyzer dist/browser/stats.json", "build": "ng build", + "build:stats": "ng build --stats-json", "build:prod": "yarn run build:ssr", "build:ssr": "yarn run build:client-and-server-bundles && yarn run compile:server", - "ng-high-memory": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng", - "build:client-and-server-bundles": "npm run ng-high-memory -- build --prod && npm run ng-high-memory -- run dspace-angular:server:production --bundleDependencies true", + "build:client-and-server-bundles": "ng build --prod && ng run dspace-angular:server:production --bundleDependencies true", "test:watch": "npm-run-all --parallel config:test:watch test", - "test": "npm run ng-high-memory -- test --sourceMap=true --watch=true", - "test:headless": "npm run ng-high-memory -- test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage", + "test": "ng test --sourceMap=true --watch=true", + "test:headless": "ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage", "lint": "ng lint", - "lint-fix": "npm run ng-high-memory -- lint --fix=true", + "lint-fix": "ng lint --fix=true", "e2e": "ng e2e", "e2e:ci": "ng e2e --protractor-config=./e2e/protractor-ci.conf.js", "compile:server": "webpack --config webpack.server.config.js --progress --color", @@ -144,7 +145,7 @@ "@types/node": "^14.14.9", "codelyzer": "^6.0.1", "compression-webpack-plugin": "^3.0.1", - "copy-webpack-plugin": "^5.1.1", + "copy-webpack-plugin": "^6.4.1", "css-loader": "3.4.0", "cssnano": "^4.1.10", "deep-freeze": "0.0.1", @@ -181,7 +182,7 @@ "tslint": "^6.1.3", "typescript": "~4.0.5", "webpack": "^4.44.2", - "webpack-bundle-analyzer": "^3.3.2", + "webpack-bundle-analyzer": "^4.4.0", "webpack-cli": "^4.2.0", "webpack-node-externals": "1.7.2" } diff --git a/postcss.config.js b/postcss.config.js index 1c46e245ea..df092d1d39 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,7 +1,7 @@ module.exports = { plugins: [ require('postcss-import')(), - require('postcss-cssnext')(), + require('postcss-preset-env')(), require('postcss-apply')(), require('postcss-responsive-type')() ] diff --git a/scripts/set-env.ts b/scripts/set-env.ts index 5eee22a4be..b3516ae68f 100644 --- a/scripts/set-env.ts +++ b/scripts/set-env.ts @@ -10,7 +10,7 @@ const targetPath = './src/environments/environment.ts'; const colors = require('colors'); require('dotenv').config(); const merge = require('deepmerge'); - +const mergeOptions = { arrayMerge: (destinationArray, sourceArray, options) => sourceArray }; const environment = process.argv[2]; let environmentFilePath; let production = false; @@ -45,10 +45,10 @@ const processEnv = { } as GlobalConfig; import(environmentFilePath) - .then((file) => generateEnvironmentFile(merge.all([commonEnv, file.environment, processEnv]))) + .then((file) => generateEnvironmentFile(merge.all([commonEnv, file.environment, processEnv], mergeOptions))) .catch(() => { console.log(colors.yellow.bold(`No specific environment file found for ` + environment)); - generateEnvironmentFile(merge(commonEnv, processEnv)) + generateEnvironmentFile(merge(commonEnv, processEnv, mergeOptions)) }); function generateEnvironmentFile(file: GlobalConfig): void { @@ -65,7 +65,7 @@ function generateEnvironmentFile(file: GlobalConfig): void { } // allow to override a few important options by environment variables -function createServerConfig(host?: string, port?: string, nameSpace?: string, ssl?: string): ServerConfig { +function createServerConfig(host?: string, port?: string, nameSpace?: string, ssl?: string): ServerConfig { const result = {} as any; if (hasValue(host)) { result.host = host; diff --git a/scripts/sync-build-dir.js b/scripts/sync-build-dir.js deleted file mode 100644 index c147f139a5..0000000000 --- a/scripts/sync-build-dir.js +++ /dev/null @@ -1,22 +0,0 @@ -const syncBuildDir = require('copyfiles'); -const path = require('path'); -const { - projectRoot, - theme, - themePath, -} = require('../webpack/helpers'); - -const projectDepth = projectRoot('./').split(path.sep).length; - -let callback; - -if (theme !== null && theme !== undefined) { - callback = () => { - syncBuildDir([path.join(themePath, '**/*'), 'build'], { up: projectDepth + 2 }, () => {}) - } -} -else { - callback = () => {}; -} - -syncBuildDir([projectRoot('src/**/*'), 'build'], { up: projectDepth + 1 }, callback); diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts index 81e9513433..7984fc50d1 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts @@ -143,7 +143,9 @@ export class GroupFormComponent implements OnInit, OnDestroy { initialisePage() { this.subs.push(this.route.params.subscribe((params) => { - this.setActiveGroup(params.groupId); + if (params.groupId !== 'newGroup') { + this.setActiveGroup(params.groupId); + } })); this.canEdit$ = this.groupDataService.getActiveGroup().pipe( hasValueOperator(), @@ -225,14 +227,12 @@ export class GroupFormComponent implements OnInit, OnDestroy { { value: this.groupDescription.value } - ], + ] }, }; if (group === null) { - console.log('createNewGroup', values); this.createNewGroup(values); } else { - console.log('editGroup', group); this.editGroup(group); } } diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html index 0ac67aff75..8e2d23f8d5 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html @@ -24,10 +24,10 @@ - @@ -42,23 +42,23 @@ - - {{ePerson.id}} - {{ePerson.name}} + + {{ePerson.eperson.id}} + {{ePerson.eperson.name}}
- -
@@ -70,7 +70,7 @@
- @@ -45,7 +45,6 @@ {{messagePrefix + 'table.id' | translate}} {{messagePrefix + 'table.name' | translate}} {{messagePrefix + 'table.members' | translate}} - {{messagePrefix + 'table.edit' | translate}} @@ -53,8 +52,7 @@ {{groupDto.group.id}} {{groupDto.group.name}} - {{(getMembers(groupDto.group) | async)?.payload?.totalElements + (getSubgroups(groupDto.group) | async)?.payload?.totalElements}} - + {{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}
diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts index db5b1d3e3b..305da75eeb 100644 --- a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts @@ -26,7 +26,7 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, - getAllSucceededRemoteData + getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { PageInfo } from '../../../core/shared/page-info.model'; import { hasValue } from '../../../shared/empty.util'; @@ -55,15 +55,12 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { currentPage: 1 }); - /** - * A list of all the current Groups within the repository or the result of the search - */ - groups$: BehaviorSubject>> = new BehaviorSubject>>({} as any); /** * A BehaviorSubject with the list of GroupDtoModel objects made from the Groups in the repository or * as the result of the search */ groupsDto$: BehaviorSubject> = new BehaviorSubject>({} as any); + deletedGroupsIds: string[] = []; /** * An observable for the pageInfo, needed to pass to the pagination component @@ -104,30 +101,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { ngOnInit() { this.search({ query: this.currentSearchQuery }); - - this.subs.push(this.groups$.pipe( - getAllSucceededRemoteDataPayload(), - switchMap((groups: PaginatedList) => { - return observableCombineLatest(groups.page.map((group: Group) => { - return observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined), - this.hasLinkedDSO(group) - ]).pipe( - map(([isAuthorized, hasLinkedDSO]: boolean[]) => { - const groupDtoModel: GroupDtoModel = new GroupDtoModel(); - groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO; - groupDtoModel.group = group; - return groupDtoModel; - } - ) - ); - })).pipe(map((dtos: GroupDtoModel[]) => { - return buildPaginatedList(groups.pageInfo, dtos); - })); - })).subscribe((value: PaginatedList) => { - this.groupsDto$.next(value); - this.pageInfoState$.next(value.pageInfo); - })); } /** @@ -154,14 +127,42 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { this.searchSub.unsubscribe(); this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub); } + this.searchSub = this.groupService.searchGroups(this.currentSearchQuery.trim(), { currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize }).pipe( - getAllSucceededRemoteData() - ).subscribe((groupsRD: RemoteData>) => { - this.groups$.next(groupsRD); - this.pageInfoState$.next(groupsRD.payload.pageInfo); + getAllSucceededRemoteDataPayload(), + switchMap((groups: PaginatedList) => { + if (groups.page.length === 0) { + return observableOf(buildPaginatedList(groups.pageInfo, [])); + } + return observableCombineLatest(groups.page.map((group: Group) => { + if (!this.deletedGroupsIds.includes(group.id)) { + return observableCombineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined), + this.hasLinkedDSO(group), + this.getSubgroups(group), + this.getMembers(group) + ]).pipe( + map(([isAuthorized, hasLinkedDSO, subgroups, members]: + [boolean, boolean, RemoteData>, RemoteData>]) => { + const groupDtoModel: GroupDtoModel = new GroupDtoModel(); + groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO; + groupDtoModel.group = group; + groupDtoModel.subgroups = subgroups.payload; + groupDtoModel.epersons = members.payload; + return groupDtoModel; + } + ) + ); + } + })).pipe(map((dtos: GroupDtoModel[]) => { + return buildPaginatedList(groups.pageInfo, dtos); + })); + })).subscribe((value: PaginatedList) => { + this.groupsDto$.next(value); + this.pageInfoState$.next(value.pageInfo); }); this.subs.push(this.searchSub); } @@ -169,16 +170,17 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { /** * Delete Group */ - deleteGroup(group: Group) { - if (hasValue(group.id)) { - this.groupService.delete(group.id).pipe(getFirstCompletedRemoteData()) + deleteGroup(group: GroupDtoModel) { + if (hasValue(group.group.id)) { + this.groupService.delete(group.group.id).pipe(getFirstCompletedRemoteData()) .subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name })); + this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id]; + this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name })); this.reset(); } else { this.notificationsService.error( - this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.name }), + this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }), this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: rd.errorMessage })); } }); @@ -201,7 +203,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { * @param group */ getMembers(group: Group): Observable>> { - return this.ePersonDataService.findAllByHref(group._links.epersons.href); + return this.ePersonDataService.findAllByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData()); } /** @@ -209,7 +211,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { * @param group */ getSubgroups(group: Group): Observable>> { - return this.groupService.findAllByHref(group._links.subgroups.href); + return this.groupService.findAllByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData()); } /** @@ -218,6 +220,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { */ hasLinkedDSO(group: Group): Observable { return this.dSpaceObjectDataService.findByHref(group._links.object.href).pipe( + getFirstSucceededRemoteData(), map((rd: RemoteData) => hasValue(rd) && hasValue(rd.payload)), catchError(() => observableOf(false)), ); @@ -233,15 +236,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { this.search({ query: '' }); } - /** - * Extract optional UUID from a group name => To be resolved to community or collection with link - * (Or will be resolved in backend and added to group object, tbd) //TODO - * @param groupName - */ - getOptionalComColFromName(groupName: string): string { - return this.groupService.getUUIDFromString(groupName); - } - /** * Unsub all subscriptions */ diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts index ccd893e6f0..dedada5f5f 100644 --- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts +++ b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts @@ -16,6 +16,8 @@ import { RouterTestingModule } from '@angular/router/testing'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component'; import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; +import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; +import { ThemeService } from '../../../../../shared/theme-support/theme.service'; describe('ItemAdminSearchResultGridElementComponent', () => { let component: ItemAdminSearchResultGridElementComponent; @@ -29,6 +31,8 @@ describe('ItemAdminSearchResultGridElementComponent', () => { } }; + const mockThemeService = getMockThemeService(); + function init() { id = '780b2588-bda5-4112-a1cd-0b15000a5339'; searchResult = new ItemSearchResult(); @@ -50,6 +54,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => { providers: [ { provide: TruncatableService, useValue: mockTruncatableService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService }, + { provide: ThemeService, useValue: mockThemeService }, ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts index 68ef5ffc5e..13158204c5 100644 --- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts +++ b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts @@ -12,6 +12,7 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; +import { ThemeService } from '../../../../../shared/theme-support/theme.service'; @listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch) @Component({ @@ -29,6 +30,7 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE constructor(protected truncatableService: TruncatableService, protected bitstreamDataService: BitstreamDataService, + private themeService: ThemeService, private componentFactoryResolver: ComponentFactoryResolver ) { super(truncatableService, bitstreamDataService); @@ -63,6 +65,6 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE * @returns {GenericConstructor} */ private getComponent(): GenericConstructor { - return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined); + return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined, this.themeService.getThemeName()); } } diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts index 218cbc0ca2..56e25264cf 100644 --- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts +++ b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts @@ -11,6 +11,8 @@ import { Collection } from '../../../../../core/shared/collection.model'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; describe('CollectionAdminSearchResultListElementComponent', () => { let component: CollectionAdminSearchResultListElementComponent; @@ -33,7 +35,8 @@ describe('CollectionAdminSearchResultListElementComponent', () => { RouterTestingModule.withRoutes([]) ], declarations: [CollectionAdminSearchResultListElementComponent], - providers: [{ provide: TruncatableService, useValue: {} }], + providers: [{ provide: TruncatableService, useValue: {} }, + { provide: DSONameService, useClass: DSONameServiceMock }], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts index 69fe8856dd..29d9925326 100644 --- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts +++ b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts @@ -11,6 +11,8 @@ import { CommunityAdminSearchResultListElementComponent } from './community-admi import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model'; import { Community } from '../../../../../core/shared/community.model'; import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; describe('CommunityAdminSearchResultListElementComponent', () => { let component: CommunityAdminSearchResultListElementComponent; @@ -33,7 +35,8 @@ describe('CommunityAdminSearchResultListElementComponent', () => { RouterTestingModule.withRoutes([]) ], declarations: [CommunityAdminSearchResultListElementComponent], - providers: [{ provide: TruncatableService, useValue: {} }], + providers: [{ provide: TruncatableService, useValue: {} }, + { provide: DSONameService, useClass: DSONameServiceMock }], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts index 1d2f25c2eb..3774a07757 100644 --- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts +++ b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts @@ -8,6 +8,8 @@ import { RouterTestingModule } from '@angular/router/testing'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component'; import { Item } from '../../../../../core/shared/item.model'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; describe('ItemAdminSearchResultListElementComponent', () => { let component: ItemAdminSearchResultListElementComponent; @@ -30,7 +32,8 @@ describe('ItemAdminSearchResultListElementComponent', () => { RouterTestingModule.withRoutes([]) ], declarations: [ItemAdminSearchResultListElementComponent], - providers: [{ provide: TruncatableService, useValue: {} }], + providers: [{ provide: TruncatableService, useValue: {} }, + { provide: DSONameService, useClass: DSONameServiceMock }], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); diff --git a/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts b/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts index 52a9366ecc..2696e621de 100644 --- a/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts +++ b/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts @@ -56,19 +56,19 @@ describe('ItemAdminSearchResultActionsComponent', () => { it('should render an edit button with the correct link', () => { const button = fixture.debugElement.query(By.css('a.edit-link')); const link = button.nativeElement.href; - expect(link).toContain(getItemEditRoute(id)); + expect(link).toContain(getItemEditRoute(item)); }); it('should render a delete button with the correct link', () => { const button = fixture.debugElement.query(By.css('a.delete-link')); const link = button.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_DELETE_PATH).toString()); + expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_DELETE_PATH).toString()); }); it('should render a move button with the correct link', () => { const a = fixture.debugElement.query(By.css('a.move-link')); const link = a.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_MOVE_PATH).toString()); + expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_MOVE_PATH).toString()); }); describe('when the item is not withdrawn', () => { @@ -80,7 +80,7 @@ describe('ItemAdminSearchResultActionsComponent', () => { it('should render a withdraw button with the correct link', () => { const a = fixture.debugElement.query(By.css('a.withdraw-link')); const link = a.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_WITHDRAW_PATH).toString()); + expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_WITHDRAW_PATH).toString()); }); it('should not render a reinstate button with the correct link', () => { @@ -103,7 +103,7 @@ describe('ItemAdminSearchResultActionsComponent', () => { it('should render a reinstate button with the correct link', () => { const a = fixture.debugElement.query(By.css('a.reinstate-link')); const link = a.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_REINSTATE_PATH).toString()); + expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_REINSTATE_PATH).toString()); }); }); @@ -116,7 +116,7 @@ describe('ItemAdminSearchResultActionsComponent', () => { it('should render a make private button with the correct link', () => { const a = fixture.debugElement.query(By.css('a.private-link')); const link = a.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_PRIVATE_PATH).toString()); + expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_PRIVATE_PATH).toString()); }); it('should not render a make public button with the correct link', () => { @@ -139,7 +139,7 @@ describe('ItemAdminSearchResultActionsComponent', () => { it('should render a make private button with the correct link', () => { const a = fixture.debugElement.query(By.css('a.public-link')); const link = a.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_PUBLIC_PATH).toString()); + expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_PUBLIC_PATH).toString()); }); }); }); diff --git a/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts b/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts index 3142f5b11a..cded519796 100644 --- a/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts +++ b/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts @@ -34,7 +34,7 @@ export class ItemAdminSearchResultActionsComponent { * Returns the path to the edit page of this item */ getEditRoute(): string { - return getItemEditRoute(this.item.uuid); + return getItemEditRoute(this.item); } /** diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.scss b/src/app/+admin/admin-sidebar/admin-sidebar.component.scss index 36355fcab9..e6eb4a7037 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.scss +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.scss @@ -1,12 +1,12 @@ -$icon-z-index: 10; - :host { + --ds-icon-z-index: 10; + left: 0; top: 0; height: 100vh; flex: 1 1 auto; nav { - background-color: $admin-sidebar-bg; + background-color: var(--ds-admin-sidebar-bg); height: 100%; flex-direction: column; > div { @@ -19,12 +19,12 @@ $icon-z-index: 10; } &.inactive ::ng-deep .sidebar-collapsible { - margin-left: -#{$sidebar-items-width}; + margin-left: calc(-1 * var(--ds-sidebar-items-width)); } .navbar-nav { .admin-menu-header { - background-color: $admin-sidebar-header-bg; + background-color: var(--ds-admin-sidebar-header-bg); .logo-wrapper { img { height: 20px; @@ -43,29 +43,29 @@ $icon-z-index: 10; .sidebar-section { display: flex; align-content: stretch; - background-color: $admin-sidebar-bg; + background-color: var(--ds-admin-sidebar-bg); .nav-item { - padding-top: $spacer; - padding-bottom: $spacer; + padding-top: var(--bs-spacer); + padding-bottom: var(--bs-spacer); } .shortcut-icon { - padding-left: $icon-padding; - padding-right: $icon-padding; + padding-left: var(--ds-icon-padding); + padding-right: var(--ds-icon-padding); } .shortcut-icon, .icon-wrapper { background-color: inherit; - z-index: $icon-z-index; + z-index: var(--ds-icon-z-index); } .sidebar-collapsible { - width: $sidebar-items-width; + width: var(--ds-sidebar-items-width); position: relative; a { - padding-right: $spacer; + padding-right: var(--bs-spacer); width: 100%; } } &.active > .sidebar-collapsible > .nav-link { - color: $navbar-dark-active-color; + color: var(--bs-navbar-dark-active-color); } } } @@ -73,4 +73,4 @@ $icon-z-index: 10; } -} \ No newline at end of file +} diff --git a/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.scss b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.scss index 1f6e288608..e3a807a14f 100644 --- a/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.scss +++ b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.scss @@ -1,13 +1,13 @@ :host ::ng-deep { .fa-chevron-right { - padding-left: $spacer/2; + padding-left: calc(var(--bs-spacer) / 2); font-size: 0.5rem; line-height: 3; } .sidebar-sub-level-items { list-style: disc; - color: $navbar-dark-color; + color: var(--bs-navbar-dark-color); overflow: hidden; } diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts index fe3e0ff3de..0b933d0859 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts @@ -19,6 +19,8 @@ import { BitstreamDataService } from '../../../../../core/data/bitstream-data.se import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock'; import { of as observableOf } from 'rxjs'; +import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; +import { ThemeService } from '../../../../../shared/theme-support/theme.service'; describe('WorkflowItemAdminWorkflowGridElementComponent', () => { let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent; @@ -28,6 +30,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { let itemRD$; let linkService; let object; + let themeService; function init() { itemRD$ = createSuccessfulRemoteDataObject$(new Item()); @@ -37,6 +40,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { wfi.item = itemRD$; object.indexableObject = wfi; linkService = getMockLinkService(); + themeService = getMockThemeService(); } beforeEach(waitForAsync(() => { @@ -51,6 +55,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { ], providers: [ { provide: LinkService, useValue: linkService }, + { provide: ThemeService, useValue: themeService }, { provide: TruncatableService, useValue: { isCollapsed: () => observableOf(true), diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts index 67a961b330..cf5746391c 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts @@ -1,7 +1,10 @@ import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { + getListableObjectComponent, + listableObjectComponent +} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { Context } from '../../../../../core/shared/context.model'; import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; @@ -13,9 +16,13 @@ import { Observable } from 'rxjs'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { followLink } from '../../../../../shared/utils/follow-link-config.model'; import { RemoteData } from '../../../../../core/data/remote-data'; -import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; +import { + getAllSucceededRemoteData, + getRemoteDataPayload +} from '../../../../../core/shared/operators'; import { take } from 'rxjs/operators'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; +import { ThemeService } from '../../../../../shared/theme-support/theme.service'; @listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @Component({ @@ -51,6 +58,7 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S private componentFactoryResolver: ComponentFactoryResolver, private linkService: LinkService, protected truncatableService: TruncatableService, + private themeService: ThemeService, protected bitstreamDataService: BitstreamDataService ) { super(truncatableService, bitstreamDataService); @@ -92,7 +100,7 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S * @returns {GenericConstructor} */ private getComponent(item: Item): GenericConstructor { - return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined); + return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined, this.themeService.getThemeName()); } } diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts index 47907999a2..a792a606e9 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts @@ -16,6 +16,8 @@ import { Item } from '../../../../../core/shared/item.model'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; describe('WorkflowItemAdminWorkflowListElementComponent', () => { let component: WorkflowItemSearchResultAdminWorkflowListElementComponent; @@ -49,6 +51,7 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => { providers: [ { provide: TruncatableService, useValue: mockTruncatableService }, { provide: LinkService, useValue: linkService }, + { provide: DSONameService, useClass: DSONameServiceMock } ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts index 80225db09f..3dd17faf25 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts @@ -12,6 +12,7 @@ import { Item } from '../../../../../core/shared/item.model'; import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; @listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) @Component({ @@ -29,8 +30,11 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S */ public item$: Observable; - constructor(private linkService: LinkService, protected truncatableService: TruncatableService) { - super(truncatableService); + constructor(private linkService: LinkService, + protected truncatableService: TruncatableService, + protected dsoNameService: DSONameService + ) { + super(truncatableService, dsoNameService); } /** diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss index d212b5347c..13de59700c 100644 --- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss @@ -2,7 +2,7 @@ ::ng-deep { .switch { position: absolute; - top: $spacer*2.5; + top: calc(var(--bs-spacer) * 2.5); } } } diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index 49fdfc6115..2e7eb4e1d1 100644 --- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -18,10 +18,14 @@ import { hasValue } from '../../shared/empty.util'; import { FormControl, FormGroup } from '@angular/forms'; import { FileSizePipe } from '../../shared/utils/file-size-pipe'; import { VarDirective } from '../../shared/utils/var.directive'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../shared/remote-data.utils'; import { RouterStub } from '../../shared/testing/router.stub'; -import { getItemEditRoute } from '../../+item-page/item-page-routing-paths'; +import { getEntityEditRoute, getItemEditRoute } from '../../+item-page/item-page-routing-paths'; import { createPaginatedList } from '../../shared/testing/utils.test'; +import { Item } from '../../core/shared/item.model'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -109,9 +113,9 @@ describe('EditBitstreamPageComponent', () => { self: 'bitstream-selflink' }, bundle: createSuccessfulRemoteDataObject$({ - item: createSuccessfulRemoteDataObject$({ + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { uuid: 'some-uuid' - }) + })) }) }); bitstreamService = jasmine.createSpyObj('bitstreamService', { @@ -237,14 +241,14 @@ describe('EditBitstreamPageComponent', () => { it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => { comp.itemId = 'some-uuid1'; comp.navigateToItemEditBitstreams(); - expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute('some-uuid1'), 'bitstreams']); + expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']); }); }); describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => { it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => { comp.itemId = undefined; comp.navigateToItemEditBitstreams(); - expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute('some-uuid'), 'bitstreams']); + expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']); }); }); }); 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 8db4e28f50..8a4d584647 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 @@ -33,9 +33,8 @@ import { Metadata } from '../../core/shared/metadata.utils'; import { Location } from '@angular/common'; import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list.model'; -import { getItemEditRoute } from '../../+item-page/item-page-routing-paths'; +import { getEntityEditRoute, getItemEditRoute } from '../../+item-page/item-page-routing-paths'; import { Bundle } from '../../core/shared/bundle.model'; -import { Item } from '../../core/shared/item.model'; @Component({ selector: 'ds-edit-bitstream-page', @@ -264,9 +263,17 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { /** * The ID of the item the bitstream originates from * Taken from the current query parameters when present + * This will determine the route of the item edit page to return to */ itemId: string; + /** + * The entity type of the item the bitstream originates from + * Taken from the current query parameters when present + * This will determine the route of the item edit page to return to + */ + entityType: string; + /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} @@ -293,6 +300,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.formGroup = this.formService.createFormGroup(this.formModel); this.itemId = this.route.snapshot.queryParams.itemId; + this.entityType = this.route.snapshot.queryParams.entityType; this.bitstreamRD$ = this.route.data.pipe(map((data) => data.bitstream)); this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions); @@ -499,10 +507,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ navigateToItemEditBitstreams() { if (hasValue(this.itemId)) { - this.router.navigate([getItemEditRoute(this.itemId), 'bitstreams']); + this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']); } else { this.bitstream.bundle.pipe(getFirstSucceededRemoteDataPayload(), - mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload(), map((item: Item) => item.uuid)))) + mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload()))) .subscribe((item) => { this.router.navigate(([getItemEditRoute(item), 'bitstreams'])); }); diff --git a/src/app/+browse-by/+browse-by-switcher/themed-browse-by-switcher.component.ts b/src/app/+browse-by/+browse-by-switcher/themed-browse-by-switcher.component.ts new file mode 100644 index 0000000000..e92fe30ba9 --- /dev/null +++ b/src/app/+browse-by/+browse-by-switcher/themed-browse-by-switcher.component.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { BrowseBySwitcherComponent } from './browse-by-switcher.component'; + +/** + * Themed wrapper for BrowseBySwitcherComponent + */ +@Component({ + selector: 'ds-themed-browse-by-switcher', + styleUrls: [], + templateUrl: '../../shared/theme-support/themed.component.html' +}) +export class ThemedBrowseBySwitcherComponent extends ThemedComponent { + protected getComponentName(): string { + return 'BrowseBySwitcherComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/+browse-by/+browse-by-switcher/browse-by-switcher.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./browse-by-switcher.component`); + } + + +} diff --git a/src/app/+browse-by/browse-by-routing.module.ts b/src/app/+browse-by/browse-by-routing.module.ts index a686e7007e..8cf989695a 100644 --- a/src/app/+browse-by/browse-by-routing.module.ts +++ b/src/app/+browse-by/browse-by-routing.module.ts @@ -1,9 +1,9 @@ import { RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; import { BrowseByGuard } from './browse-by-guard'; -import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component'; import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver'; import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver'; +import { ThemedBrowseBySwitcherComponent } from './+browse-by-switcher/themed-browse-by-switcher.component'; @NgModule({ imports: [ @@ -14,7 +14,7 @@ import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.reso children: [ { path: ':id', - component: BrowseBySwitcherComponent, + component: ThemedBrowseBySwitcherComponent, canActivate: [BrowseByGuard], resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver }, data: { title: 'browse.title', breadcrumbKey: 'browse.metadata' } diff --git a/src/app/+browse-by/browse-by.module.ts b/src/app/+browse-by/browse-by.module.ts index 60574f4074..08b6c5739b 100644 --- a/src/app/+browse-by/browse-by.module.ts +++ b/src/app/+browse-by/browse-by.module.ts @@ -5,6 +5,7 @@ import { SharedModule } from '../shared/shared.module'; import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component'; import { BrowseByDatePageComponent } from './+browse-by-date-page/browse-by-date-page.component'; import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component'; +import { ThemedBrowseBySwitcherComponent } from './+browse-by-switcher/themed-browse-by-switcher.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -20,6 +21,7 @@ const ENTRY_COMPONENTS = [ ], declarations: [ BrowseBySwitcherComponent, + ThemedBrowseBySwitcherComponent, ...ENTRY_COMPONENTS ], exports: [ diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 7e44883a53..5879e523af 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { CollectionPageComponent } from './collection-page.component'; import { CollectionPageResolver } from './collection-page.resolver'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; @@ -21,6 +20,7 @@ import { import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard'; import { MenuItemType } from '../shared/menu/initial-menus-state'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; +import { ThemedCollectionPageComponent } from './themed-collection-page.component'; @NgModule({ imports: [ @@ -62,7 +62,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; }, { path: '', - component: CollectionPageComponent, + component: ThemedCollectionPageComponent, pathMatch: 'full', } ], diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index beb7413415..bbe2cb5e66 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -35,7 +35,7 @@
- +
diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index 8065480604..5d76ce7859 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -15,13 +15,19 @@ import { Bitstream } from '../core/shared/bitstream.model'; import { Collection } from '../core/shared/collection.model'; import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; import { Item } from '../core/shared/item.model'; -import { getFirstSucceededRemoteData, redirectOn4xx, toDSpaceObjectListRD } from '../core/shared/operators'; +import { + getAllSucceededRemoteDataPayload, + getFirstSucceededRemoteData, + redirectOn4xx, + toDSpaceObjectListRD +} from '../core/shared/operators'; import { fadeIn, fadeInOut } from '../shared/animations/fade'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { AuthService } from '../core/auth/auth.service'; import {PaginationChangeEvent} from '../shared/pagination/paginationChangeEvent.interface'; +import { getCollectionPageRoute } from './collection-page-routing-paths'; @Component({ selector: 'ds-collection-page', @@ -44,6 +50,11 @@ export class CollectionPageComponent implements OnInit { sortConfig: SortOptions }>; + /** + * Route to the community page + */ + collectionPageRoute$: Observable; + constructor( private collectionDataService: CollectionDataService, private searchService: SearchService, @@ -94,6 +105,11 @@ export class CollectionPageComponent implements OnInit { ) ); + this.collectionPageRoute$ = this.collectionRD$.pipe( + getAllSucceededRemoteDataPayload(), + map((collection) => getCollectionPageRoute(collection.id)) + ); + this.route.queryParams.pipe(take(1)).subscribe((params) => { this.metadata.processRemoteData(this.collectionRD$); }); diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/+collection-page/collection-page.module.ts index 83ba12f037..7946e7a1a4 100644 --- a/src/app/+collection-page/collection-page.module.ts +++ b/src/app/+collection-page/collection-page.module.ts @@ -13,6 +13,7 @@ import { CollectionItemMapperComponent } from './collection-item-mapper/collecti import { SearchService } from '../core/shared/search/search.service'; import { StatisticsModule } from '../statistics/statistics.module'; import { CollectionFormModule } from './collection-form/collection-form.module'; +import { ThemedCollectionPageComponent } from './themed-collection-page.component'; @NgModule({ imports: [ @@ -25,6 +26,7 @@ import { CollectionFormModule } from './collection-form/collection-form.module'; ], declarations: [ CollectionPageComponent, + ThemedCollectionPageComponent, CreateCollectionPageComponent, DeleteCollectionPageComponent, EditItemTemplatePageComponent, diff --git a/src/app/+collection-page/collection-page.resolver.spec.ts b/src/app/+collection-page/collection-page.resolver.spec.ts index 5ded339fb8..4b1ea9834c 100644 --- a/src/app/+collection-page/collection-page.resolver.spec.ts +++ b/src/app/+collection-page/collection-page.resolver.spec.ts @@ -6,17 +6,21 @@ describe('CollectionPageResolver', () => { describe('resolve', () => { let resolver: CollectionPageResolver; let collectionService: any; + let store: any; const uuid = '1234-65487-12354-1235'; beforeEach(() => { collectionService = { findById: (id: string) => createSuccessfulRemoteDataObject$({ id }) }; - resolver = new CollectionPageResolver(collectionService); + store = jasmine.createSpyObj('store', { + dispatch: {}, + }); + resolver = new CollectionPageResolver(collectionService, store); }); it('should resolve a collection with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/+collection-page/collection-page.resolver.ts b/src/app/+collection-page/collection-page.resolver.ts index 44d238fc97..f6f87f117c 100644 --- a/src/app/+collection-page/collection-page.resolver.ts +++ b/src/app/+collection-page/collection-page.resolver.ts @@ -4,15 +4,31 @@ import { Collection } from '../core/shared/collection.model'; import { Observable } from 'rxjs'; import { CollectionDataService } from '../core/data/collection-data.service'; import { RemoteData } from '../core/data/remote-data'; -import { followLink } from '../shared/utils/follow-link-config.model'; +import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { Store } from '@ngrx/store'; +import { ResolvedAction } from '../core/resolving/resolver.actions'; + +/** + * 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 COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ + followLink('parentCommunity', undefined, true, true, true, + followLink('parentCommunity') + ), + followLink('logo') +]; /** * This class represents a resolver that requests a specific collection before the route is activated */ @Injectable() export class CollectionPageResolver implements Resolve> { - constructor(private collectionService: CollectionDataService) { + constructor( + private collectionService: CollectionDataService, + private store: Store + ) { } /** @@ -23,8 +39,19 @@ export class CollectionPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.collectionService.findById(route.params.id, true, false, followLink('logo')).pipe( + const collectionRD$ = this.collectionService.findById( + route.params.id, + true, + false, + ...COLLECTION_PAGE_LINKS_TO_FOLLOW + ).pipe( getFirstCompletedRemoteData() ); + + collectionRD$.subscribe((collectionRD: RemoteData) => { + this.store.dispatch(new ResolvedAction(state.url, collectionRD.payload)); + }); + + return collectionRD$; } } diff --git a/src/app/+collection-page/themed-collection-page.component.ts b/src/app/+collection-page/themed-collection-page.component.ts new file mode 100644 index 0000000000..4ad9ed87e3 --- /dev/null +++ b/src/app/+collection-page/themed-collection-page.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { CollectionPageComponent } from './collection-page.component'; + +/** + * Themed wrapper for CollectionPageComponent + */ +@Component({ + selector: 'ds-themed-community-page', + styleUrls: [], + templateUrl: '../shared/theme-support/themed.component.html', +}) +export class ThemedCollectionPageComponent extends ThemedComponent { + protected getComponentName(): string { + return 'CollectionPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/+collection-page/collection-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./collection-page.component`); + } + +} diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 571bff7413..ad1b1fd2f2 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { CommunityPageComponent } from './community-page.component'; import { CommunityPageResolver } from './community-page.resolver'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; @@ -14,6 +13,7 @@ import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-rou import { CommunityPageAdministratorGuard } from './community-page-administrator.guard'; import { MenuItemType } from '../shared/menu/initial-menus-state'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; +import { ThemedCommunityPageComponent } from './themed-community-page.component'; @NgModule({ imports: [ @@ -45,7 +45,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; }, { path: '', - component: CommunityPageComponent, + component: ThemedCommunityPageComponent, pathMatch: 'full', } ], diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index 418e69ed10..1b4f90a52b 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -21,7 +21,7 @@
- +
diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index e4812e6514..7ba8dfc6c0 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -13,8 +13,9 @@ import { MetadataService } from '../core/metadata/metadata.service'; import { fadeInOut } from '../shared/animations/fade'; import { hasValue } from '../shared/empty.util'; -import { redirectOn4xx } from '../core/shared/operators'; +import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../core/shared/operators'; import { AuthService } from '../core/auth/auth.service'; +import { getCommunityPageRoute } from './community-page-routing-paths'; @Component({ selector: 'ds-community-page', @@ -36,6 +37,12 @@ export class CommunityPageComponent implements OnInit { * The logo of this community */ logoRD$: Observable>; + + /** + * Route to the community page + */ + communityPageRoute$: Observable; + constructor( private communityDataService: CommunityDataService, private metadata: MetadataService, @@ -55,6 +62,10 @@ export class CommunityPageComponent implements OnInit { map((rd: RemoteData) => rd.payload), filter((community: Community) => hasValue(community)), mergeMap((community: Community) => community.logo)); + this.communityPageRoute$ = this.communityRD$.pipe( + getAllSucceededRemoteDataPayload(), + map((community) => getCommunityPageRoute(community.id)) + ); } } diff --git a/src/app/+community-page/community-page.module.ts b/src/app/+community-page/community-page.module.ts index a9eaef033d..3ae75f166c 100644 --- a/src/app/+community-page/community-page.module.ts +++ b/src/app/+community-page/community-page.module.ts @@ -11,6 +11,14 @@ import { CreateCommunityPageComponent } from './create-community-page/create-com import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; import { StatisticsModule } from '../statistics/statistics.module'; import { CommunityFormModule } from './community-form/community-form.module'; +import { ThemedCommunityPageComponent } from './themed-community-page.component'; + +const DECLARATIONS = [CommunityPageComponent, + ThemedCommunityPageComponent, + CommunityPageSubCollectionListComponent, + CommunityPageSubCommunityListComponent, + CreateCommunityPageComponent, + DeleteCommunityPageComponent]; @NgModule({ imports: [ @@ -21,11 +29,10 @@ import { CommunityFormModule } from './community-form/community-form.module'; CommunityFormModule ], declarations: [ - CommunityPageComponent, - CommunityPageSubCollectionListComponent, - CommunityPageSubCommunityListComponent, - CreateCommunityPageComponent, - DeleteCommunityPageComponent + ...DECLARATIONS + ], + exports: [ + ...DECLARATIONS ] }) diff --git a/src/app/+community-page/community-page.resolver.spec.ts b/src/app/+community-page/community-page.resolver.spec.ts index e75f5ad57e..f181dbfff6 100644 --- a/src/app/+community-page/community-page.resolver.spec.ts +++ b/src/app/+community-page/community-page.resolver.spec.ts @@ -6,17 +6,21 @@ describe('CommunityPageResolver', () => { describe('resolve', () => { let resolver: CommunityPageResolver; let communityService: any; + let store: any; const uuid = '1234-65487-12354-1235'; beforeEach(() => { communityService = { findById: (id: string) => createSuccessfulRemoteDataObject$({ id }) }; - resolver = new CommunityPageResolver(communityService); + store = jasmine.createSpyObj('store', { + dispatch: {}, + }); + resolver = new CommunityPageResolver(communityService, store); }); it('should resolve a community with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/+community-page/community-page.resolver.ts b/src/app/+community-page/community-page.resolver.ts index c5780f0a0f..01de9294f3 100644 --- a/src/app/+community-page/community-page.resolver.ts +++ b/src/app/+community-page/community-page.resolver.ts @@ -4,15 +4,31 @@ import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { Community } from '../core/shared/community.model'; import { CommunityDataService } from '../core/data/community-data.service'; -import { followLink } from '../shared/utils/follow-link-config.model'; +import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { ResolvedAction } from '../core/resolving/resolver.actions'; +import { Store } from '@ngrx/store'; + +/** + * 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 COMMUNITY_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ + followLink('logo'), + followLink('subcommunities'), + followLink('collections'), + followLink('parentCommunity') +]; /** * This class represents a resolver that requests a specific community before the route is activated */ @Injectable() export class CommunityPageResolver implements Resolve> { - constructor(private communityService: CommunityDataService) { + constructor( + private communityService: CommunityDataService, + private store: Store + ) { } /** @@ -23,15 +39,19 @@ export class CommunityPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.communityService.findById( + const communityRD$ = this.communityService.findById( route.params.id, true, false, - followLink('logo'), - followLink('subcommunities'), - followLink('collections') + ...COMMUNITY_PAGE_LINKS_TO_FOLLOW ).pipe( getFirstCompletedRemoteData(), ); + + communityRD$.subscribe((communityRD: RemoteData) => { + this.store.dispatch(new ResolvedAction(state.url, communityRD.payload)); + }); + + return communityRD$; } } diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts index 73e96fcc9c..13a91563c8 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -18,11 +18,14 @@ import { PageInfo } from '../../core/shared/page-info.model'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; +import { ThemeService } from '../../shared/theme-support/theme.service'; describe('CommunityPageSubCollectionList Component', () => { let comp: CommunityPageSubCollectionListComponent; let fixture: ComponentFixture; let collectionDataServiceStub: any; + let themeService; let subCollList = []; const collections = [Object.assign(new Community(), { @@ -110,6 +113,8 @@ describe('CommunityPageSubCollectionList Component', () => { } }; + themeService = getMockThemeService(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -124,6 +129,7 @@ describe('CommunityPageSubCollectionList Component', () => { { provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: SelectableListService, useValue: {} }, + { provide: ThemeService, useValue: themeService }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index b52df64db7..21ba1b28b0 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -18,11 +18,14 @@ import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { CommunityDataService } from '../../core/data/community-data.service'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; +import { ThemeService } from '../../shared/theme-support/theme.service'; describe('CommunityPageSubCommunityListComponent Component', () => { let comp: CommunityPageSubCommunityListComponent; let fixture: ComponentFixture; let communityDataServiceStub: any; + let themeService; let subCommList = []; const subcommunities = [Object.assign(new Community(), { @@ -111,6 +114,8 @@ describe('CommunityPageSubCommunityListComponent Component', () => { } }; + themeService = getMockThemeService(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -125,6 +130,7 @@ describe('CommunityPageSubCommunityListComponent Component', () => { { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: SelectableListService, useValue: {} }, + { provide: ThemeService, useValue: themeService }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+community-page/themed-community-page.component.ts b/src/app/+community-page/themed-community-page.component.ts new file mode 100644 index 0000000000..97dd59821c --- /dev/null +++ b/src/app/+community-page/themed-community-page.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { CommunityPageComponent } from './community-page.component'; + +/** + * Themed wrapper for CommunityPageComponent + */ +@Component({ + selector: 'ds-themed-community-page', + styleUrls: [], + templateUrl: '../shared/theme-support/themed.component.html', +}) +export class ThemedCommunityPageComponent extends ThemedComponent { + protected getComponentName(): string { + return 'CommunityPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/+community-page/community-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./community-page.component`); + } + +} diff --git a/src/app/+home-page/home-news/home-news.component.scss b/src/app/+home-page/home-news/home-news.component.scss index 50d3619ad1..e58dd08ccf 100644 --- a/src/app/+home-page/home-news/home-news.component.scss +++ b/src/app/+home-page/home-news/home-news.component.scss @@ -1,7 +1,7 @@ :host { display: block; - margin-top: -$content-spacing; - margin-bottom: -$content-spacing; + margin-top: calc(-1 * var(--ds-content-spacing)); + margin-bottom: calc(-1 * var(--ds-content-spacing)); } .display-3 { @@ -11,4 +11,4 @@ .dspace-logo { height: 110px; width: 110px; -} \ No newline at end of file +} diff --git a/src/app/+home-page/home-news/home-news.component.ts b/src/app/+home-page/home-news/home-news.component.ts index cebe217623..62c8e94671 100644 --- a/src/app/+home-page/home-news/home-news.component.ts +++ b/src/app/+home-page/home-news/home-news.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, } from '@angular/core'; @Component({ selector: 'ds-home-news', @@ -10,5 +10,4 @@ import { Component } from '@angular/core'; * Component to render the news section on the home page */ export class HomeNewsComponent { - } diff --git a/themes/default/styles/_themed_bootstrap_variables.scss b/src/app/+home-page/home-news/themed-home-news.component.scss similarity index 100% rename from themes/default/styles/_themed_bootstrap_variables.scss rename to src/app/+home-page/home-news/themed-home-news.component.scss diff --git a/src/app/+home-page/home-news/themed-home-news.component.ts b/src/app/+home-page/home-news/themed-home-news.component.ts new file mode 100644 index 0000000000..09abbf8935 --- /dev/null +++ b/src/app/+home-page/home-news/themed-home-news.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { HomeNewsComponent } from './home-news.component'; + +@Component({ + selector: 'ds-themed-home-news', + styleUrls: [], + templateUrl: '../../shared/theme-support/themed.component.html', +}) + +/** + * Component to render the news section on the home page + */ +export class ThemedHomeNewsComponent extends ThemedComponent { + protected getComponentName(): string { + return 'HomeNewsComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/+home-page/home-news/home-news.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./home-news.component`); + } + +} diff --git a/src/app/+home-page/home-page-routing.module.ts b/src/app/+home-page/home-page-routing.module.ts index c94df56643..ec6a547359 100644 --- a/src/app/+home-page/home-page-routing.module.ts +++ b/src/app/+home-page/home-page-routing.module.ts @@ -1,17 +1,17 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { HomePageComponent } from './home-page.component'; import { HomePageResolver } from './home-page.resolver'; import { MenuItemType } from '../shared/menu/initial-menus-state'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; +import { ThemedHomePageComponent } from './themed-home-page.component'; @NgModule({ imports: [ RouterModule.forChild([ { path: '', - component: HomePageComponent, + component: ThemedHomePageComponent, pathMatch: 'full', data: { title: 'home.title', diff --git a/src/app/+home-page/home-page.component.html b/src/app/+home-page/home-page.component.html index 5515df595b..5902fa30af 100644 --- a/src/app/+home-page/home-page.component.html +++ b/src/app/+home-page/home-page.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/+home-page/home-page.module.ts b/src/app/+home-page/home-page.module.ts index 51e978bbfe..d304c78696 100644 --- a/src/app/+home-page/home-page.module.ts +++ b/src/app/+home-page/home-page.module.ts @@ -7,6 +7,16 @@ import { HomePageRoutingModule } from './home-page-routing.module'; import { HomePageComponent } from './home-page.component'; import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component'; import { StatisticsModule } from '../statistics/statistics.module'; +import { ThemedHomeNewsComponent } from './home-news/themed-home-news.component'; +import { ThemedHomePageComponent } from './themed-home-page.component'; + +const DECLARATIONS = [ + HomePageComponent, + ThemedHomePageComponent, + TopLevelCommunityListComponent, + ThemedHomeNewsComponent, + HomeNewsComponent, +]; @NgModule({ imports: [ @@ -16,10 +26,11 @@ import { StatisticsModule } from '../statistics/statistics.module'; StatisticsModule.forRoot() ], declarations: [ - HomePageComponent, - TopLevelCommunityListComponent, - HomeNewsComponent, - ] + ...DECLARATIONS, + ], + exports: [ + ...DECLARATIONS, + ], }) export class HomePageModule { diff --git a/src/app/+home-page/themed-home-page.component.ts b/src/app/+home-page/themed-home-page.component.ts new file mode 100644 index 0000000000..69d6a60908 --- /dev/null +++ b/src/app/+home-page/themed-home-page.component.ts @@ -0,0 +1,26 @@ +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { HomePageComponent } from './home-page.component'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-themed-home-page', + styleUrls: [], + templateUrl: '../shared/theme-support/themed.component.html', +}) +export class ThemedHomePageComponent extends ThemedComponent { + protected inAndOutputNames: (keyof HomePageComponent & keyof this)[]; + + + protected getComponentName(): string { + return 'HomePageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/+home-page/home-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./home-page.component`); + } + +} diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts index 4e0b8d4d22..0daa0a0ae0 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts @@ -18,11 +18,14 @@ import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { CommunityDataService } from '../../core/data/community-data.service'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; +import { ThemeService } from '../../shared/theme-support/theme.service'; describe('TopLevelCommunityList Component', () => { let comp: TopLevelCommunityListComponent; let fixture: ComponentFixture; let communityDataServiceStub: any; + let themeService; const topCommList = [Object.assign(new Community(), { id: '123456789-1', @@ -101,6 +104,8 @@ describe('TopLevelCommunityList Component', () => { } }; + themeService = getMockThemeService(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -115,6 +120,7 @@ describe('TopLevelCommunityList Component', () => { { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: SelectableListService, useValue: {} }, + { provide: ThemeService, useValue: themeService }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+import-external-page/import-external-routing.module.ts b/src/app/+import-external-page/import-external-routing.module.ts index 91cdbf9877..e5c6da5b21 100644 --- a/src/app/+import-external-page/import-external-routing.module.ts +++ b/src/app/+import-external-page/import-external-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { SubmissionImportExternalComponent } from '../submission/import-external/submission-import-external.component'; +import { ThemedSubmissionImportExternalComponent } from '../submission/import-external/themed-submission-import-external.component'; @NgModule({ imports: [ @@ -9,7 +9,7 @@ import { SubmissionImportExternalComponent } from '../submission/import-external { canActivate: [ AuthenticatedGuard ], path: '', - component: SubmissionImportExternalComponent, + component: ThemedSubmissionImportExternalComponent, pathMatch: 'full', data: { title: 'submission.import-external.page.title' diff --git a/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts index f1b5fadf12..7635758463 100644 --- a/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts +++ b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts @@ -17,7 +17,7 @@ import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operato import { UploaderComponent } from '../../../shared/uploader/uploader.component'; import { RequestService } from '../../../core/data/request.service'; import { getBitstreamModuleRoute } from '../../../app-routing-paths'; -import { getItemEditRoute } from '../../item-page-routing-paths'; +import { getEntityEditRoute } from '../../item-page-routing-paths'; @Component({ selector: 'ds-upload-bitstream', @@ -37,6 +37,12 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { */ itemId: string; + /** + * The entity type of the item + * This is fetched from the current URL and will determine the item's page route + */ + entityType: string; + /** * The item to upload a bitstream to */ @@ -100,6 +106,7 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { */ ngOnInit(): void { this.itemId = this.route.snapshot.params.id; + this.entityType = this.route.snapshot.params['entity-type']; this.itemRD$ = this.route.data.pipe(map((data) => data.dso)); this.bundlesRD$ = this.itemRD$.pipe( switchMap((itemRD: RemoteData) => itemRD.payload.bundles) @@ -167,7 +174,7 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { }); // Bring over the item ID as a query parameter - const queryParams = { itemId: this.itemId }; + const queryParams = { itemId: this.itemId, entityType: this.entityType }; this.router.navigate([getBitstreamModuleRoute(), bitstream.id, 'edit'], { queryParams: queryParams }); } @@ -193,7 +200,7 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { * When cancel is clicked, navigate back to the item's edit bitstreams page */ onCancel() { - this.router.navigate([getItemEditRoute(this.itemId), 'bitstreams']); + this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']); } /** diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index ad7ebba37f..c6a8d1827c 100644 --- a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -14,6 +14,7 @@ import { first, map, switchMap, tap } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; import { environment } from '../../../../environments/environment'; +import { getItemPageRoute } from '../../item-page-routing-paths'; import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page.resolver'; import { getAllSucceededRemoteData } from '../../../core/shared/operators'; import { hasValue } from '../../../shared/empty.util'; @@ -36,6 +37,11 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl */ updates$: Observable; + /** + * Route to the item's page + */ + itemPageRoute: string; + /** * A subscription that checks when the item is deleted in cache and reloads the item by sending a new request * This is used to update the item in cache after bitstreams are deleted @@ -69,6 +75,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl getAllSucceededRemoteData() ).subscribe((rd: RemoteData) => { this.item = rd.payload; + this.itemPageRoute = getItemPageRoute(this.item); this.postItemInit(); this.initializeUpdates(); }); diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.scss b/src/app/+item-page/edit-item-page/edit-item-page.component.scss index bb3bdaaeb0..7fd08c83af 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.component.scss +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.scss @@ -1,3 +1,3 @@ .btn { - min-width: $edit-item-button-min-width; + min-width: var(--ds-edit-item-button-min-width); } diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.ts b/src/app/+item-page/edit-item-page/edit-item-page.component.ts index 218a60a2de..ec7cdb022d 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.component.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.ts @@ -55,6 +55,6 @@ export class EditItemPageComponent implements OnInit { * @param item The item for which the url is requested */ getItemPage(item: Item): string { - return getItemPageRoute(item.id); + return getItemPageRoute(item); } } diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index 20056a9ea4..da10d33add 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -74,18 +74,18 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard'; component: ItemRelationshipsComponent, data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true } }, + /* TODO - uncomment & fix when view page exists { path: 'view', - /* TODO - change when view page exists */ component: ItemBitstreamsComponent, data: { title: 'item.edit.tabs.view.title', showBreadcrumbs: true } - }, + }, */ + /* TODO - uncomment & fix when curate page exists { path: 'curate', - /* TODO - change when curate page exists */ component: ItemBitstreamsComponent, data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true } - }, + }, */ { path: 'versionhistory', component: ItemVersionHistoryComponent, diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index 82ca1f58d9..9ab2029896 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -1,7 +1,7 @@
diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index 0400e765de..1fcbe99702 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss @@ -1,19 +1,19 @@ .header-row { - color: $table-dark-color; - background-color: $table-dark-bg; - border-color: $table-dark-border-color; + color: var(--bs-table-dark-color); + background-color: var(--bs-table-dark-bg); + border-color: var(--bs-table-dark-border-color); } .bundle-row { - color: $table-head-color; - background-color: $table-head-bg; - border-color: $table-border-color; + color: var(--bs-table-head-color); + background-color: var(--bs-table-head-bg); + border-color: var(--bs-table-border-color); } .row-element { padding: 12px; padding: 0.75em; - border-bottom: $table-border-width solid $table-border-color; + border-bottom: var(--bs-table-border-width) solid var(--bs-table-border-color); } .drag-handle { diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index c28ef9b525..b732e6a950 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -8,7 +8,7 @@
- - diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts index ef78d11de5..116a0feb21 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts @@ -183,7 +183,7 @@ describe('ItemDeleteComponent', () => { describe('notify', () => { it('should navigate to the item edit page on failed deletion of the item', () => { comp.notify(false); - expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute('fake-id')]); + expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem)]); }); }); }); diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts index ff8e8540d5..366b22bec7 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts @@ -355,7 +355,7 @@ export class ItemDeleteComponent this.router.navigate(['']); } else { this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error')); - this.router.navigate([getItemEditRoute(this.item.id)]); + this.router.navigate([getItemEditRoute(this.item)]); } } } diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.scss b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.scss index 1790157fa5..a2a6786b36 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.scss +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.scss @@ -1,13 +1,13 @@ .btn[disabled] { - color: $gray-600; - border-color: $gray-600; + color: var(--bs-gray-600); + border-color: var(--bs-gray-600); z-index: 0; // prevent border colors jumping on hover } .metadata-field { - width: $edit-item-metadata-field-width; + width: var(--ds-edit-item-metadata-field-width); } .language-field { - width: $edit-item-language-field-width; -} \ No newline at end of file + width: var(--ds-edit-item-language-field-width); +} diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.scss b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.scss index a74ecb8f47..46ce1a6f4b 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.scss +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.scss @@ -1,20 +1,20 @@ .button-row { .btn { - margin-right: 0.5 * $spacer; + margin-right: calc(0.5 * var(--bs-spacer)); &:last-child { margin-right: 0; } @media screen and (min-width: map-get($grid-breakpoints, sm)) { - min-width: $edit-item-button-min-width; + min-width: var(--ds-edit-item-button-min-width); } } &.top .btn { - margin-top: $spacer/2; - margin-bottom: $spacer/2; + margin-top: calc(var(--bs-spacer) / 2); + margin-bottom: calc(var(--bs-spacer) / 2); } -} \ No newline at end of file +} diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.html b/src/app/+item-page/edit-item-page/item-move/item-move.component.html index cf5ada77cf..74ca9aae4e 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.html +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.html @@ -19,7 +19,7 @@
-
+

@@ -39,7 +39,7 @@ {{'item.edit.move.processing' | translate}} - diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts index 7f9b53f7ba..dd91c65e1e 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts @@ -57,9 +57,9 @@ describe('ItemMoveComponent', () => { const routeStub = { data: observableOf({ - dso: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), { id: 'item1' - }) + })) }) }; @@ -122,7 +122,10 @@ describe('ItemMoveComponent', () => { }); describe('moveCollection', () => { it('should call itemDataService.moveToCollection', () => { - comp.itemId = 'item-id'; + comp.item = Object.assign(new Item(), { + id: 'item-id', + uuid: 'item-id', + }); comp.selectedCollectionName = 'selected-collection-id'; comp.selectedCollection = collection1; comp.moveCollection(); diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index c63caef3ea..b1ed121b40 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -10,7 +10,7 @@ import { NotificationsService } from '../../../shared/notifications/notification import { TranslateService } from '@ngx-translate/core'; import { getFirstSucceededRemoteData, - getFirstCompletedRemoteData + getFirstCompletedRemoteData, getAllSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { ItemDataService } from '../../../core/data/item-data.service'; import { Observable, of as observableOf } from 'rxjs'; @@ -19,7 +19,7 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio import { SearchService } from '../../../core/shared/search/search.service'; import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; import { SearchResult } from '../../../shared/search/search-result.model'; -import { getItemEditRoute } from '../../item-page-routing-paths'; +import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; @Component({ selector: 'ds-item-move', @@ -43,11 +43,16 @@ export class ItemMoveComponent implements OnInit { selectedCollection: Collection; canSubmit = false; - itemId: string; + item: Item; processing = false; pagination = new PaginationComponentOptions(); + /** + * Route to the item's page + */ + itemPageRoute$: Observable; + constructor(private route: ActivatedRoute, private router: Router, private notificationsService: NotificationsService, @@ -58,8 +63,12 @@ export class ItemMoveComponent implements OnInit { ngOnInit(): void { this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getFirstSucceededRemoteData()) as Observable>; + this.itemPageRoute$ = this.itemRD$.pipe( + getAllSucceededRemoteDataPayload(), + map((item) => getItemPageRoute(item)) + ); this.itemRD$.subscribe((rd) => { - this.itemId = rd.payload.id; + this.item = rd.payload; } ); this.pagination.pageSize = 5; @@ -116,9 +125,9 @@ export class ItemMoveComponent implements OnInit { */ moveCollection() { this.processing = true; - this.itemDataService.moveToCollection(this.itemId, this.selectedCollection).pipe(getFirstCompletedRemoteData()).subscribe( + this.itemDataService.moveToCollection(this.item.id, this.selectedCollection).pipe(getFirstCompletedRemoteData()).subscribe( (response: RemoteData) => { - this.router.navigate([getItemEditRoute(this.itemId)]); + this.router.navigate([getItemEditRoute(this.item)]); if (response.hasSucceeded) { this.notificationsService.success(this.translateService.get('item.edit.move.success')); } else { diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts index 8e08b333ce..594e7b806a 100644 --- a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts @@ -47,9 +47,9 @@ describe('ItemReinstateComponent', () => { routeStub = { data: observableOf({ - dso: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), { id: 'fake-id' - }) + })) }) }; diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss index 54498499d7..1466447116 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss @@ -1,10 +1,10 @@ .relationship-row:not(.alert) { - padding: $alert-padding-y 0; + padding: var(--bs-alert-padding-y) 0; } .relationship-row.alert { - margin-left: -$alert-padding-x; - margin-right: -$alert-padding-x; + margin-left: calc(-1 * var(--bs-alert-padding-x)); + margin-right: calc(-1 * var(--bs-alert-padding-x)); margin-top: -1px; margin-bottom: -1px; } diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.scss b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.scss index a974ec9a17..b1b8ca22a3 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.scss +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.scss @@ -1,6 +1,6 @@ .btn[disabled] { - color: $gray-600; - border-color: $gray-600; + color: var(--bs-gray-600); + border-color: var(--bs-gray-600); z-index: 0; // prevent border colors jumping on hover } diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss index 39ad631342..91c1423ba3 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss @@ -1,19 +1,19 @@ .button-row { .btn { - margin-right: 0.5 * $spacer; + margin-right: calc(0.5 * var(--bs-spacer)); &:last-child { margin-right: 0; } @media screen and (min-width: map-get($grid-breakpoints, sm)) { - min-width: $edit-item-button-min-width; + min-width: var(--ds-edit-item-button-min-width); } } &.top .btn { - margin-top: $spacer/2; - margin-bottom: $spacer/2; + margin-top: calc(var(--bs-spacer) / 2); + margin-bottom: calc(var(--bs-spacer) / 2); } diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.html b/src/app/+item-page/edit-item-page/item-status/item-status.component.html index 3fcf10a2f5..d4f61c99c1 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.html +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.html @@ -12,7 +12,7 @@ {{'item.edit.tabs.status.labels.itemPage' | translate}}:

diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts index 63d15d62d8..0829480670 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts @@ -20,6 +20,7 @@ describe('ItemStatusComponent', () => { const mockItem = Object.assign(new Item(), { id: 'fake-id', + uuid: 'fake-id', handle: 'fake/handle', lastModified: '2018', _links: { @@ -27,7 +28,7 @@ describe('ItemStatusComponent', () => { } }); - const itemPageUrl = `items/${mockItem.id}`; + const itemPageUrl = `/items/${mockItem.uuid}`; const routeStub = { parent: { diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts index 498a39d160..2745fc8df7 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts @@ -10,6 +10,7 @@ import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-path import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { hasValue } from '../../../shared/empty.util'; +import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators'; @Component({ selector: 'ds-item-status', @@ -50,6 +51,11 @@ export class ItemStatusComponent implements OnInit { */ actionsKeys; + /** + * Route to the item's page + */ + itemPageRoute$: Observable; + constructor(private route: ActivatedRoute, private authorizationService: AuthorizationDataService) { } @@ -109,15 +115,10 @@ export class ItemStatusComponent implements OnInit { }); } }); - - } - - /** - * Get the url to the simple item page - * @returns {string} url - */ - getItemPage(item: Item): string { - return getItemPageRoute(item.id); + this.itemPageRoute$ = this.itemRD$.pipe( + getAllSucceededRemoteDataPayload(), + map((item) => getItemPageRoute(item)) + ); } /** @@ -125,7 +126,7 @@ export class ItemStatusComponent implements OnInit { * @returns {string} url */ getCurrentUrl(item: Item): string { - return getItemEditRoute(item.id); + return getItemEditRoute(item); } trackOperation(index: number, operation: ItemOperation) { diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.html b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.html index fef76231c6..73793ede05 100644 --- a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.html +++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.html @@ -6,11 +6,11 @@ -
-
\ No newline at end of file + diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts index 78ef2def9e..9a19837665 100644 --- a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts +++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts @@ -74,9 +74,9 @@ describe('AbstractSimpleItemActionComponent', () => { routeStub = { data: observableOf({ - dso: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), { id: 'fake-id' - }) + })) }) }; @@ -136,14 +136,14 @@ describe('AbstractSimpleItemActionComponent', () => { comp.processRestResponse(successfulRemoteData); expect(notificationsServiceStub.success).toHaveBeenCalled(); - expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem.id)]); + expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem)]); }); it('should process a RemoteData to navigate and display success notification', () => { comp.processRestResponse(failedRemoteData); expect(notificationsServiceStub.error).toHaveBeenCalled(); - expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem.id)]); + expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem)]); }); }); diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts index 7d374e85e0..f4028354de 100644 --- a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts +++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts @@ -9,7 +9,7 @@ import { Observable } from 'rxjs'; import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { first, map } from 'rxjs/operators'; import { findSuccessfulAccordingTo } from '../edit-item-operators'; -import { getItemEditRoute } from '../../item-page-routing-paths'; +import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; /** * Component to render and handle simple item edit actions such as withdrawal and reinstatement. @@ -30,6 +30,11 @@ export class AbstractSimpleItemActionComponent implements OnInit { headerMessage: string; descriptionMessage: string; + /** + * Route to the item's page + */ + itemPageRoute: string; + protected predicate: Predicate>; constructor(protected route: ActivatedRoute, @@ -47,6 +52,7 @@ export class AbstractSimpleItemActionComponent implements OnInit { this.itemRD$.pipe(first()).subscribe((rd) => { this.item = rd.payload; + this.itemPageRoute = getItemPageRoute(this.item); } ); @@ -71,11 +77,11 @@ export class AbstractSimpleItemActionComponent implements OnInit { this.itemDataService.findById(this.item.id).pipe( findSuccessfulAccordingTo(this.predicate)).subscribe(() => { this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success')); - this.router.navigate([getItemEditRoute(this.item.id)]); + this.router.navigate([getItemEditRoute(this.item)]); }); } else { this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error')); - this.router.navigate([getItemEditRoute(this.item.id)]); + this.router.navigate([getItemEditRoute(this.item)]); } } diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.scss b/src/app/+item-page/full/field-components/file-section/full-file-section.component.scss index 5bb04cac2f..5384f90cec 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.scss +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.scss @@ -2,4 +2,4 @@ dt { text-align: right; } -} \ No newline at end of file +} 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 762390c7d4..349dd1c928 100644 --- a/src/app/+item-page/full/full-item-page.component.html +++ b/src/app/+item-page/full/full-item-page.component.html @@ -7,11 +7,11 @@
- +
diff --git a/src/app/+item-page/full/full-item-page.component.ts b/src/app/+item-page/full/full-item-page.component.ts index 741f1e76a7..aea350e58e 100644 --- a/src/app/+item-page/full/full-item-page.component.ts +++ b/src/app/+item-page/full/full-item-page.component.ts @@ -18,9 +18,8 @@ import { hasValue } from '../../shared/empty.util'; import { AuthService } from '../../core/auth/auth.service'; /** - * This component renders a simple item page. + * This component renders a full item page. * The route parameter 'id' is used to request the item it represents. - * All fields of the item that should be displayed, are defined in its template. */ @Component({ diff --git a/src/app/+item-page/full/themed-full-item-page.component.ts b/src/app/+item-page/full/themed-full-item-page.component.ts new file mode 100644 index 0000000000..c184d0a4e4 --- /dev/null +++ b/src/app/+item-page/full/themed-full-item-page.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { FullItemPageComponent } from './full-item-page.component'; + +/** + * Themed wrapper for FullItemPageComponent + */ +@Component({ + selector: 'ds-themed-full-item-page', + styleUrls: [], + templateUrl: './../../shared/theme-support/themed.component.html', +}) +export class ThemedFullItemPageComponent extends ThemedComponent { + protected getComponentName(): string { + return 'FullItemPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/+item-page/full/full-item-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./full-item-page.component`); + } +} diff --git a/src/app/+item-page/item-page-routing-paths.ts b/src/app/+item-page/item-page-routing-paths.ts index 1a7c23c430..8ad5583269 100644 --- a/src/app/+item-page/item-page-routing-paths.ts +++ b/src/app/+item-page/item-page-routing-paths.ts @@ -1,4 +1,6 @@ import { URLCombiner } from '../core/url-combiner/url-combiner'; +import { Item } from '../core/shared/item.model'; +import { isNotEmpty } from '../shared/empty.util'; export const ITEM_MODULE_PATH = 'items'; @@ -6,12 +8,30 @@ export function getItemModuleRoute() { return `/${ITEM_MODULE_PATH}`; } -export function getItemPageRoute(itemId: string) { - return new URLCombiner(getItemModuleRoute(), itemId).toString(); +/** + * Get the route to an item's page + * Depending on the item's relationship type, the route will either start with /items or /entities + * @param item The item to retrieve the route for + */ +export function getItemPageRoute(item: Item) { + const type = item.firstMetadataValue('relationship.type'); + return getEntityPageRoute(type, item.uuid); } -export function getItemEditRoute(id: string) { - return new URLCombiner(getItemModuleRoute(), id, ITEM_EDIT_PATH).toString(); +export function getItemEditRoute(item: Item) { + return new URLCombiner(getItemPageRoute(item), ITEM_EDIT_PATH).toString(); +} + +export function getEntityPageRoute(entityType: string, itemId: string) { + if (isNotEmpty(entityType)) { + return new URLCombiner('/entities', encodeURIComponent(entityType.toLowerCase()), itemId).toString(); + } else { + return new URLCombiner(getItemModuleRoute(), itemId).toString(); + } +} + +export function getEntityEditRoute(entityType: string, itemId: string) { + return new URLCombiner(getEntityPageRoute(entityType, itemId), ITEM_EDIT_PATH).toString(); } export const ITEM_EDIT_PATH = 'edit'; diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index b04a783b2f..c9f2c402bb 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -1,18 +1,17 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; - -import { ItemPageComponent } from './simple/item-page.component'; -import { FullItemPageComponent } from './full/full-item-page.component'; import { ItemPageResolver } from './item-page.resolver'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver'; import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; import { LinkService } from '../core/cache/builders/link.service'; import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; -import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths'; +import { ITEM_EDIT_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths'; import { ItemPageAdministratorGuard } from './item-page-administrator.guard'; import { MenuItemType } from '../shared/menu/initial-menus-state'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; +import { ThemedItemPageComponent } from './simple/themed-item-page.component'; +import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; @NgModule({ imports: [ @@ -27,12 +26,12 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; children: [ { path: '', - component: ItemPageComponent, + component: ThemedItemPageComponent, pathMatch: 'full', }, { path: 'full', - component: FullItemPageComponent, + component: ThemedFullItemPageComponent, }, { path: ITEM_EDIT_PATH, diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index f1a49b1bd7..fe326ddcf7 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -25,6 +25,8 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental- import { UntypedItemComponent } from './simple/item-types/untyped-item/untyped-item.component'; import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; +import { ThemedItemPageComponent } from './simple/themed-item-page.component'; +import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -32,10 +34,27 @@ const ENTRY_COMPONENTS = [ UntypedItemComponent ]; -import { MediaViewerComponent } from './media-viewer/media-viewer.component'; -import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component'; -import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component'; -import { NgxGalleryModule } from '@kolkov/ngx-gallery'; +const DECLARATIONS = [ + ItemPageComponent, + ThemedItemPageComponent, + FullItemPageComponent, + ThemedFullItemPageComponent, + MetadataUriValuesComponent, + ItemPageAuthorFieldComponent, + ItemPageDateFieldComponent, + ItemPageAbstractFieldComponent, + ItemPageUriFieldComponent, + ItemPageTitleFieldComponent, + ItemPageFieldComponent, + FileSectionComponent, + CollectionsComponent, + FullFileSectionComponent, + PublicationComponent, + UntypedItemComponent, + ItemComponent, + UploadBitstreamComponent, + AbstractIncrementalListComponent, +]; @NgModule({ imports: [ @@ -49,26 +68,10 @@ import { NgxGalleryModule } from '@kolkov/ngx-gallery'; NgxGalleryModule, ], declarations: [ - ItemPageComponent, - FullItemPageComponent, - MetadataUriValuesComponent, - ItemPageAuthorFieldComponent, - ItemPageDateFieldComponent, - ItemPageAbstractFieldComponent, - ItemPageUriFieldComponent, - ItemPageTitleFieldComponent, - ItemPageFieldComponent, - FileSectionComponent, - CollectionsComponent, - FullFileSectionComponent, - PublicationComponent, - UntypedItemComponent, - ItemComponent, - UploadBitstreamComponent, - AbstractIncrementalListComponent, - MediaViewerComponent, - MediaViewerVideoComponent, - MediaViewerImageComponent, + ...DECLARATIONS + ], + exports: [ + ...DECLARATIONS ] }) export class ItemPageModule { diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index d90806bfc3..a131cda7f8 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { ItemDataService } from '../core/data/item-data.service'; @@ -7,9 +7,21 @@ import { Item } from '../core/shared/item.model'; import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model'; import { FindListOptions } from '../core/data/request.models'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { Store } from '@ngrx/store'; +import { ResolvedAction } from '../core/resolving/resolver.actions'; +import { map } from 'rxjs/operators'; +import { hasValue } from '../shared/empty.util'; +import { getItemPageRoute } from './item-page-routing-paths'; +/** + * 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 ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('owningCollection'), + followLink('owningCollection', undefined, true, true, true, + followLink('parentCommunity', undefined, true, true, true, + followLink('parentCommunity')) + ), followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')), followLink('relationships'), followLink('version', undefined, true, true, true, followLink('versionhistory')), @@ -20,7 +32,11 @@ export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ */ @Injectable() export class ItemPageResolver implements Resolve> { - constructor(private itemService: ItemDataService) { + constructor( + private itemService: ItemDataService, + private store: Store, + private router: Router + ) { } /** @@ -31,12 +47,30 @@ export class ItemPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.itemService.findById(route.params.id, + const itemRD$ = this.itemService.findById(route.params.id, true, false, ...ITEM_PAGE_LINKS_TO_FOLLOW ).pipe( getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + if (rd.hasSucceeded && hasValue(rd.payload)) { + const itemRoute = getItemPageRoute(rd.payload); + const thisRoute = state.url; + if (!thisRoute.startsWith(itemRoute)) { + const itemId = rd.payload.uuid; + const subRoute = thisRoute.substring(thisRoute.indexOf(itemId) + itemId.length, thisRoute.length); + this.router.navigateByUrl(itemRoute + subRoute); + } + } + return rd; + }) ); + + itemRD$.subscribe((itemRD: RemoteData) => { + this.store.dispatch(new ResolvedAction(state.url, itemRD.payload)); + }); + + return itemRD$; } } diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index 3267cf743a..67e278c2fb 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -11,9 +11,10 @@ import { Item } from '../../core/shared/item.model'; import { MetadataService } from '../../core/metadata/metadata.service'; import { fadeInOut } from '../../shared/animations/fade'; -import { redirectOn4xx } from '../../core/shared/operators'; +import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators'; import { ViewMode } from '../../core/shared/view-mode.model'; import { AuthService } from '../../core/auth/auth.service'; +import { getItemPageRoute } from '../item-page-routing-paths'; /** * This component renders a simple item page. @@ -44,6 +45,11 @@ export class ItemPageComponent implements OnInit { */ viewMode = ViewMode.StandalonePage; + /** + * Route to the item's page + */ + itemPageRoute$: Observable; + constructor( private route: ActivatedRoute, private router: Router, @@ -61,5 +67,9 @@ export class ItemPageComponent implements OnInit { redirectOn4xx(this.router, this.authService) ); this.metadataService.processRemoteData(this.itemRD$); + this.itemPageRoute$ = this.itemRD$.pipe( + getAllSucceededRemoteDataPayload(), + map((item) => getItemPageRoute(item)) + ); } } diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.html b/src/app/+item-page/simple/item-types/publication/publication.component.html index dd3e7dbccd..a004712e0f 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.html +++ b/src/app/+item-page/simple/item-types/publication/publication.component.html @@ -3,7 +3,7 @@ {{'publication.page.titleprefix' | translate}}
- +
@@ -79,7 +79,7 @@ diff --git a/src/app/+item-page/simple/item-types/shared/item.component.ts b/src/app/+item-page/simple/item-types/shared/item.component.ts index ea9df1b4b4..120eda930f 100644 --- a/src/app/+item-page/simple/item-types/shared/item.component.ts +++ b/src/app/+item-page/simple/item-types/shared/item.component.ts @@ -1,10 +1,11 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from '../../../../../environments/environment'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; +import { getItemPageRoute } from '../../../item-page-routing-paths'; @Component({ selector: 'ds-item', @@ -13,14 +14,22 @@ import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/oper /** * A generic component for displaying metadata and relations of an item */ -export class ItemComponent { +export class ItemComponent implements OnInit { @Input() object: Item; + /** + * Route to the item page + */ + itemPageRoute: string; mediaViewer = environment.mediaViewer; constructor(protected bitstreamDataService: BitstreamDataService) { } + ngOnInit(): void { + this.itemPageRoute = getItemPageRoute(this.object); + } + // TODO refactor to return RemoteData, and thumbnail template to deal with loading getThumbnail(): Observable { return this.bitstreamDataService.getThumbnailFor(this.object).pipe( diff --git a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html index 4f0dbb0866..7a1366dda9 100644 --- a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -3,7 +3,7 @@
- +
@@ -64,7 +64,7 @@ diff --git a/src/app/+item-page/simple/themed-item-page.component.ts b/src/app/+item-page/simple/themed-item-page.component.ts new file mode 100644 index 0000000000..3d04f04acd --- /dev/null +++ b/src/app/+item-page/simple/themed-item-page.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { ItemPageComponent } from './item-page.component'; + +/** + * Themed wrapper for ItemPageComponent + */ +@Component({ + selector: 'ds-themed-item-page', + styleUrls: [], + templateUrl: './../../shared/theme-support/themed.component.html', +}) + +export class ThemedItemPageComponent extends ThemedComponent { + protected getComponentName(): string { + return 'ItemPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/+item-page/simple/item-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./item-page.component`); + } + +} diff --git a/src/app/+login-page/login-page-routing.module.ts b/src/app/+login-page/login-page-routing.module.ts index 9fa4a9e5ad..3a48852625 100644 --- a/src/app/+login-page/login-page-routing.module.ts +++ b/src/app/+login-page/login-page-routing.module.ts @@ -1,14 +1,13 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; - -import { LoginPageComponent } from './login-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; +import { ThemedLoginPageComponent } from './themed-login-page.component'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } } + { path: '', pathMatch: 'full', component: ThemedLoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } } ]) ], providers: [ diff --git a/src/app/+login-page/login-page.component.scss b/src/app/+login-page/login-page.component.scss index 58e7272e5f..d628a5089f 100644 --- a/src/app/+login-page/login-page.component.scss +++ b/src/app/+login-page/login-page.component.scss @@ -1,4 +1,4 @@ .login-logo { - height: $login-logo-height; - width: $login-logo-width; + height: var(--ds-login-logo-height); + width: var(--ds-login-logo-width); } diff --git a/src/app/+login-page/login-page.module.ts b/src/app/+login-page/login-page.module.ts index 4d3f726c40..4facc82df1 100644 --- a/src/app/+login-page/login-page.module.ts +++ b/src/app/+login-page/login-page.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '../shared/shared.module'; import { LoginPageComponent } from './login-page.component'; import { LoginPageRoutingModule } from './login-page-routing.module'; +import { ThemedLoginPageComponent } from './themed-login-page.component'; @NgModule({ imports: [ @@ -11,7 +12,8 @@ import { LoginPageRoutingModule } from './login-page-routing.module'; SharedModule, ], declarations: [ - LoginPageComponent + LoginPageComponent, + ThemedLoginPageComponent ] }) export class LoginPageModule { diff --git a/src/app/+login-page/themed-login-page.component.ts b/src/app/+login-page/themed-login-page.component.ts new file mode 100644 index 0000000000..cdf5932802 --- /dev/null +++ b/src/app/+login-page/themed-login-page.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { LoginPageComponent } from './login-page.component'; + +/** + * Themed wrapper for LoginPageComponent + */ +@Component({ + selector: 'ds-themed-login-page', + styleUrls: [], + templateUrl: './../shared/theme-support/themed.component.html' +}) +export class ThemedLoginPageComponent extends ThemedComponent { + protected getComponentName(): string { + return 'LoginPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/+login-page/login-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./login-page.component`); + } +} diff --git a/src/app/+logout-page/logout-page-routing.module.ts b/src/app/+logout-page/logout-page-routing.module.ts index 64894c1f87..a5df0fe580 100644 --- a/src/app/+logout-page/logout-page-routing.module.ts +++ b/src/app/+logout-page/logout-page-routing.module.ts @@ -1,8 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; - -import { LogoutPageComponent } from './logout-page.component'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { ThemedLogoutPageComponent } from './themed-logout-page.component'; @NgModule({ imports: [ @@ -10,7 +9,7 @@ import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; { canActivate: [AuthenticatedGuard], path: '', - component: LogoutPageComponent, + component: ThemedLogoutPageComponent, data: { title: 'logout.title' } } ]) diff --git a/src/app/+logout-page/logout-page.module.ts b/src/app/+logout-page/logout-page.module.ts index b085a5117b..59a5459e7f 100644 --- a/src/app/+logout-page/logout-page.module.ts +++ b/src/app/+logout-page/logout-page.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '../shared/shared.module'; import { LogoutPageComponent } from './logout-page.component'; import { LogoutPageRoutingModule } from './logout-page-routing.module'; +import { ThemedLogoutPageComponent } from './themed-logout-page.component'; @NgModule({ imports: [ @@ -11,7 +12,8 @@ import { LogoutPageRoutingModule } from './logout-page-routing.module'; SharedModule, ], declarations: [ - LogoutPageComponent + LogoutPageComponent, + ThemedLogoutPageComponent ] }) export class LogoutPageModule { diff --git a/src/app/+logout-page/themed-logout-page.component.ts b/src/app/+logout-page/themed-logout-page.component.ts new file mode 100644 index 0000000000..19e1e2d1d1 --- /dev/null +++ b/src/app/+logout-page/themed-logout-page.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { LogoutPageComponent } from './logout-page.component'; + +/** + * Themed wrapper for LogoutPageComponent + */ +@Component({ + selector: 'ds-themed-logout-page', + styleUrls: [], + templateUrl: './../shared/theme-support/themed.component.html' +}) +export class ThemedLogoutPageComponent extends ThemedComponent { + protected getComponentName(): string { + return 'LogoutPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/+logout-page/logout-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./logout-page.component`); + } +} diff --git a/src/app/+lookup-by-id/lookup-by-id-routing.module.ts b/src/app/+lookup-by-id/lookup-by-id-routing.module.ts index 76c90ed6d4..52a7c8a358 100644 --- a/src/app/+lookup-by-id/lookup-by-id-routing.module.ts +++ b/src/app/+lookup-by-id/lookup-by-id-routing.module.ts @@ -1,8 +1,8 @@ import { LookupGuard } from './lookup-guard'; import { NgModule } from '@angular/core'; import { RouterModule, UrlSegment } from '@angular/router'; -import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component'; import { isNotEmpty } from '../shared/empty.util'; +import { ThemedObjectNotFoundComponent } from './objectnotfound/themed-objectnotfound.component'; @NgModule({ imports: [ @@ -10,7 +10,7 @@ import { isNotEmpty } from '../shared/empty.util'; { matcher: urlMatcher, canActivate: [LookupGuard], - component: ObjectNotFoundComponent } + component: ThemedObjectNotFoundComponent } ]) ], providers: [ diff --git a/src/app/+lookup-by-id/lookup-by-id.module.ts b/src/app/+lookup-by-id/lookup-by-id.module.ts index 1b070c1279..0202a43033 100644 --- a/src/app/+lookup-by-id/lookup-by-id.module.ts +++ b/src/app/+lookup-by-id/lookup-by-id.module.ts @@ -4,6 +4,7 @@ import { SharedModule } from '../shared/shared.module'; import { LookupRoutingModule } from './lookup-by-id-routing.module'; import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component'; import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service'; +import { ThemedObjectNotFoundComponent } from './objectnotfound/themed-objectnotfound.component'; @NgModule({ imports: [ @@ -12,7 +13,8 @@ import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service'; SharedModule, ], declarations: [ - ObjectNotFoundComponent + ObjectNotFoundComponent, + ThemedObjectNotFoundComponent ], providers: [ DsoRedirectDataService diff --git a/src/app/+lookup-by-id/objectnotfound/themed-objectnotfound.component.ts b/src/app/+lookup-by-id/objectnotfound/themed-objectnotfound.component.ts new file mode 100644 index 0000000000..e1bec33dfd --- /dev/null +++ b/src/app/+lookup-by-id/objectnotfound/themed-objectnotfound.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { ObjectNotFoundComponent } from './objectnotfound.component'; + +/** + * Themed wrapper for ObjectNotFoundComponent + */ +@Component({ + selector: 'ds-themed-objnotfound', + styleUrls: [], + templateUrl: '../../shared/theme-support/themed.component.html', +}) +export class ThemedObjectNotFoundComponent extends ThemedComponent { + protected getComponentName(): string { + return 'ObjectNotFoundComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/+lookup-by-id/objectnotfound/objectnotfound.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./objectnotfound.component`); + } + +} diff --git a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts index 4ff31d67cf..7c6d8918cb 100644 --- a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts +++ b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts @@ -21,6 +21,10 @@ import { UploaderService } from '../../shared/uploader/uploader.service'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { UploaderComponent } from '../../shared/uploader/uploader.component'; +import { HttpXsrfTokenExtractor } from '@angular/common/http'; +import { CookieService } from '../../core/services/cookie.service'; +import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock'; +import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock'; describe('MyDSpaceNewSubmissionComponent test', () => { @@ -55,6 +59,8 @@ describe('MyDSpaceNewSubmissionComponent test', () => { ChangeDetectorRef, MyDSpaceNewSubmissionComponent, UploaderService, + { provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') }, + { provide: CookieService, useValue: new CookieServiceMock() }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/+my-dspace-page/my-dspace-page-routing.module.ts b/src/app/+my-dspace-page/my-dspace-page-routing.module.ts index d70a007e3a..46deb36e20 100644 --- a/src/app/+my-dspace-page/my-dspace-page-routing.module.ts +++ b/src/app/+my-dspace-page/my-dspace-page-routing.module.ts @@ -1,15 +1,14 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; - -import { MyDSpacePageComponent } from './my-dspace-page.component'; import { MyDSpaceGuard } from './my-dspace.guard'; +import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component'; @NgModule({ imports: [ RouterModule.forChild([ { path: '', - component: MyDSpacePageComponent, + component: ThemedMyDSpacePageComponent, data: { title: 'mydspace.title' }, canActivate: [ MyDSpaceGuard diff --git a/src/app/+my-dspace-page/my-dspace-page.component.html b/src/app/+my-dspace-page/my-dspace-page.component.html index 6f1cc41a1e..55d1e304d0 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.html +++ b/src/app/+my-dspace-page/my-dspace-page.component.html @@ -6,6 +6,7 @@ [configurationList]="(configurationList$ | async)" [resultCount]="(resultsRD$ | async)?.payload.totalElements" [viewModeList]="viewModeList" + [refreshFilters]="refreshFilters.asObservable()" [inPlaceSearch]="inPlaceSearch">
@@ -39,7 +41,8 @@
+ [context]="context$ | async" + (contentChange)="onResultsContentChange()">
diff --git a/src/app/+my-dspace-page/my-dspace-page.component.ts b/src/app/+my-dspace-page/my-dspace-page.component.ts index 0f08795cdc..5ee2a47d9f 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.ts +++ b/src/app/+my-dspace-page/my-dspace-page.component.ts @@ -7,7 +7,7 @@ import { OnInit } from '@angular/core'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; import { map, switchMap, tap, } from 'rxjs/operators'; import { PaginatedList } from '../core/data/paginated-list.model'; @@ -101,6 +101,11 @@ export class MyDSpacePageComponent implements OnInit { */ context$: Observable; + /** + * Emit an event every time search sidebars must refresh their contents. + */ + refreshFilters: Subject = new Subject(); + constructor(private service: SearchService, private sidebarService: SidebarService, private windowService: HostWindowService, @@ -148,6 +153,14 @@ export class MyDSpacePageComponent implements OnInit { } + /** + * Handle the contentChange event from within the my dspace content. + * Notify search sidebars to refresh their content. + */ + onResultsContentChange() { + this.refreshFilters.next(); + } + /** * Set the sidebar to a collapsed state */ @@ -184,5 +197,6 @@ export class MyDSpacePageComponent implements OnInit { if (hasValue(this.sub)) { this.sub.unsubscribe(); } + this.refreshFilters.complete(); } } diff --git a/src/app/+my-dspace-page/my-dspace-page.module.ts b/src/app/+my-dspace-page/my-dspace-page.module.ts index 40bbcc903c..52c80c90b0 100644 --- a/src/app/+my-dspace-page/my-dspace-page.module.ts +++ b/src/app/+my-dspace-page/my-dspace-page.module.ts @@ -11,6 +11,15 @@ import { MyDSpaceGuard } from './my-dspace.guard'; import { MyDSpaceConfigurationService } from './my-dspace-configuration.service'; import { CollectionSelectorComponent } from './collection-selector/collection-selector.component'; import { MyDspaceSearchModule } from './my-dspace-search.module'; +import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component'; + +const DECLARATIONS = [ + MyDSpacePageComponent, + ThemedMyDSpacePageComponent, + MyDSpaceResultsComponent, + MyDSpaceNewSubmissionComponent, + CollectionSelectorComponent +]; @NgModule({ imports: [ @@ -19,16 +28,12 @@ import { MyDspaceSearchModule } from './my-dspace-search.module'; MyDspacePageRoutingModule, MyDspaceSearchModule.withEntryComponents() ], - declarations: [ - MyDSpacePageComponent, - MyDSpaceResultsComponent, - MyDSpaceNewSubmissionComponent, - CollectionSelectorComponent - ], + declarations: DECLARATIONS, providers: [ MyDSpaceGuard, MyDSpaceConfigurationService - ] + ], + exports: DECLARATIONS, }) /** diff --git a/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.html b/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.html index 3a829e6ece..2710285f0d 100644 --- a/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.html +++ b/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.html @@ -5,7 +5,8 @@ [sortConfig]="searchConfig.sort" [objects]="searchResults" [hideGear]="true" - [context]="context"> + [context]="context" + (contentChange)="contentChange.emit()"> diff --git a/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts b/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts index 35b13c8bae..32b6d9c9f7 100644 --- a/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts +++ b/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { fadeIn, fadeInOut } from '../../shared/animations/fade'; @@ -41,6 +41,12 @@ export class MyDSpaceResultsComponent { * The current context for the search results */ @Input() context: Context; + + /** + * Emit when one of the results has changed. + */ + @Output() contentChange = new EventEmitter(); + /** * A boolean representing if search results entry are separated by a line */ diff --git a/src/app/+my-dspace-page/my-dspace-search.module.ts b/src/app/+my-dspace-page/my-dspace-search.module.ts index 2fe1cd2a55..a97f2207e7 100644 --- a/src/app/+my-dspace-page/my-dspace-search.module.ts +++ b/src/app/+my-dspace-page/my-dspace-search.module.ts @@ -14,12 +14,16 @@ import { ClaimedTaskSearchResultDetailElementComponent } from '../shared/object- import { ItemSearchResultListElementSubmissionComponent } from '../shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component'; import { WorkflowItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component'; import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component'; +import { ClaimedApprovedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component'; +import { ClaimedDeclinedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator WorkspaceItemSearchResultListElementComponent, WorkflowItemSearchResultListElementComponent, ClaimedSearchResultListElementComponent, + ClaimedApprovedSearchResultListElementComponent, + ClaimedDeclinedSearchResultListElementComponent, PoolSearchResultListElementComponent, ItemSearchResultDetailElementComponent, WorkspaceItemSearchResultDetailElementComponent, diff --git a/src/app/+my-dspace-page/themed-my-dspace-page.component.ts b/src/app/+my-dspace-page/themed-my-dspace-page.component.ts new file mode 100644 index 0000000000..47275496ca --- /dev/null +++ b/src/app/+my-dspace-page/themed-my-dspace-page.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { MyDSpacePageComponent } from './my-dspace-page.component'; + +/** + * Themed wrapper for MyDSpacePageComponent + */ +@Component({ + selector: 'ds-themed-my-dspace-page', + styleUrls: [], + templateUrl: './../shared/theme-support/themed.component.html' +}) +export class ThemedMyDSpacePageComponent extends ThemedComponent { + protected inAndOutputNames: (keyof MyDSpacePageComponent & keyof this)[]; + + protected getComponentName(): string { + return 'MyDSpacePageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/+my-dspace-page/my-dspace-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./my-dspace-page.component`); + } +} diff --git a/src/app/+search-page/configuration-search-page.component.spec.ts b/src/app/+search-page/configuration-search-page.component.spec.ts index f49d329edd..8ce4154c66 100644 --- a/src/app/+search-page/configuration-search-page.component.spec.ts +++ b/src/app/+search-page/configuration-search-page.component.spec.ts @@ -2,20 +2,63 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { configureSearchComponentTestingModule } from './search.component.spec'; import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; +import { Component, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; +import { RouteService } from '../core/services/route.service'; +import createSpy = jasmine.createSpy; + +const CONFIGURATION = 'test-configuration'; +const QUERY = 'test query'; + +@Component({ + template: ` + + + `, +}) +class HostComponent { + @ViewChild('configurationSearchPage') configurationSearchPage: ConfigurationSearchPageComponent; +} describe('ConfigurationSearchPageComponent', () => { let comp: ConfigurationSearchPageComponent; - let fixture: ComponentFixture; + let fixture: ComponentFixture; let searchConfigService: SearchConfigurationService; + let routeService: RouteService; beforeEach(waitForAsync(() => { - configureSearchComponentTestingModule(ConfigurationSearchPageComponent); + configureSearchComponentTestingModule(ConfigurationSearchPageComponent, [HostComponent]); })); beforeEach(() => { - fixture = TestBed.createComponent(ConfigurationSearchPageComponent); - comp = fixture.componentInstance; - searchConfigService = (comp as any).searchConfigService; + fixture = TestBed.createComponent(HostComponent); + + // Set router url to a dummy value for SearchComponent#ngOnInit + spyOnProperty(TestBed.inject(Router), 'url', 'get').and.returnValue('some/url/here'); + + routeService = TestBed.inject(RouteService); + routeService.setParameter = createSpy('setParameter'); + fixture.detectChanges(); + + comp = fixture.componentInstance.configurationSearchPage; + searchConfigService = (comp as any).searchConfigService; + }); + + it('should set route parameters on init', () => { + expect(comp.configuration).toBe(CONFIGURATION); + expect(comp.fixedFilterQuery).toBe(QUERY); + + expect(routeService.setParameter).toHaveBeenCalledWith('configuration', CONFIGURATION); + expect(routeService.setParameter).toHaveBeenCalledWith('fixedFilterQuery', QUERY); + }); + + it('should reset route parameters on destroy', () => { + fixture.destroy(); + + expect(routeService.setParameter).toHaveBeenCalledWith('configuration', undefined); + expect(routeService.setParameter).toHaveBeenCalledWith('fixedFilterQuery', undefined); }); }); diff --git a/src/app/+search-page/configuration-search-page.component.ts b/src/app/+search-page/configuration-search-page.component.ts index befac7f331..62663a6fb3 100644 --- a/src/app/+search-page/configuration-search-page.component.ts +++ b/src/app/+search-page/configuration-search-page.component.ts @@ -1,7 +1,14 @@ import { HostWindowService } from '../shared/host-window.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { SearchComponent } from './search.component'; -import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Inject, + Input, + OnDestroy, + OnInit +} from '@angular/core'; import { pushInOut } from '../shared/animations/push'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; @@ -27,7 +34,7 @@ import { Router } from '@angular/router'; ] }) -export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit { +export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit, OnDestroy { /** * The configuration to use for the search options * If empty, the configuration will be determined by the route parameter called 'configuration' @@ -65,4 +72,17 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery); } } + + /** + * Reset the updated query/configuration set in ngOnInit() + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + if (hasValue(this.configuration)) { + this.routeService.setParameter('configuration', undefined); + } + if (hasValue(this.fixedFilterQuery)) { + this.routeService.setParameter('fixedFilterQuery', undefined); + } + } } diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index f71c7b45ee..9e39ad1bc9 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -2,11 +2,11 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; -import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; -import { SearchPageComponent } from './search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { SearchPageModule } from './search-page.module'; +import { ThemedSearchPageComponent } from './themed-search-page.component'; +import { ThemedConfigurationSearchPageComponent } from './themed-configuration-search-page.component'; @NgModule({ imports: [ @@ -15,8 +15,8 @@ import { SearchPageModule } from './search-page.module'; path: '', resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' }, children: [ - { path: '', component: SearchPageComponent }, - { path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] } + { path: '', component: ThemedSearchPageComponent }, + { path: ':configuration', component: ThemedConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] } ] }] ) diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index 286f1d12be..0cf6164f8a 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -13,11 +13,13 @@ import { SearchFilterService } from '../core/shared/search/search-filter.service import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; +import { ThemedSearchPageComponent } from './themed-search-page.component'; const components = [ SearchPageComponent, SearchComponent, - SearchTrackerComponent + SearchTrackerComponent, + ThemedSearchPageComponent ]; @NgModule({ diff --git a/src/app/+search-page/search.component.scss b/src/app/+search-page/search.component.scss index 44d4aabd13..82de1f4772 100644 --- a/src/app/+search-page/search.component.scss +++ b/src/app/+search-page/search.component.scss @@ -6,5 +6,5 @@ } ::ng-deep .search-controls { - margin-bottom: $spacer; + margin-bottom: var(--bs-spacer); } diff --git a/src/app/+search-page/search.component.spec.ts b/src/app/+search-page/search.component.spec.ts index 2ad497aa49..989aed403d 100644 --- a/src/app/+search-page/search.component.spec.ts +++ b/src/app/+search-page/search.component.spec.ts @@ -84,10 +84,10 @@ const routeServiceStub = { } }; -export function configureSearchComponentTestingModule(compType) { +export function configureSearchComponentTestingModule(compType, additionalDeclarations: any[] = []) { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule], - declarations: [compType], + declarations: [compType, ...additionalDeclarations], providers: [ { provide: SearchService, useValue: searchServiceStub }, { diff --git a/src/app/+search-page/themed-configuration-search-page.component.ts b/src/app/+search-page/themed-configuration-search-page.component.ts new file mode 100644 index 0000000000..9f7277be0e --- /dev/null +++ b/src/app/+search-page/themed-configuration-search-page.component.ts @@ -0,0 +1,72 @@ +import { Component, Input } from '@angular/core'; +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; +import { Observable } from 'rxjs'; +import { Context } from '../core/shared/context.model'; + +/** + * Themed wrapper for ConfigurationSearchPageComponent + */ +@Component({ + selector: 'ds-themed-configuration-search-page', + styleUrls: [], + templateUrl: '../shared/theme-support/themed.component.html', +}) +export class ThemedConfigurationSearchPageComponent extends ThemedComponent { + /** + * The configuration to use for the search options + * If empty, the configuration will be determined by the route parameter called 'configuration' + */ + @Input() configuration: string; + + /** + * The actual query for the fixed filter. + * If empty, the query will be determined by the route parameter called 'filter' + */ + @Input() fixedFilterQuery: string; + + /** + * True when the search component should show results on the current page + */ + @Input() inPlaceSearch = true; + + /** + * Whether or not the search bar should be visible + */ + @Input() + searchEnabled = true; + + /** + * The width of the sidebar (bootstrap columns) + */ + @Input() + sideBarWidth = 3; + + /** + * The currently applied configuration (determines title of search) + */ + @Input() + configuration$: Observable; + + /** + * The current context + */ + @Input() + context: Context; + + protected inAndOutputNames: (keyof ConfigurationSearchPageComponent & keyof this)[] = + ['configuration', 'fixedFilterQuery', 'inPlaceSearch', 'searchEnabled', 'sideBarWidth', 'configuration$', 'context']; + + protected getComponentName(): string { + return 'ConfigurationSearchPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/+search-page/configuration-search-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./configuration-search-page.component`); + } + +} diff --git a/src/app/+search-page/themed-search-page.component.ts b/src/app/+search-page/themed-search-page.component.ts new file mode 100644 index 0000000000..0b2f673373 --- /dev/null +++ b/src/app/+search-page/themed-search-page.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { SearchPageComponent } from './search-page.component'; + +/** + * Themed wrapper for SearchPageComponent + */ +@Component({ + selector: 'ds-themed-search-page', + styleUrls: [], + templateUrl: '../shared/theme-support/themed.component.html', +}) +export class ThemedSearchPageComponent extends ThemedComponent { + + protected getComponentName(): string { + return 'SearchPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/+search-page/search-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./search-page.component`); + } +} diff --git a/src/app/+submit-page/submit-page-routing.module.ts b/src/app/+submit-page/submit-page-routing.module.ts index 7a123bfc31..92c9bd8cdf 100644 --- a/src/app/+submit-page/submit-page-routing.module.ts +++ b/src/app/+submit-page/submit-page-routing.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { SubmissionSubmitComponent } from '../submission/submit/submission-submit.component'; +import { ThemedSubmissionSubmitComponent } from '../submission/submit/themed-submission-submit.component'; @NgModule({ imports: [ @@ -11,7 +11,7 @@ import { SubmissionSubmitComponent } from '../submission/submit/submission-submi canActivate: [AuthenticatedGuard], path: '', pathMatch: 'full', - component: SubmissionSubmitComponent, + component: ThemedSubmissionSubmitComponent, data: { title: 'submission.submit.title' } } ]) diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/themed-workflow-item-delete.component.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/themed-workflow-item-delete.component.ts new file mode 100644 index 0000000000..498975075f --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/themed-workflow-item-delete.component.ts @@ -0,0 +1,26 @@ +import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { Component } from '@angular/core'; + +/** + * Themed wrapper for WorkflowItemDeleteComponent + */ + +@Component({ + selector: 'ds-themed-workflow-item-delete', + styleUrls: [], + templateUrl: './../../shared/theme-support/themed.component.html' +}) +export class ThemedWorkflowItemDeleteComponent extends ThemedComponent { + protected getComponentName(): string { + return 'WorkflowItemDeleteComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./workflow-item-delete.component`); + } +} diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/themed-workflow-item-send-back.component.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/themed-workflow-item-send-back.component.ts new file mode 100644 index 0000000000..a2cc68ac74 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/themed-workflow-item-send-back.component.ts @@ -0,0 +1,26 @@ +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { Component } from '@angular/core'; +import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component'; + +/** + * Themed wrapper for WorkflowItemActionPageComponent + */ + +@Component({ + selector: 'ds-themed-workflow-item-send-back', + styleUrls: [], + templateUrl: './../../shared/theme-support/themed.component.html' +}) +export class ThemedWorkflowItemSendBackComponent extends ThemedComponent { + protected getComponentName(): string { + return 'WorkflowItemSendBackComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./workflow-item-send-back.component`); + } +} diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts index 27b7fe1199..2d104cf376 100644 --- a/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts +++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts @@ -2,15 +2,11 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { SubmissionEditComponent } from '../submission/edit/submission-edit.component'; -import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component'; import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; -import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component'; -import { - WORKFLOW_ITEM_SEND_BACK_PATH, - WORKFLOW_ITEM_DELETE_PATH, - WORKFLOW_ITEM_EDIT_PATH -} from './workflowitems-edit-page-routing-paths'; +import { WORKFLOW_ITEM_DELETE_PATH, WORKFLOW_ITEM_EDIT_PATH, WORKFLOW_ITEM_SEND_BACK_PATH } from './workflowitems-edit-page-routing-paths'; +import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component'; +import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component'; +import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component'; @NgModule({ imports: [ @@ -22,19 +18,19 @@ import { { canActivate: [AuthenticatedGuard], path: WORKFLOW_ITEM_EDIT_PATH, - component: SubmissionEditComponent, + component: ThemedSubmissionEditComponent, data: { title: 'submission.edit.title' } }, { canActivate: [AuthenticatedGuard], path: WORKFLOW_ITEM_DELETE_PATH, - component: WorkflowItemDeleteComponent, + component: ThemedWorkflowItemDeleteComponent, data: { title: 'workflow-item.delete.title' } }, { canActivate: [AuthenticatedGuard], path: WORKFLOW_ITEM_SEND_BACK_PATH, - component: WorkflowItemSendBackComponent, + component: ThemedWorkflowItemSendBackComponent, data: { title: 'workflow-item.send-back.title' } } ] diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts index ef1e49abf5..6e4b3212e8 100644 --- a/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts +++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts @@ -5,6 +5,8 @@ import { WorkflowItemsEditPageRoutingModule } from './workflowitems-edit-page-ro import { SubmissionModule } from '../submission/submission.module'; import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component'; import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component'; +import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component'; +import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component'; @NgModule({ imports: [ @@ -13,7 +15,7 @@ import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflo SharedModule, SubmissionModule, ], - declarations: [WorkflowItemDeleteComponent, WorkflowItemSendBackComponent] + declarations: [WorkflowItemDeleteComponent, ThemedWorkflowItemDeleteComponent, WorkflowItemSendBackComponent, ThemedWorkflowItemSendBackComponent] }) /** * This module handles all modules that need to access the workflowitems edit page. 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 d10c53e138..791bb4f9aa 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 @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { SubmissionEditComponent } from '../submission/edit/submission-edit.component'; +import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component'; @NgModule({ imports: [ @@ -11,7 +11,7 @@ import { SubmissionEditComponent } from '../submission/edit/submission-edit.comp { canActivate: [AuthenticatedGuard], path: ':id/edit', - component: SubmissionEditComponent, + component: ThemedSubmissionEditComponent, data: { title: 'submission.edit.title' } } ]) diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 8db4ba5aa7..24108dadd5 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -5,6 +5,7 @@ import { Item } from './core/shared/item.model'; import { getCommunityPageRoute } from './+community-page/community-page-routing-paths'; import { getCollectionPageRoute } from './+collection-page/collection-page-routing-paths'; import { getItemPageRoute } from './+item-page/item-page-routing-paths'; +import { hasValue } from './shared/empty.util'; export const BITSTREAM_MODULE_PATH = 'bitstreams'; @@ -45,13 +46,15 @@ export function getWorkflowItemModuleRoute() { } export function getDSORoute(dso: DSpaceObject): string { - switch ((dso as any).type) { - case Community.type.value: - return getCommunityPageRoute(dso.uuid); - case Collection.type.value: - return getCollectionPageRoute(dso.uuid); - case Item.type.value: - return getItemPageRoute(dso.uuid); + if (hasValue(dso)) { + switch ((dso as any).type) { + case Community.type.value: + return getCommunityPageRoute(dso.uuid); + case Collection.type.value: + return getCollectionPageRoute(dso.uuid); + case Item.type.value: + return getItemPageRoute(dso as Item); + } } } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index e14672f1f5..13e1133d82 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -2,19 +2,9 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; -import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; import { AuthenticatedGuard } from './core/auth/authenticated.guard'; import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; -import { - ADMIN_MODULE_PATH, - BITSTREAM_MODULE_PATH, - FORBIDDEN_PATH, - FORGOT_PASSWORD_PATH, - INFO_MODULE_PATH, - PROFILE_MODULE_PATH, - REGISTER_PATH, - WORKFLOW_ITEM_MODULE_PATH -} from './app-routing-paths'; +import { ADMIN_MODULE_PATH, BITSTREAM_MODULE_PATH, FORBIDDEN_PATH, FORGOT_PASSWORD_PATH, INFO_MODULE_PATH, PROFILE_MODULE_PATH, REGISTER_PATH, WORKFLOW_ITEM_MODULE_PATH } from './app-routing-paths'; import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths'; import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths'; @@ -22,7 +12,8 @@ import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths'; import { ReloadGuard } from './core/reload/reload.guard'; import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard'; import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard'; -import { ForbiddenComponent } from './forbidden/forbidden.component'; +import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component'; +import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; @NgModule({ imports: [ @@ -30,7 +21,7 @@ import { ForbiddenComponent } from './forbidden/forbidden.component'; path: '', canActivate: [AuthBlockingGuard], children: [ { path: '', redirectTo: '/home', pathMatch: 'full' }, - { path: 'reload/:rnd', component: PageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] }, + { path: 'reload/:rnd', component: ThemedPageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] }, { path: 'home', loadChildren: () => import('./+home-page/home-page.module') @@ -86,6 +77,11 @@ import { ForbiddenComponent } from './forbidden/forbidden.component'; .then((m) => m.ItemPageModule), canActivate: [EndUserAgreementCurrentUserGuard] }, + { path: 'entities/:entity-type', + loadChildren: () => import('./+item-page/item-page.module') + .then((m) => m.ItemPageModule), + canActivate: [EndUserAgreementCurrentUserGuard] + }, { path: BITSTREAM_MODULE_PATH, loadChildren: () => import('./+bitstream-page/bitstream-page.module') @@ -168,14 +164,14 @@ import { ForbiddenComponent } from './forbidden/forbidden.component'; }, { path: FORBIDDEN_PATH, - component: ForbiddenComponent + component: ThemedForbiddenComponent }, { path: 'statistics', loadChildren: () => import('./statistics-page/statistics-page-routing.module') .then((m) => m.StatisticsPageRoutingModule), }, - { path: '**', pathMatch: 'full', component: PageNotFoundComponent }, + { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }, ]} ],{ onSameUrlNavigation: 'reload', diff --git a/src/app/app.component.html b/src/app/app.component.html index fa534855e7..a40e5ea163 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,30 +1 @@ -
- -
- - - - -
-
- -
- -
- -
- -
- - -
-
- -
- -
-
+ diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 397f9d66e4..e69de29bb2 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,66 +0,0 @@ -@import "../styles/helpers/font_awesome_imports.scss"; -@import "../../node_modules/bootstrap/scss/bootstrap.scss"; -@import "../../node_modules/nouislider/distribute/nouislider.min"; - -html { - position: relative; - min-height: 100%; -} - -body { - overflow-x: hidden; -} - -// Sticky Footer -.outer-wrapper { - display: flex; - margin: 0; -} - -.inner-wrapper { - flex: 1 1 auto; - flex-flow: column nowrap; - display: flex; - min-height: 100vh; - flex-direction: column; - width: 100%; - position: relative; -} - -.main-content { - z-index: $main-z-index; - flex: 1 1 100%; - margin-top: $content-spacing; - margin-bottom: $content-spacing; -} - -.alert.hide { - padding: 0; - margin: 0; -} - -ds-header-navbar-wrapper { - z-index: none; -} - -ds-admin-sidebar { - position: fixed; - z-index: $sidebar-z-index; -} - -.ds-full-screen-loader { - height: 100vh; -} - -.sticky-top { - z-index: 0; -} - -.media-viewer - .change-gallery - .ngx-gallery - ngx-gallery-preview.ngx-gallery-active { - right: 0; - left: auto; - width: 96%; -} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 7eec1c0ff9..3f2dc45ce7 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,7 +1,7 @@ import { Store, StoreModule } from '@ngrx/store'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { CommonModule, DOCUMENT } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; @@ -32,6 +32,10 @@ import { storeModuleConfig } from './app.reducer'; import { LocaleService } from './core/locale/locale.service'; import { authReducer } from './core/auth/auth.reducer'; import { provideMockStore } from '@ngrx/store/testing'; +import { GoogleAnalyticsService } from './statistics/google-analytics.service'; +import { ThemeService } from './shared/theme-support/theme.service'; +import { getMockThemeService } from './shared/mocks/theme-service.mock'; +import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; let comp: AppComponent; let fixture: ComponentFixture; @@ -42,15 +46,18 @@ const initialState = { describe('App component', () => { + let breadcrumbsServiceSpy; + function getMockLocaleService(): LocaleService { return jasmine.createSpyObj('LocaleService', { setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode') }); } - // waitForAsync beforeEach - beforeEach(waitForAsync(() => { - return TestBed.configureTestingModule({ + const getDefaultTestBedConf = () => { + breadcrumbsServiceSpy = jasmine.createSpyObj(['listenForRouteChanges']); + + return { imports: [ CommonModule, StoreModule.forRoot(authReducer, storeModuleConfig), @@ -74,12 +81,19 @@ describe('App component', () => { { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: LocaleService, useValue: getMockLocaleService() }, + { provide: ThemeService, useValue: getMockThemeService() }, + { provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy }, provideMockStore({ initialState }), AppComponent, RouteService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] - }); + }; + }; + + // waitForAsync beforeEach + beforeEach(waitForAsync(() => { + return TestBed.configureTestingModule(getDefaultTestBedConf()); })); // synchronous beforeEach @@ -113,4 +127,65 @@ describe('App component', () => { }); }); + + describe('the constructor', () => { + it('should call breadcrumbsService.listenForRouteChanges', () => { + expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1); + }); + }); + + describe('when GoogleAnalyticsService is provided', () => { + let googleAnalyticsSpy; + + beforeEach(() => { + // NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset + TestBed.resetTestingModule(); + TestBed.configureTestingModule(getDefaultTestBedConf()); + googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [ + 'addTrackingIdToPage', + ]); + TestBed.overrideProvider(GoogleAnalyticsService, {useValue: googleAnalyticsSpy}); + fixture = TestBed.createComponent(AppComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create component', () => { + expect(comp).toBeTruthy(); + }); + + describe('the constructor', () => { + it('should call googleAnalyticsService.addTrackingIdToPage()', () => { + expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('when ThemeService returns a custom theme', () => { + let document; + let headSpy; + + beforeEach(() => { + // NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset + TestBed.resetTestingModule(); + TestBed.configureTestingModule(getDefaultTestBedConf()); + TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')}); + document = TestBed.inject(DOCUMENT); + headSpy = jasmine.createSpyObj('head', ['appendChild']); + spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]); + fixture = TestBed.createComponent(AppComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should append a link element with the correct attributes to the head element', () => { + const link = document.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('type', 'text/css'); + link.setAttribute('class', 'theme-css'); + link.setAttribute('href', '/custom-theme.css'); + + expect(headSpy.appendChild).toHaveBeenCalledWith(link); + }); + }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 10cda90755..a596ba56cd 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -5,12 +5,12 @@ import { Component, HostListener, Inject, - OnInit, Optional, - ViewEncapsulation + OnInit, + Optional, } from '@angular/core'; import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router'; -import { BehaviorSubject, combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; +import { BehaviorSubject, Observable, of } from 'rxjs'; import { select, Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; @@ -23,24 +23,26 @@ import { isAuthenticationBlocking } from './core/auth/selectors'; import { AuthService } from './core/auth/auth.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { MenuService } from './shared/menu/menu.service'; -import { MenuID } from './shared/menu/initial-menus-state'; -import { slideSidebarPadding } from './shared/animations/slide'; import { HostWindowService } from './shared/host-window.service'; -import { Theme } from '../config/theme.inferface'; +import { ThemeConfig } from '../config/theme.model'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; import { environment } from '../environments/environment'; import { models } from './core/core.module'; import { LocaleService } from './core/locale/locale.service'; -import { hasValue } from './shared/empty.util'; +import { hasValue, isNotEmpty } from './shared/empty.util'; import { KlaroService } from './shared/cookies/klaro.service'; +import { GoogleAnalyticsService } from './statistics/google-analytics.service'; +import { DOCUMENT } from '@angular/common'; +import { ThemeService } from './shared/theme-support/theme.service'; +import { BASE_THEME_NAME } from './shared/theme-support/theme.constants'; +import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects'; +import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; @Component({ selector: 'ds-app', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, - animations: [slideSidebarPadding] }) export class AppComponent implements OnInit, AfterViewInit { isLoading$: BehaviorSubject = new BehaviorSubject(true); @@ -48,7 +50,7 @@ export class AppComponent implements OnInit, AfterViewInit { slideSidebarOver: Observable; collapsedSidebarWidth: Observable; totalSidebarWidth: Observable; - theme: Observable = of({} as any); + theme: Observable = of({} as any); notificationOptions = environment.notifications; models; @@ -59,6 +61,8 @@ export class AppComponent implements OnInit, AfterViewInit { constructor( @Inject(NativeWindowService) private _window: NativeWindowRef, + @Inject(DOCUMENT) private document: any, + private themeService: ThemeService, private translate: TranslateService, private store: Store, private metadata: MetadataService, @@ -70,11 +74,24 @@ export class AppComponent implements OnInit, AfterViewInit { private menuService: MenuService, private windowService: HostWindowService, private localeService: LocaleService, - @Optional() private cookiesService: KlaroService + private breadcrumbsService: BreadcrumbsService, + @Optional() private cookiesService: KlaroService, + @Optional() private googleAnalyticsService: GoogleAnalyticsService, ) { /* Use models object so all decorators are actually called */ this.models = models; + + this.themeService.getThemeName$().subscribe((themeName: string) => { + if (hasValue(themeName)) { + this.setThemeCss(themeName); + } else if (hasValue(DEFAULT_THEME_CONFIG)) { + this.setThemeCss(DEFAULT_THEME_CONFIG.name); + } else { + this.setThemeCss(BASE_THEME_NAME); + } + }); + // Load all the languages that are defined as active from the config file translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code)); @@ -84,10 +101,14 @@ export class AppComponent implements OnInit, AfterViewInit { // set the current language code this.localeService.setCurrentLanguageCode(); - angulartics2GoogleAnalytics.startTracking(); + // analytics + if (hasValue(googleAnalyticsService)) { + googleAnalyticsService.addTrackingIdToPage(); + } angulartics2DSpace.startTracking(); metadata.listenForRouteChange(); + breadcrumbsService.listenForRouteChanges(); if (environment.debug) { console.info(environment); @@ -111,17 +132,6 @@ export class AppComponent implements OnInit, AfterViewInit { const color: string = environment.production ? 'red' : 'green'; console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`); this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight); - - this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN); - - this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth'); - this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth'); - - const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN); - this.slideSidebarOver = combineLatestObservable(sidebarCollapsed, this.windowService.isXsOrSm()) - .pipe( - map(([collapsed, mobile]) => collapsed || mobile) - ); } private storeCSSVariables() { @@ -172,4 +182,34 @@ export class AppComponent implements OnInit, AfterViewInit { this.cookiesService.initialize(); } } + + /** + * Update the theme css file in + * + * @param themeName The name of the new theme + * @private + */ + private setThemeCss(themeName: string): void { + const head = this.document.getElementsByTagName('head')[0]; + // Array.from to ensure we end up with an array, not an HTMLCollection, which would be + // automatically updated if we add nodes later + const currentThemeLinks = Array.from(this.document.getElementsByClassName('theme-css')); + const link = this.document.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('type', 'text/css'); + link.setAttribute('class', 'theme-css'); + link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`); + // wait for the new css to download before removing the old one to prevent a + // flash of unstyled content + link.onload = () => { + if (isNotEmpty(currentThemeLinks)) { + currentThemeLinks.forEach((currentThemeLink: any) => { + if (hasValue(currentThemeLink)) { + currentThemeLink.remove(); + } + }); + } + }; + head.appendChild(link); + } } diff --git a/src/app/app.effects.ts b/src/app/app.effects.ts index 64573609c7..871fae8d6b 100644 --- a/src/app/app.effects.ts +++ b/src/app/app.effects.ts @@ -3,11 +3,13 @@ import { NotificationsEffects } from './shared/notifications/notifications.effec import { NavbarEffects } from './navbar/navbar.effects'; import { SidebarEffects } from './shared/sidebar/sidebar-effects.service'; import { RelationshipEffects } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects'; +import { ThemeEffects } from './shared/theme-support/theme.effects'; export const appEffects = [ StoreEffects, NavbarEffects, NotificationsEffects, SidebarEffects, + ThemeEffects, RelationshipEffects ]; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index be1233fd98..5bfa593ef1 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -43,6 +43,14 @@ import { ForbiddenComponent } from './forbidden/forbidden.component'; import { AuthInterceptor } from './core/auth/auth.interceptor'; import { LocaleInterceptor } from './core/locale/locale.interceptor'; import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor'; +import { RootComponent } from './root/root.component'; +import { ThemedRootComponent } from './root/themed-root.component'; +import { ThemedEntryComponentModule } from '../themes/themed-entry-component.module'; +import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component'; +import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; +import { ThemedHeaderComponent } from './header/themed-header.component'; +import { ThemedFooterComponent } from './footer/themed-footer.component'; +import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component'; export function getBase() { return environment.ui.nameSpace; @@ -65,6 +73,7 @@ const IMPORTS = [ EffectsModule.forRoot(appEffects), StoreModule.forRoot(appReducers, storeModuleConfig), StoreRouterConnectingModule.forRoot(), + ThemedEntryComponentModule.withEntryComponents(), ]; IMPORTS.push( @@ -120,22 +129,28 @@ const PROVIDERS = [ const DECLARATIONS = [ AppComponent, + RootComponent, + ThemedRootComponent, HeaderComponent, + ThemedHeaderComponent, HeaderNavbarWrapperComponent, AdminSidebarComponent, AdminSidebarSectionComponent, ExpandableAdminSidebarSectionComponent, FooterComponent, + ThemedFooterComponent, PageNotFoundComponent, + ThemedPageNotFoundComponent, NotificationComponent, NotificationsBoardComponent, SearchNavbarComponent, BreadcrumbsComponent, + ThemedBreadcrumbsComponent, ForbiddenComponent, + ThemedForbiddenComponent, ]; const EXPORTS = [ - AppComponent ]; @NgModule({ @@ -150,7 +165,8 @@ const EXPORTS = [ ...DECLARATIONS, ], exports: [ - ...EXPORTS + ...EXPORTS, + ...DECLARATIONS, ] }) export class AppModule { diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index 813b8d0f4f..5dffda5e94 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -12,7 +12,10 @@ import { metadataRegistryReducer, MetadataRegistryState } from './+admin/admin-registries/metadata-registry/metadata-registry.reducers'; -import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer'; +import { + CommunityListReducer, + CommunityListState +} from './community-list-page/community-list.reducer'; import { hasValue } from './shared/empty.util'; import { NameVariantListsState, @@ -20,19 +23,32 @@ import { } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; import { formReducer, FormState } from './shared/form/form.reducer'; import { menusReducer, MenusState } from './shared/menu/menu.reducer'; -import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers'; +import { + notificationsReducer, + NotificationsState +} from './shared/notifications/notifications.reducers'; import { selectableListReducer, SelectableListsState } from './shared/object-list/selectable-list/selectable-list.reducer'; -import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer'; +import { + ObjectSelectionListState, + objectSelectionReducer +} from './shared/object-select/object-select.reducer'; import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer'; import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer'; -import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer'; -import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer'; +import { + filterReducer, + SearchFiltersState +} from './shared/search/search-filters/search-filter/search-filter.reducer'; +import { + sidebarFilterReducer, + SidebarFiltersState +} from './shared/sidebar/filter/sidebar-filter.reducer'; import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; +import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer'; export interface AppState { router: fromRouter.RouterReducerState; @@ -45,6 +61,7 @@ export interface AppState { searchFilter: SearchFiltersState; truncatable: TruncatablesState; cssVariables: CSSVariablesState; + theme: ThemeState; menus: MenusState; objectSelection: ObjectSelectionListState; selectableLists: SelectableListsState; @@ -65,6 +82,7 @@ export const appReducers: ActionReducerMap = { searchFilter: filterReducer, truncatable: truncatableReducer, cssVariables: cssVariablesReducer, + theme: themeReducer, menus: menusReducer, objectSelection: objectSelectionReducer, selectableLists: selectableListReducer, diff --git a/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts b/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts index 0ff8fc5033..cc9277ee50 100644 --- a/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts +++ b/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts @@ -1,4 +1,4 @@ -import { BreadcrumbsService } from '../../core/breadcrumbs/breadcrumbs.service'; +import { BreadcrumbsProviderService } from '../../core/breadcrumbs/breadcrumbsProviderService'; /** * Interface for breadcrumb configuration objects @@ -7,7 +7,7 @@ export interface BreadcrumbConfig { /** * The service used to calculate the breadcrumb object */ - provider: BreadcrumbsService; + provider: BreadcrumbsProviderService; /** * The key that is used to calculate the breadcrumb display value diff --git a/src/app/breadcrumbs/breadcrumbs.component.html b/src/app/breadcrumbs/breadcrumbs.component.html index 1f142526e2..a6072ed58a 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.html +++ b/src/app/breadcrumbs/breadcrumbs.component.html @@ -1,5 +1,5 @@ -