From 5a6e4b12787ee21c16745a5c419167700fae1e9a Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 25 Feb 2021 15:04:32 +0100 Subject: [PATCH] Add support for dynamic themes --- README.md | 1 - angular.json | 18 +- package.json | 4 +- postcss.config.js | 2 +- scripts/sync-build-dir.js | 22 - ...arch-result-grid-element.component.spec.ts | 5 + ...in-search-result-grid-element.component.ts | 4 +- .../admin-sidebar.component.scss | 30 +- ...dable-admin-sidebar-section.component.scss | 4 +- ...in-workflow-grid-element.component.spec.ts | 5 + ...t-admin-workflow-grid-element.component.ts | 14 +- .../edit-bitstream-page.component.scss | 2 +- .../collection-page.resolver.spec.ts | 8 +- .../collection-page.resolver.ts | 33 +- .../community-page.resolver.spec.ts | 8 +- .../community-page.resolver.ts | 32 +- ...page-sub-collection-list.component.spec.ts | 6 + ...-page-sub-community-list.component.spec.ts | 6 + .../home-news/home-news.component.scss | 6 +- .../home-news/home-news.component.ts | 3 +- .../home-news/themed-home-news.component.scss | 0 .../home-news/themed-home-news.component.ts | 27 + .../+home-page/home-page-routing.module.ts | 4 +- src/app/+home-page/home-page.component.html | 2 +- src/app/+home-page/home-page.component.ts | 6 +- src/app/+home-page/home-page.module.ts | 19 +- .../+home-page/themed-home-page.component.ts | 26 + ...top-level-community-list.component.spec.ts | 6 + .../edit-item-page.component.scss | 2 +- .../item-bitstreams.component.scss | 14 +- .../edit-in-place-field.component.scss | 10 +- .../item-metadata.component.scss | 12 +- .../edit-relationship-list.component.scss | 6 +- .../edit-relationship.component.scss | 4 +- .../item-relationships.component.scss | 10 +- .../full-file-section.component.scss | 4 +- src/app/+item-page/item-page.module.ts | 41 +- src/app/+item-page/item-page.resolver.ts | 24 +- src/app/+login-page/login-page.component.scss | 4 +- src/app/+search-page/search.component.scss | 2 +- src/app/app-routing-paths.ts | 17 +- src/app/app.component.html | 31 +- src/app/app.component.scss | 53 -- src/app/app.component.ts | 76 +- src/app/app.effects.ts | 2 + src/app/app.module.ts | 10 +- src/app/app.reducer.ts | 28 +- .../collection-breadcrumb.resolver.ts | 9 +- .../community-breadcrumb.resolver.ts | 7 +- .../breadcrumbs/dso-breadcrumbs.service.ts | 2 +- src/app/core/breadcrumbs/dso-name.service.ts | 2 +- .../breadcrumbs/item-breadcrumb.resolver.ts | 12 +- src/app/core/cache/builders/link.service.ts | 32 +- .../cache/server-sync-buffer.effects.spec.ts | 9 +- .../core/cache/server-sync-buffer.effects.ts | 3 +- .../object-updates.effects.spec.ts | 8 +- .../object-updates/object-updates.effects.ts | 6 +- src/app/core/index/index.effects.spec.ts | 5 +- src/app/core/index/index.effects.ts | 3 +- src/app/core/resolving/resolver.actions.ts | 27 + src/app/core/shared/context.model.ts | 2 +- ...ult-list-submission-element.component.scss | 4 +- ...ult-list-submission-element.component.scss | 4 +- src/app/footer/footer.component.scss | 50 +- .../header-navbar-wrapper.component.scss | 6 +- src/app/header/header.component.scss | 10 +- .../expandable-navbar-section.component.scss | 8 +- src/app/navbar/navbar.component.scss | 18 +- src/app/navbar/navbar.component.ts | 1 - src/app/navbar/navbar.effects.ts | 3 +- src/app/navbar/navbar.module.ts | 2 + src/app/root/root.component.html | 30 + .../app/root/root.component.scss | 0 src/app/root/root.component.spec.ts | 77 ++ src/app/root/root.component.ts | 79 ++ src/app/root/themed-root.component.ts | 35 + .../search-navbar.component.scss | 4 +- src/app/shared/chips/chips.component.scss | 2 +- .../collection-dropdown.component.scss | 6 +- .../dso-page-edit-button.component.scss | 2 +- .../dso-selector/dso-selector.component.scss | 2 +- .../file-dropzone-no-uploader.scss | 14 +- ...sting-metadata-list-element.component.scss | 2 +- ...sting-relation-list-element.component.scss | 2 +- .../dynamic-form-array.component.scss | 18 +- .../lookup/dynamic-lookup.component.scss | 8 +- .../onebox/dynamic-onebox.component.scss | 8 +- ...dynamic-scrollable-dropdown.component.scss | 12 +- .../models/tag/dynamic-tag.component.scss | 8 +- ...-lookup-relation-search-tab.component.scss | 4 +- src/app/shared/form/form.component.scss | 6 +- .../input-suggestions.component.scss | 2 +- .../container/log-in-container.component.scss | 12 +- ...etadata-representation-loader.component.ts | 8 +- .../metadata-representation.decorator.ts | 40 +- src/app/shared/mocks/theme-service.mock.ts | 7 + src/app/shared/ngrx/no-op.action.ts | 14 + .../notification/notification.component.scss | 10 +- .../notifications-board.component.scss | 4 +- ...table-object-component-loader.component.ts | 17 +- .../listable-object.decorator.ts | 42 +- .../object-detail.component.scss | 2 +- .../object-grid/object-grid.component.scss | 18 +- .../page-size-selector.component.scss | 2 +- .../search-form/search-form.component.scss | 4 +- .../search-authority-filter.component.scss | 4 +- .../search-boolean-filter.component.scss | 2 +- .../search-facet-option.component.scss | 4 +- .../search-facet-range-option.component.scss | 6 +- ...earch-facet-selected-option.component.scss | 4 +- .../search-filter.component.scss | 4 +- .../search-hierarchy-filter.component.scss | 2 +- .../search-range-filter.component.scss | 15 +- .../search-text-filter.component.scss | 2 +- .../search-settings.component.scss | 2 +- .../search-sidebar.component.scss | 8 +- ...ebar-filter-selected-option.component.scss | 2 +- .../filter/sidebar-filter.component.scss | 4 +- .../sidebar/page-with-sidebar.component.scss | 6 +- .../sidebar/sidebar-dropdown.component.scss | 2 +- .../date/starts-with-date.component.scss | 4 +- .../text/starts-with-text.component.scss | 4 +- src/app/shared/theme-support/theme.actions.ts | 22 + .../shared/theme-support/theme.constants.ts | 1 + .../theme-support/theme.effects.spec.ts | 173 ++++ src/app/shared/theme-support/theme.effects.ts | 185 ++++ .../theme-support/theme.reducer.spec.ts | 18 + src/app/shared/theme-support/theme.reducer.ts | 22 + src/app/shared/theme-support/theme.service.ts | 46 + .../theme-support/themed.component.html | 1 + .../theme-support/themed.component.scss | 0 .../shared/theme-support/themed.component.ts | 116 +++ .../truncatable-part.component.scss | 1 + .../shared/uploader/uploader.component.scss | 18 +- src/app/store.actions.ts | 2 +- .../submission-form-collection.component.scss | 6 +- ...submission-form-section-add.component.scss | 2 +- .../form/submission-form.component.scss | 14 +- ...n-import-external-searchbar.component.scss | 8 +- .../section-container.component.scss | 12 +- .../file/section-upload-file.component.scss | 6 +- src/config/global-config.interface.ts | 8 +- src/config/theme.inferface.ts | 5 - src/config/theme.model.spec.ts | 122 +++ src/config/theme.model.ts | 92 ++ src/environments/environment.common.ts | 38 +- src/environments/mock-environment.ts | 35 +- src/index.html | 1 + src/modules/app/browser-app.module.ts | 15 +- src/modules/app/server-app.module.ts | 7 +- src/styles.scss | 1 - src/styles/_bootstrap_variables_mapping.scss | 826 ++++++++++++++++++ src/styles/_custom_variables.scss | 100 ++- src/styles/_exposed_variables.scss | 18 +- src/styles/_global-styles.scss | 49 ++ src/styles/_mixins.scss | 12 +- src/styles/_variables.scss | 2 - src/styles/base-theme.scss | 6 + .../home-news/home-news.component.html | 0 .../home-news/home-news.component.scss | 0 .../home-news/home-news.component.ts | 16 + .../app/+home-page/home-page.component.html | 0 .../app/+home-page/home-page.component.scss | 0 .../app/+home-page/home-page.component.ts | 13 + .../publication/publication.component.html | 0 .../publication/publication.component.scss | 0 .../publication/publication.component.ts | 22 + .../custom/app/root/root.component.html | 0 .../custom/app/root/root.component.scss | 0 src/themes/custom/app/root/root.component.ts | 15 + src/themes/custom/entry-components.ts | 5 + src/themes/custom/styles/_global-styles.scss | 4 + .../styles/_theme_css_variable_overrides.scss | 8 + .../_theme_sass_variable_overrides.scss | 16 + src/themes/custom/styles/theme.scss | 12 + src/themes/custom/theme.module.ts | 96 ++ .../home-news/home-news.component.html | 0 .../home-news/home-news.component.scss | 17 + .../app/+home-page/home-page.component.html | 0 .../app/+home-page/home-page.component.scss | 18 +- .../simple/item-page.component.html | 0 .../publication/publication.component.html | 0 .../publication/publication.component.scss | 30 + .../journal-issue.component.html | 0 .../journal-issue.component.scss | 30 + .../journal-volume.component.html | 0 .../journal-volume.component.scss | 30 + .../item-pages/journal/journal.component.html | 0 .../item-pages/journal/journal.component.scss | 38 + .../org-unit/org-unit.component.html | 0 .../org-unit/org-unit.component.scss | 30 + .../item-pages/person/person.component.html | 0 .../item-pages/person/person.component.scss | 38 + .../item-pages/project/project.component.html | 0 .../item-pages/project/project.component.scss | 30 + .../mantis/app/navbar/navbar.component.html | 0 .../mantis/app/navbar/navbar.component.scss | 0 .../search-form/search-form.component.html | 0 .../search-facet-option.component.html | 0 .../search-facet-range-option.component.html | 0 .../search-filter.component.html | 0 .../search-filter.component.scss | 4 +- .../search-range-filter.component.scss | 2 +- .../search-filters.component.html | 0 .../search-settings.component.html | 0 .../search-settings.component.scss | 4 +- src/themes/mantis/readme.md | 2 + .../styles/_themed_bootstrap_variables.scss | 0 .../styles/_themed_custom_variables.scss | 4 + src/themes/themed-entry-component.module.ts | 23 + .../home-news/home-news.component.scss | 15 - .../publication/publication.component.scss | 30 - .../journal-issue.component.scss | 30 - .../journal-volume.component.scss | 30 - .../item-pages/journal/journal.component.scss | 38 - .../org-unit/org-unit.component.scss | 30 - .../item-pages/person/person.component.scss | 38 - .../item-pages/project/project.component.scss | 30 - .../styles/_themed_custom_variables.scss | 2 - tsconfig.app.json | 3 +- webpack/helpers.ts | 65 +- webpack/webpack.common.ts | 59 +- webpack/webpack.prod.ts | 4 +- yarn.lock | 131 ++- 224 files changed, 3438 insertions(+), 1026 deletions(-) delete mode 100644 scripts/sync-build-dir.js rename themes/default/styles/_themed_bootstrap_variables.scss => src/app/+home-page/home-news/themed-home-news.component.scss (100%) create mode 100644 src/app/+home-page/home-news/themed-home-news.component.ts create mode 100644 src/app/+home-page/themed-home-page.component.ts create mode 100644 src/app/core/resolving/resolver.actions.ts create mode 100644 src/app/root/root.component.html rename themes/default/styles/_themed_custom_variables.scss => src/app/root/root.component.scss (100%) create mode 100644 src/app/root/root.component.spec.ts create mode 100644 src/app/root/root.component.ts create mode 100644 src/app/root/themed-root.component.ts create mode 100644 src/app/shared/mocks/theme-service.mock.ts create mode 100644 src/app/shared/ngrx/no-op.action.ts create mode 100644 src/app/shared/theme-support/theme.actions.ts create mode 100644 src/app/shared/theme-support/theme.constants.ts create mode 100644 src/app/shared/theme-support/theme.effects.spec.ts create mode 100644 src/app/shared/theme-support/theme.effects.ts create mode 100644 src/app/shared/theme-support/theme.reducer.spec.ts create mode 100644 src/app/shared/theme-support/theme.reducer.ts create mode 100644 src/app/shared/theme-support/theme.service.ts create mode 100644 src/app/shared/theme-support/themed.component.html create mode 100644 src/app/shared/theme-support/themed.component.scss create mode 100644 src/app/shared/theme-support/themed.component.ts delete mode 100644 src/config/theme.inferface.ts create mode 100644 src/config/theme.model.spec.ts create mode 100644 src/config/theme.model.ts delete mode 100644 src/styles.scss create mode 100644 src/styles/_bootstrap_variables_mapping.scss create mode 100644 src/styles/_global-styles.scss create mode 100644 src/styles/base-theme.scss create mode 100644 src/themes/custom/app/+home-page/home-news/home-news.component.html create mode 100644 src/themes/custom/app/+home-page/home-news/home-news.component.scss create mode 100644 src/themes/custom/app/+home-page/home-news/home-news.component.ts create mode 100644 src/themes/custom/app/+home-page/home-page.component.html create mode 100644 src/themes/custom/app/+home-page/home-page.component.scss create mode 100644 src/themes/custom/app/+home-page/home-page.component.ts create mode 100644 src/themes/custom/app/+item-page/simple/item-types/publication/publication.component.html create mode 100644 src/themes/custom/app/+item-page/simple/item-types/publication/publication.component.scss create mode 100644 src/themes/custom/app/+item-page/simple/item-types/publication/publication.component.ts create mode 100644 src/themes/custom/app/root/root.component.html create mode 100644 src/themes/custom/app/root/root.component.scss create mode 100644 src/themes/custom/app/root/root.component.ts create mode 100644 src/themes/custom/entry-components.ts create mode 100644 src/themes/custom/styles/_global-styles.scss create mode 100644 src/themes/custom/styles/_theme_css_variable_overrides.scss create mode 100644 src/themes/custom/styles/_theme_sass_variable_overrides.scss create mode 100644 src/themes/custom/styles/theme.scss create mode 100644 src/themes/custom/theme.module.ts rename {themes => src/themes}/mantis/app/+home-page/home-news/home-news.component.html (100%) create mode 100644 src/themes/mantis/app/+home-page/home-news/home-news.component.scss rename {themes => src/themes}/mantis/app/+home-page/home-page.component.html (100%) rename {themes => src/themes}/mantis/app/+home-page/home-page.component.scss (56%) rename {themes => src/themes}/mantis/app/+item-page/simple/item-page.component.html (100%) rename {themes => src/themes}/mantis/app/+item-page/simple/item-types/publication/publication.component.html (100%) create mode 100644 src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss rename {themes => src/themes}/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html (100%) create mode 100644 src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss rename {themes => src/themes}/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html (100%) create mode 100644 src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss rename {themes => src/themes}/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html (100%) create mode 100644 src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss rename {themes => src/themes}/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html (100%) create mode 100644 src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss rename {themes => src/themes}/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html (100%) create mode 100644 src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss rename {themes => src/themes}/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html (100%) create mode 100644 src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss rename {themes => src/themes}/mantis/app/navbar/navbar.component.html (100%) rename {themes => src/themes}/mantis/app/navbar/navbar.component.scss (100%) rename {themes => src/themes}/mantis/app/shared/search-form/search-form.component.html (100%) rename {themes => src/themes}/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html (100%) rename {themes => src/themes}/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html (100%) rename {themes => src/themes}/mantis/app/shared/search/search-filters/search-filter/search-filter.component.html (100%) rename {themes => src/themes}/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss (63%) rename {themes => src/themes}/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss (82%) rename {themes => src/themes}/mantis/app/shared/search/search-filters/search-filters.component.html (100%) rename {themes => src/themes}/mantis/app/shared/search/search-settings/search-settings.component.html (100%) rename {themes => src/themes}/mantis/app/shared/search/search-settings/search-settings.component.scss (61%) create mode 100644 src/themes/mantis/readme.md rename {themes => src/themes}/mantis/styles/_themed_bootstrap_variables.scss (100%) create mode 100644 src/themes/mantis/styles/_themed_custom_variables.scss create mode 100644 src/themes/themed-entry-component.module.ts delete mode 100644 themes/mantis/app/+home-page/home-news/home-news.component.scss delete mode 100644 themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss delete mode 100644 themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss delete mode 100644 themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss delete mode 100644 themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss delete mode 100644 themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss delete mode 100644 themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss delete mode 100644 themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss delete mode 100644 themes/mantis/styles/_themed_custom_variables.scss diff --git a/README.md b/README.md index ec8956033c1..3f5351ec82d 100644 --- a/README.md +++ b/README.md @@ -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 8555fe9c2d3..19cbe94be64 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 90ac7db0d98..d221d373fd4 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "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", "build:client-and-server-bundles": "ng build --prod && ng run dspace-angular:server:production --bundleDependencies true", @@ -179,7 +181,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 1c46e245eaa..df092d1d39f 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/sync-build-dir.js b/scripts/sync-build-dir.js deleted file mode 100644 index c147f139a5a..00000000000 --- 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-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 ccd893e6f03..dedada5f5fa 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 68ef5ffc5ea..13158204c5c 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-sidebar/admin-sidebar.component.scss b/src/app/+admin/admin-sidebar/admin-sidebar.component.scss index 36355fcab91..e6eb4a7037e 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 1f6e2886084..e3a807a14fe 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 fe3e0ff3de0..0b933d08595 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 67a961b3307..cf5746391c5 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/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss index d212b5347c2..13de59700c1 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/+collection-page/collection-page.resolver.spec.ts b/src/app/+collection-page/collection-page.resolver.spec.ts index 5ded339fb82..4b1ea9834c1 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 44d238fc97a..f6f87f117c1 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/+community-page/community-page.resolver.spec.ts b/src/app/+community-page/community-page.resolver.spec.ts index e75f5ad57e8..f181dbfff65 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 c5780f0a0f5..01de9294f37 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 73e96fcc9c5..13a91563c80 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 b52df64db73..21ba1b28b00 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/+home-page/home-news/home-news.component.scss b/src/app/+home-page/home-news/home-news.component.scss index 50d3619ad1d..e58dd08ccff 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 cebe2176238..62c8e94671a 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 00000000000..09abbf89356 --- /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 c94df566436..ec6a547359c 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 5515df595bb..5902fa30afd 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.component.ts b/src/app/+home-page/home-page.component.ts index 65caa014300..797b2094056 100644 --- a/src/app/+home-page/home-page.component.ts +++ b/src/app/+home-page/home-page.component.ts @@ -10,7 +10,7 @@ import { Site } from '../core/shared/site.model'; templateUrl: './home-page.component.html' }) export class HomePageComponent implements OnInit { - + testInput = 'Bingo!'; site$: Observable; constructor( @@ -23,4 +23,8 @@ export class HomePageComponent implements OnInit { map((data) => data.site as Site), ); } + + onOutput(event: string) { + console.log('testOutput:', event); + } } diff --git a/src/app/+home-page/home-page.module.ts b/src/app/+home-page/home-page.module.ts index 51e978bbfe8..d304c786966 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 00000000000..69d6a60908d --- /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 4e0b8d4d227..0daa0a0ae07 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/+item-page/edit-item-page/edit-item-page.component.scss b/src/app/+item-page/edit-item-page/edit-item-page.component.scss index bb3bdaaeb0e..7fd08c83afd 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/item-bitstreams/item-bitstreams.component.scss b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index 0400e765dea..1fcbe99702b 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-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 1790157fa5d..a2a6786b36f 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 a74ecb8f47c..c502f69f98b 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; + @media screen and (min-width: var(--ds-grid-breakpoints-sm)) { + 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-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 54498499d7a..1466447116e 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 a974ec9a174..b1b8ca22a30 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 39ad631342a..033cea8eeac 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; + @media screen and (min-width: var(--ds-grid-breakpoints-sm)) { + 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/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 5bb04cac2f4..c60f2f9ff96 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 @@ -1,5 +1,5 @@ -@media screen and (min-width: map-get($grid-breakpoints, md)) { +@media screen and (min-width: var(--ds-grid-breakpoints-md)) { dt { text-align: right; } -} \ No newline at end of file +} diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index 299048e0941..ed8d872b363 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -32,6 +32,26 @@ const ENTRY_COMPONENTS = [ UntypedItemComponent ]; +const DECLARATIONS = [ + ItemPageComponent, + FullItemPageComponent, + MetadataUriValuesComponent, + ItemPageAuthorFieldComponent, + ItemPageDateFieldComponent, + ItemPageAbstractFieldComponent, + ItemPageUriFieldComponent, + ItemPageTitleFieldComponent, + ItemPageFieldComponent, + FileSectionComponent, + CollectionsComponent, + FullFileSectionComponent, + PublicationComponent, + UntypedItemComponent, + ItemComponent, + UploadBitstreamComponent, + AbstractIncrementalListComponent, +]; + @NgModule({ imports: [ CommonModule, @@ -43,23 +63,10 @@ const ENTRY_COMPONENTS = [ ResearchEntitiesModule.withEntryComponents() ], declarations: [ - ItemPageComponent, - FullItemPageComponent, - MetadataUriValuesComponent, - ItemPageAuthorFieldComponent, - ItemPageDateFieldComponent, - ItemPageAbstractFieldComponent, - ItemPageUriFieldComponent, - ItemPageTitleFieldComponent, - ItemPageFieldComponent, - FileSectionComponent, - CollectionsComponent, - FullFileSectionComponent, - PublicationComponent, - UntypedItemComponent, - ItemComponent, - UploadBitstreamComponent, - AbstractIncrementalListComponent, + ...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 d90806bfc31..4bf353fffdf 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -7,9 +7,18 @@ 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'; +/** + * 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 +29,10 @@ 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 + ) { } /** @@ -31,12 +43,18 @@ 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(), ); + + itemRD$.subscribe((itemRD: RemoteData) => { + this.store.dispatch(new ResolvedAction(state.url, itemRD.payload)); + }); + + return itemRD$; } } diff --git a/src/app/+login-page/login-page.component.scss b/src/app/+login-page/login-page.component.scss index 58e7272e5f0..d628a5089f6 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/+search-page/search.component.scss b/src/app/+search-page/search.component.scss index 44d4aabd130..82de1f47729 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/app-routing-paths.ts b/src/app/app-routing-paths.ts index 8db4ba5aa7d..64c7e8e0a6c 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.uuid); + } } } diff --git a/src/app/app.component.html b/src/app/app.component.html index fa534855e79..a40e5ea163a 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 b18e7e1402c..e69de29bb2d 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,53 +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: $nav-z-index; -} - -ds-admin-sidebar { - position: fixed; - z-index: $sidebar-z-index; -} - -.ds-full-screen-loader { - height: 100vh; -} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 1ef5c868a66..8a38d14fe30 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,25 +23,25 @@ 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 { 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'; @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); @@ -49,7 +49,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; @@ -60,6 +60,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, @@ -77,6 +79,17 @@ export class AppComponent implements OnInit, AfterViewInit { /* 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)); @@ -116,17 +129,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() { @@ -177,4 +179,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 64573609c7a..871fae8d6b8 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 be1233fd980..cc428483c9f 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -43,6 +43,9 @@ 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'; export function getBase() { return environment.ui.nameSpace; @@ -65,6 +68,7 @@ const IMPORTS = [ EffectsModule.forRoot(appEffects), StoreModule.forRoot(appReducers, storeModuleConfig), StoreRouterConnectingModule.forRoot(), + ThemedEntryComponentModule.withEntryComponents(), ]; IMPORTS.push( @@ -120,6 +124,8 @@ const PROVIDERS = [ const DECLARATIONS = [ AppComponent, + RootComponent, + ThemedRootComponent, HeaderComponent, HeaderNavbarWrapperComponent, AdminSidebarComponent, @@ -135,7 +141,6 @@ const DECLARATIONS = [ ]; const EXPORTS = [ - AppComponent ]; @NgModule({ @@ -150,7 +155,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 813b8d0f4f1..5dffda5e94a 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/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index 8e0638cddbb..d41446c185e 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -3,7 +3,8 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { Collection } from '../shared/collection.model'; import { CollectionDataService } from '../data/collection-data.service'; -import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../+collection-page/collection-page.resolver'; /** * The class that resolves the BreadcrumbConfig object for a Collection @@ -22,10 +23,6 @@ export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver[] { - return [ - followLink('parentCommunity', undefined, true, true, true, - followLink('parentCommunity') - ) - ]; + return COLLECTION_PAGE_LINKS_TO_FOLLOW; } } diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts index e9d8b9879c7..27cc207c584 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -3,7 +3,8 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { CommunityDataService } from '../data/community-data.service'; import { Community } from '../shared/community.model'; -import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../+community-page/community-page.resolver'; /** * The class that resolves the BreadcrumbConfig object for a Community @@ -22,8 +23,6 @@ export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver[] { - return [ - followLink('parentCommunity') - ]; + return COMMUNITY_PAGE_LINKS_TO_FOLLOW; } } diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index cfc04ff513c..f822a953a88 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -28,7 +28,7 @@ export class DSOBreadcrumbsService implements BreadcrumbsService { return `${dso.firstMetadataValue('person.familyName')}, ${dso.firstMetadataValue('person.givenName')}`; }, diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index e4d7e81e98d..2b9bbd6b3d5 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -3,7 +3,8 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { ItemDataService } from '../data/item-data.service'; import { Item } from '../shared/item.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; -import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../+item-page/item-page.resolver'; /** * The class that resolves the BreadcrumbConfig object for an Item @@ -22,13 +23,6 @@ export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { * Requesting them as embeds will limit the number of requests */ get followLinks(): FollowLinkConfig[] { - return [ - followLink('owningCollection', undefined, true, true, true, - followLink('parentCommunity', undefined, true, true, true, - followLink('parentCommunity')) - ), - followLink('bundles'), - followLink('relationships') - ]; + return ITEM_PAGE_LINKS_TO_FOLLOW; } } diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index d340eec9d55..56a1154b771 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -3,7 +3,15 @@ import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { GenericConstructor } from '../../shared/generic-constructor'; import { HALResource } from '../../shared/hal-resource.model'; -import { getDataServiceFor, getLinkDefinition, getLinkDefinitions, LinkDefinition } from './build-decorators'; +import { + getDataServiceFor, + getLinkDefinition, + getLinkDefinitions, + LinkDefinition +} from './build-decorators'; +import { RemoteData } from '../../data/remote-data'; +import { Observable } from 'rxjs/internal/Observable'; +import { EMPTY } from 'rxjs'; /** * A Service to handle the resolving and removing @@ -33,12 +41,14 @@ export class LinkService { } /** - * Resolve the given {@link FollowLinkConfig} for the given model + * Resolve the given {@link FollowLinkConfig} for the given model and return the result. This does + * not attach the link result to the property on the model. Useful when you're working with a + * readonly object * * @param model the {@link HALResource} to resolve the link for * @param linkToFollow the {@link FollowLinkConfig} to resolve */ - public resolveLink(model, linkToFollow: FollowLinkConfig): T { + public resolveLinkWithoutAttaching(model, linkToFollow: FollowLinkConfig): Observable> { const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name); if (hasNoValue(matchingLinkDef)) { @@ -61,9 +71,9 @@ export class LinkService { try { if (matchingLinkDef.isList) { - model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); + return service.findAllByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); } else { - model[linkToFollow.name] = service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); + return service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); } } catch (e) { console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} at ${href}`); @@ -71,6 +81,18 @@ export class LinkService { } } } + return EMPTY; + } + + /** + * Resolve the given {@link FollowLinkConfig} for the given model and return the model with the + * link property attached. + * + * @param model the {@link HALResource} to resolve the link for + * @param linkToFollow the {@link FollowLinkConfig} to resolve + */ + public resolveLink(model, linkToFollow: FollowLinkConfig): T { + model[linkToFollow.name] = this.resolveLinkWithoutAttaching(model, linkToFollow); return model; } diff --git a/src/app/core/cache/server-sync-buffer.effects.spec.ts b/src/app/core/cache/server-sync-buffer.effects.spec.ts index a236d3bb71f..a53c6af9823 100644 --- a/src/app/core/cache/server-sync-buffer.effects.spec.ts +++ b/src/app/core/cache/server-sync-buffer.effects.spec.ts @@ -13,9 +13,14 @@ import { RestRequestMethod } from '../data/rest-request-method'; import { DSpaceObject } from '../shared/dspace-object.model'; import { ApplyPatchObjectCacheAction } from './object-cache.actions'; import { ObjectCacheService } from './object-cache.service'; -import { CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions'; +import { + CommitSSBAction, + EmptySSBAction, + ServerSyncBufferActionTypes +} from './server-sync-buffer.actions'; import { ServerSyncBufferEffects } from './server-sync-buffer.effects'; import { storeModuleConfig } from '../../app.reducer'; +import { NoOpAction } from '../../shared/ngrx/no-op.action'; describe('ServerSyncBufferEffects', () => { let ssbEffects: ServerSyncBufferEffects; @@ -143,7 +148,7 @@ describe('ServerSyncBufferEffects', () => { payload: { method: RestRequestMethod.PATCH } } }); - const expected = cold('b', { b: { type: 'NO_ACTION' } }); + const expected = cold('b', { b: new NoOpAction() }); expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected); }); diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 363d566e000..d8ed88e12c1 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -21,6 +21,7 @@ import { RestRequestMethod } from '../data/rest-request-method'; import { environment } from '../../../environments/environment'; import { ObjectCacheEntry } from './object-cache.reducer'; import { Operation } from 'fast-json-patch'; +import { NoOpAction } from '../../shared/ngrx/no-op.action'; @Injectable() export class ServerSyncBufferEffects { @@ -80,7 +81,7 @@ export class ServerSyncBufferEffects { switchMap((array) => [...array, new EmptySSBAction(action.payload)]) ); } else { - return observableOf({ type: 'NO_ACTION' }); + return observableOf(new NoOpAction()); } }) ); diff --git a/src/app/core/data/object-updates/object-updates.effects.spec.ts b/src/app/core/data/object-updates/object-updates.effects.spec.ts index 8c4467c0c0d..ffd20a73006 100644 --- a/src/app/core/data/object-updates/object-updates.effects.spec.ts +++ b/src/app/core/data/object-updates/object-updates.effects.spec.ts @@ -11,10 +11,14 @@ import { RemoveFieldUpdateAction, RemoveObjectUpdatesAction } from './object-updates.actions'; -import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; +import { + INotification, + Notification +} from '../../../shared/notifications/models/notification.model'; import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { filter } from 'rxjs/operators'; import { hasValue } from '../../../shared/empty.util'; +import { NoOpAction } from '../../../shared/ngrx/no-op.action'; describe('ObjectUpdatesEffects', () => { let updatesEffects: ObjectUpdatesEffects; @@ -97,7 +101,7 @@ describe('ObjectUpdatesEffects', () => { actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); actions = hot('b', { b: new ReinstateObjectUpdatesAction(testURL) }); updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => { - expect(t).toEqual({ type: 'NO_ACTION' }); + expect(t).toEqual(new NoOpAction()); } ); }); diff --git a/src/app/core/data/object-updates/object-updates.effects.ts b/src/app/core/data/object-updates/object-updates.effects.ts index 169fa89cd37..c9c3237ef5e 100644 --- a/src/app/core/data/object-updates/object-updates.effects.ts +++ b/src/app/core/data/object-updates/object-updates.effects.ts @@ -3,7 +3,8 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { DiscardObjectUpdatesAction, ObjectUpdatesAction, - ObjectUpdatesActionTypes, RemoveAllObjectUpdatesAction, + ObjectUpdatesActionTypes, + RemoveAllObjectUpdatesAction, RemoveObjectUpdatesAction } from './object-updates.actions'; import { delay, filter, map, switchMap, take, tap } from 'rxjs/operators'; @@ -17,6 +18,7 @@ import { RemoveNotificationAction } from '../../../shared/notifications/notifications.actions'; import { Action } from '@ngrx/store'; +import { NoOpAction } from '../../../shared/ngrx/no-op.action'; /** * NGRX effects for ObjectUpdatesActions @@ -111,7 +113,7 @@ export class ObjectUpdatesEffects { map((updateAction: ObjectUpdatesAction) => { if (updateAction.type === ObjectUpdatesActionTypes.REINSTATE) { // If someone reinstated, do nothing, just let the reinstating happen - return { type: 'NO_ACTION' }; + return new NoOpAction(); } // If someone performed another action, assume the user does not want to reinstate and remove all changes return removeAction; diff --git a/src/app/core/index/index.effects.spec.ts b/src/app/core/index/index.effects.spec.ts index 6e2bcac24f5..efca0ae3647 100644 --- a/src/app/core/index/index.effects.spec.ts +++ b/src/app/core/index/index.effects.spec.ts @@ -8,6 +8,7 @@ import { Item } from '../shared/item.model'; import { AddToIndexAction } from './index.actions'; import { IndexName } from './index.reducer'; import { provideMockStore } from '@ngrx/store/testing'; +import { NoOpAction } from '../../shared/ngrx/no-op.action'; describe('ObjectUpdatesEffects', () => { let indexEffects: UUIDIndexEffects; @@ -79,14 +80,14 @@ describe('ObjectUpdatesEffects', () => { it('should emit NO_ACTION when a AddToObjectCacheAction without an alternativeLink is dispatched', () => { action = new AddToObjectCacheAction(objectToCache, timeCompleted, msToLive, requestUUID, undefined); actions = hot('--a-', { a: action }); - const expected = cold('--b-', { b: { type: 'NO_ACTION' } }); + const expected = cold('--b-', { b: new NoOpAction() }); expect(indexEffects.addAlternativeObjectLink$).toBeObservable(expected); }); it('should emit NO_ACTION when a AddToObjectCacheAction with an alternativeLink that\'s the same as the objectToCache\'s selfLink is dispatched', () => { action = new AddToObjectCacheAction(objectToCache, timeCompleted, msToLive, requestUUID, objectToCache._links.self.href); actions = hot('--a-', { a: action }); - const expected = cold('--b-', { b: { type: 'NO_ACTION' } }); + const expected = cold('--b-', { b: new NoOpAction() }); expect(indexEffects.addAlternativeObjectLink$).toBeObservable(expected); }); }); diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 61abe90be97..a1ab0b20a8d 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -19,6 +19,7 @@ import { RestRequestMethod } from '../data/rest-request-method'; import { getUrlWithoutEmbedParams, uuidFromHrefSelector } from './index.selectors'; import { Store, select } from '@ngrx/store'; import { CoreState } from '../core.reducers'; +import { NoOpAction } from '../../shared/ngrx/no-op.action'; @Injectable() export class UUIDIndexEffects { @@ -53,7 +54,7 @@ export class UUIDIndexEffects { selfLink ); } else { - return { type: 'NO_ACTION' }; + return new NoOpAction(); } }) ); diff --git a/src/app/core/resolving/resolver.actions.ts b/src/app/core/resolving/resolver.actions.ts new file mode 100644 index 00000000000..db41d20b6ed --- /dev/null +++ b/src/app/core/resolving/resolver.actions.ts @@ -0,0 +1,27 @@ +import { type } from '../../shared/ngrx/type'; +import { Action } from '@ngrx/store'; +import { DSpaceObject } from '../shared/dspace-object.model'; + +export const ResolverActionTypes = { + RESOLVED: type('dspace/resolver/RESOLVED') +}; + +/** + * An action that indicates a route object has been resolved. + * + * It isn't used in a reducer for now. Its purpose is to be able to be notified that an object + * has been resolved in an effect. + */ +export class ResolvedAction implements Action { + type = ResolverActionTypes.RESOLVED; + payload: { + url: string, + dso: DSpaceObject + }; + + constructor(url: string, dso: DSpaceObject) { + this.payload = { url, dso }; + } +} + +export type ResolverAction = ResolvedAction; diff --git a/src/app/core/shared/context.model.ts b/src/app/core/shared/context.model.ts index 3613a143bd4..126896e3e12 100644 --- a/src/app/core/shared/context.model.ts +++ b/src/app/core/shared/context.model.ts @@ -3,7 +3,7 @@ */ export enum Context { - Undefined = 'undefined', + Any = 'undefined', ItemPage = 'itemPage', Search = 'search', Workflow = 'workflow', diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss index 78cc32591b2..2272183e51f 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss @@ -1,5 +1,3 @@ -$submission-relationship-thumbnail-width: 80px; - .person-thumbnail { - width: $submission-relationship-thumbnail-width; + width: var(--ds-submission-relationship-thumbnail-width); } diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss index 78cc32591b2..28da0b74cd7 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss @@ -1,5 +1,3 @@ -$submission-relationship-thumbnail-width: 80px; - .person-thumbnail { - width: $submission-relationship-thumbnail-width; + width: var(--ds-submission-relationship-thumbnail-width); } diff --git a/src/app/footer/footer.component.scss b/src/app/footer/footer.component.scss index fb51fc258dc..5020ae17b16 100644 --- a/src/app/footer/footer.component.scss +++ b/src/app/footer/footer.component.scss @@ -1,40 +1,42 @@ -$footer-bg: $gray-100; -$footer-border: 1px solid darken($footer-bg, 10%); -$footer-padding: $spacer * 1.5; -$footer-logo-height: 55px; +:host { + --ds-footer-bg: var(--bs-gray-100); + --ds-footer-border: 1px solid var(--bs-gray-300); + --ds-footer-padding: calc(var(--bs-spacer) * 1.5); + --ds-footer-logo-height: 55px; -.footer { - background-color: $footer-bg; - border-top: $footer-border; + .footer { + background-color: var(--ds-footer-bg); + border-top: var(--ds-footer-border); text-align: center; - padding: $footer-padding; - padding-bottom: $spacer; + padding: var(--ds-footer-padding); + padding-bottom: var(--bs-spacer); p { - margin: 0; + margin: 0; } img { - height: $footer-logo-height; + height: var(--ds-footer-logo-height); } ul { - padding-top: $spacer * 0.5; + padding-top: calc(var(--bs-spacer) * 0.5); - li { - display: inline-flex; - a { - padding: 0 $spacer/2; - color: inherit - } + li { + display: inline-flex; + a { + padding: 0 calc(var(--bs-spacer) / 2); + color: inherit + } - &:not(:last-child) { - &:after { - content: ''; - border-right: 1px map-get($theme-colors, secondary) solid; - } + &:not(:last-child) { + &:after { + content: ''; + border-right: 1px var(--bs-secondary) solid; + } - } } + } } + } } diff --git a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss index c3eba35b796..152c8dbaeeb 100644 --- a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss +++ b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss @@ -1,7 +1,7 @@ -@media screen and (max-width: map-get($grid-breakpoints, md)) { +@media screen and (max-width: var(--ds-grid-breakpoints-md)) { :host.open { - background-color: $white; + background-color: var(--bs-white); top: 0; position: sticky; } -} \ No newline at end of file +} diff --git a/src/app/header/header.component.scss b/src/app/header/header.component.scss index 70c66f119d0..c1d31e4e60d 100644 --- a/src/app/header/header.component.scss +++ b/src/app/header/header.component.scss @@ -1,7 +1,7 @@ .navbar-brand img { - height: $header-logo-height; - @media screen and (max-width: map-get($grid-breakpoints, sm)) { - height: $header-logo-height-xs; + height: var(--ds-header-logo-height); + @media screen and (max-width: var(--ds-grid-breakpoints-sm)) { + height: var(--ds-header-logo-height-xs); } } .navbar-toggler .navbar-toggler-icon { @@ -11,10 +11,10 @@ .navbar ::ng-deep { a { - color: $header-icon-color; + color: var(--ds-header-icon-color); &:hover, &focus { - color: darken($header-icon-color, 15%); + color: var(--ds-header-icon-color-hover); } } } diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss index f724c3e7511..044c1965457 100644 --- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss +++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss @@ -4,18 +4,18 @@ border-top-left-radius: 0; border-top-right-radius: 0; ::ng-deep a.nav-link { - padding-right: $spacer; - padding-left: $spacer; + padding-right: var(--bs-spacer); + padding-left: var(--bs-spacer); white-space: nowrap; } } /** Mobile menu styling **/ -@media screen and (max-width: map-get($grid-breakpoints, md)) { +@media screen and (max-width: var(--ds-grid-breakpoints-md)) { .dropdown-toggle { &:after { float: right; - margin-top: $spacer/2; + margin-top: calc(var(--bs-spacer) / 2); } } .dropdown-menu { diff --git a/src/app/navbar/navbar.component.scss b/src/app/navbar/navbar.component.scss index d0fa04991d9..2fa35a6f4b2 100644 --- a/src/app/navbar/navbar.component.scss +++ b/src/app/navbar/navbar.component.scss @@ -1,13 +1,13 @@ nav.navbar { - border-bottom: 1px $gray-400 solid; + border-bottom: 1px var(--bs-gray-400) solid; align-items: baseline; } /** Mobile menu styling **/ -@media screen and (max-width: map-get($grid-breakpoints, md)) { +@media screen and (max-width: var(--ds-grid-breakpoints-md)) { .navbar { width: 100%; - background-color: $white; + background-color: var(--bs-white); position: absolute; overflow: hidden; height: 0; @@ -17,19 +17,19 @@ nav.navbar { } } -@media screen and (min-width: map-get($grid-breakpoints, md)) { +@media screen and (min-width: var(--ds-grid-breakpoints-md)) { .reset-padding-md { - margin-left: -$spacer/2; - margin-right: -$spacer/2; + margin-left: -calc(var(--bs-spacer) / 2); + margin-right: -calc(var(--bs-spacer) / 2); } } /* TODO remove when https://github.com/twbs/bootstrap/issues/24726 is fixed */ .navbar-expand-md.navbar-container { - @media screen and (max-width: map-get($grid-breakpoints, md)) { + @media screen and (max-width: var(--ds-grid-breakpoints-md)) { > .container { - padding: 0 $spacer; + padding: 0 var(--bs-spacer); } padding: 0; } -} \ No newline at end of file +} diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index 06b493c80ac..1a306ea9326 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -85,5 +85,4 @@ export class NavbarComponent extends MenuComponent { }))); } - } diff --git a/src/app/navbar/navbar.effects.ts b/src/app/navbar/navbar.effects.ts index a50b2df5adc..6cb11f21c0b 100644 --- a/src/app/navbar/navbar.effects.ts +++ b/src/app/navbar/navbar.effects.ts @@ -12,6 +12,7 @@ import { import { MenuID } from '../shared/menu/initial-menus-state'; import { MenuService } from '../shared/menu/menu.service'; import { MenuState } from '../shared/menu/menu.reducer'; +import { NoOpAction } from '../shared/ngrx/no-op.action'; @Injectable() export class NavbarEffects { @@ -51,7 +52,7 @@ export class NavbarEffects { return new CollapseMenuAction(MenuID.PUBLIC); } } - return { type: 'NO_ACTION' }; + return new NoOpAction(); })); }) ); diff --git a/src/app/navbar/navbar.module.ts b/src/app/navbar/navbar.module.ts index 795b0541e73..5e1e11d10fb 100644 --- a/src/app/navbar/navbar.module.ts +++ b/src/app/navbar/navbar.module.ts @@ -9,6 +9,7 @@ import { NavbarSectionComponent } from './navbar-section/navbar-section.componen import { ExpandableNavbarSectionComponent } from './expandable-navbar-section/expandable-navbar-section.component'; import { NavbarComponent } from './navbar.component'; import { MenuModule } from '../shared/menu/menu.module'; +import { FormsModule } from '@angular/forms'; const effects = [ NavbarEffects @@ -24,6 +25,7 @@ const ENTRY_COMPONENTS = [ imports: [ CommonModule, MenuModule, + FormsModule, EffectsModule.forFeature(effects), CoreModule.forRoot() ], diff --git a/src/app/root/root.component.html b/src/app/root/root.component.html new file mode 100644 index 00000000000..96439cd9998 --- /dev/null +++ b/src/app/root/root.component.html @@ -0,0 +1,30 @@ +
+ +
+ + + + +
+
+ +
+ +
+ +
+ +
+ + +
+
+ +
+ +
+
diff --git a/themes/default/styles/_themed_custom_variables.scss b/src/app/root/root.component.scss similarity index 100% rename from themes/default/styles/_themed_custom_variables.scss rename to src/app/root/root.component.scss diff --git a/src/app/root/root.component.spec.ts b/src/app/root/root.component.spec.ts new file mode 100644 index 00000000000..c945a357640 --- /dev/null +++ b/src/app/root/root.component.spec.ts @@ -0,0 +1,77 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RootComponent } from './root.component'; +import { CommonModule } from '@angular/common'; +import { StoreModule } from '@ngrx/store'; +import { authReducer } from '../core/auth/auth.reducer'; +import { storeModuleConfig } from '../app.reducer'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; +import { NativeWindowRef, NativeWindowService } from '../core/services/window.service'; +import { MetadataService } from '../core/metadata/metadata.service'; +import { MetadataServiceMock } from '../shared/mocks/metadata-service.mock'; +import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; +import { AngularticsProviderMock } from '../shared/mocks/angulartics-provider.service.mock'; +import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider'; +import { AuthService } from '../core/auth/auth.service'; +import { AuthServiceMock } from '../shared/mocks/auth.service.mock'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RouterMock } from '../shared/mocks/router.mock'; +import { MockActivatedRoute } from '../shared/mocks/active-router.mock'; +import { MenuService } from '../shared/menu/menu.service'; +import { CSSVariableService } from '../shared/sass-helper/sass-helper.service'; +import { CSSVariableServiceStub } from '../shared/testing/css-variable-service.stub'; +import { HostWindowService } from '../shared/host-window.service'; +import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub'; +import { LocaleService } from '../core/locale/locale.service'; +import { provideMockStore } from '@ngrx/store/testing'; +import { RouteService } from '../core/services/route.service'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { MenuServiceStub } from '../shared/testing/menu-service.stub'; + +describe('RootComponent', () => { + let component: RootComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot(authReducer, storeModuleConfig), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [RootComponent], // declare the test component + providers: [ + { provide: NativeWindowService, useValue: new NativeWindowRef() }, + { provide: MetadataService, useValue: new MetadataServiceMock() }, + { provide: Angulartics2GoogleAnalytics, useValue: new AngularticsProviderMock() }, + { provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() }, + { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: Router, useValue: new RouterMock() }, + { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, + { provide: MenuService, useValue: new MenuServiceStub() }, + { provide: CSSVariableService, useClass: CSSVariableServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, + { provide: LocaleService, useValue: {} }, + provideMockStore({ core: { auth: { loading: false } } } as any), + RootComponent, + RouteService + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RootComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts new file mode 100644 index 00000000000..f9c475a8faa --- /dev/null +++ b/src/app/root/root.component.ts @@ -0,0 +1,79 @@ +import { map } from 'rxjs/operators'; +import { Component, Inject, OnInit, Optional, Input } from '@angular/core'; +import { Router } from '@angular/router'; + +import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; +import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; + +import { MetadataService } from '../core/metadata/metadata.service'; +import { HostWindowState } from '../shared/search/host-window.reducer'; +import { NativeWindowRef, NativeWindowService } from '../core/services/window.service'; +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 { HostWindowService } from '../shared/host-window.service'; +import { ThemeConfig } from '../../config/theme.model'; +import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider'; +import { environment } from '../../environments/environment'; +import { LocaleService } from '../core/locale/locale.service'; +import { KlaroService } from '../shared/cookies/klaro.service'; +import { slideSidebarPadding } from '../shared/animations/slide'; + +@Component({ + selector: 'ds-root', + templateUrl: './root.component.html', + styleUrls: ['./root.component.scss'], + animations: [slideSidebarPadding], +}) +export class RootComponent implements OnInit { + sidebarVisible: Observable; + slideSidebarOver: Observable; + collapsedSidebarWidth: Observable; + totalSidebarWidth: Observable; + theme: Observable = of({} as any); + notificationOptions = environment.notifications; + models; + + /** + * Whether or not the authentication is currently blocking the UI + */ + @Input() isNotAuthBlocking: boolean; + + /** + * Whether or not the the application is loading; + */ + @Input() isLoading: boolean; + + constructor( + @Inject(NativeWindowService) private _window: NativeWindowRef, + private translate: TranslateService, + private store: Store, + private metadata: MetadataService, + private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics, + private angulartics2DSpace: Angulartics2DSpace, + private authService: AuthService, + private router: Router, + private cssService: CSSVariableService, + private menuService: MenuService, + private windowService: HostWindowService, + private localeService: LocaleService, + @Optional() private cookiesService: KlaroService + ) { + } + + ngOnInit() { + 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) + ); + } +} diff --git a/src/app/root/themed-root.component.ts b/src/app/root/themed-root.component.ts new file mode 100644 index 00000000000..43aacc416f7 --- /dev/null +++ b/src/app/root/themed-root.component.ts @@ -0,0 +1,35 @@ +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { RootComponent } from './root.component'; +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ds-themed-root', + styleUrls: [], + templateUrl: '../shared/theme-support/themed.component.html', +}) +export class ThemedRootComponent extends ThemedComponent { + /** + * Whether or not the authentication is currently blocking the UI + */ + @Input() isNotAuthBlocking: boolean; + + /** + * Whether or not the the application is loading; + */ + @Input() isLoading: boolean; + + protected inAndOutputNames: (keyof RootComponent & keyof this)[] = ['isLoading', 'isNotAuthBlocking']; + + protected getComponentName(): string { + return 'RootComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/root/root.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./root.component`); + } + +} diff --git a/src/app/search-navbar/search-navbar.component.scss b/src/app/search-navbar/search-navbar.component.scss index 3606c47afcb..4de51a36dda 100644 --- a/src/app/search-navbar/search-navbar.component.scss +++ b/src/app/search-navbar/search-navbar.component.scss @@ -1,5 +1,5 @@ input[type="text"] { - margin-top: -0.5 * $font-size-base; + margin-top: calc(-0.5 * var(--bs-font-size-base)); &:focus { background-color: rgba(255, 255, 255, 0.5) !important; @@ -16,7 +16,7 @@ a.submit-icon { -@media screen and (max-width: map-get($grid-breakpoints, sm)) { +@media screen and (max-width: var(--ds-grid-breakpoints-sm)) { #query:focus { max-width: 250px !important; width: 40vw !important; diff --git a/src/app/shared/chips/chips.component.scss b/src/app/shared/chips/chips.component.scss index 76be7559200..a79deb0f2f6 100644 --- a/src/app/shared/chips/chips.component.scss +++ b/src/app/shared/chips/chips.component.scss @@ -1,5 +1,5 @@ .chip-selected { - background-color: map-get($theme-colors, info) !important; + background-color: var(--bs-info) !important; } .chip-label { diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.scss b/src/app/shared/collection-dropdown/collection-dropdown.component.scss index deecc395103..907a111900c 100644 --- a/src/app/shared/collection-dropdown/collection-dropdown.component.scss +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.scss @@ -1,15 +1,15 @@ .scrollable-menu { height: auto; - max-height: $dropdown-menu-max-height; + max-height: var(--ds-dropdown-menu-max-height); overflow-x: hidden; } .collection-item { - border-bottom: $dropdown-border-width solid $dropdown-border-color; + border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); } #collectionControlsDropdownMenu { outline: 0; left: 0 !important; - box-shadow: $btn-focus-box-shadow; + box-shadow: var(--bs-btn-focus-box-shadow); } diff --git a/src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.scss b/src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.scss index c308aa71b96..e8b7d689a35 100644 --- a/src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.scss +++ b/src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.scss @@ -1,3 +1,3 @@ .btn-dark { - background-color: $admin-sidebar-bg; + background-color: var(--ds-admin-sidebar-bg); } diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss b/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss index 37d2ebeca7b..6bc70532dff 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss @@ -1,5 +1,5 @@ .scrollable-menu { height: auto; - max-height: $dso-selector-list-max-height; + max-height: var(--ds-dso-selector-list-max-height); overflow-x: hidden; } diff --git a/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.scss b/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.scss index 94b0fefb4bc..5d06cd57185 100644 --- a/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.scss +++ b/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.scss @@ -1,5 +1,5 @@ .ds-base-drop-zone { - border: 2px dashed $gray-600; + border: 2px dashed var(--bs-gray-600); } .ds-document-drop-zone { @@ -9,21 +9,21 @@ } .ds-document-drop-zone-active { - z-index: $drop-zone-area-z-index !important; + z-index: var(--ds-drop-zone-area-z-index) !important; } .ds-document-drop-zone-inner { - background-color: rgba($white, 0.7); - z-index: $drop-zone-area-inner-z-index; + background-color: rgba(255, 255, 255, 0.7); + z-index: var(--ds-drop-zone-area-inner-z-index); top: 0; left: 0; } .ds-document-drop-zone-inner-content { - border: 4px dashed map-get($theme-colors, primary); - z-index: $drop-zone-area-inner-z-index; + border: 4px dashed var(--bs-primary); + z-index: var(--ds-drop-zone-area-inner-z-index); } .ds-document-drop-zone-inner-content p { - font-size: ($font-size-lg * 2.5); + font-size: calc(var(--bs-font-size-lg) * 2.5); } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss index ab63e324bde..1146e55750a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss @@ -1,3 +1,3 @@ span.text-contents{ - padding: $btn-padding-y 0; + padding: var(--bs-btn-padding-y) 0; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.scss index ab63e324bde..1146e55750a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.scss @@ -1,3 +1,3 @@ span.text-contents{ - padding: $btn-padding-y 0; + padding: var(--bs-btn-padding-y) 0; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss index b61bb9232b7..8fb13f9c557 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss @@ -5,16 +5,16 @@ } .cdk-drag { - margin-left: -(2 * $spacer); - margin-right: -(0.5 * $spacer); - padding-right: (0.5 * $spacer); + margin-left: calc(-2 * var(--bs-spacer)); + margin-right: calc(-0.5 * var(--bs-spacer)); + padding-right: calc(0.5 * var(--bs-spacer)); .drag-icon { visibility: hidden; - width: (2 * $spacer); - color: $gray-600; - margin: $btn-padding-y 0; - line-height: $btn-line-height; - text-indent: 0.5 * $spacer + width: calc(2 * var(--bs-spacer)); + color: var(--bs-gray-600); + margin: var(--bs-btn-padding-y) 0; + line-height: var(--bs-btn-line-height); + text-indent: calc(0.5 * var(--bs-spacer)) } &:hover, &:focus { @@ -37,7 +37,7 @@ .cdk-drag-preview { background-color: white; - border-radius: $border-radius-sm; + border-radius: var(--bs-border-radius-sm); margin-left: 0; box-shadow: 0 5px 5px 0px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss index e1ba2442e55..dee03647de4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss @@ -5,7 +5,7 @@ :host ::ng-deep .dropdown-menu { left: 0 !important; width: 100% !important; - max-height: $dropdown-menu-max-height; + max-height: var(--ds-dropdown-menu-max-height); overflow-y: auto !important; overflow-x: hidden; } @@ -14,8 +14,8 @@ :host ::ng-deep .dropdown-item:active, :host ::ng-deep .dropdown-item:focus, :host ::ng-deep .dropdown-item:hover { - color: $dropdown-link-hover-color !important; - background-color: $dropdown-link-hover-bg !important; + color: var(--bs-dropdown-link-hover-color) !important; + background-color: var(--bs-dropdown-link-hover-bg) !important; } div { @@ -23,5 +23,5 @@ div { } .lookup-item { - border-bottom: $dropdown-border-width solid $dropdown-border-color; + border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.scss index d6ce88eed90..e4a5a64ea3f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.scss @@ -1,20 +1,20 @@ :host ::ng-deep .dropdown-menu { width: 100% !important; - max-height: $dropdown-menu-max-height; + max-height: var(--ds-dropdown-menu-max-height); overflow-y: auto !important; overflow-x: hidden; } :host ::ng-deep .dropdown-item { - border-bottom: $dropdown-border-width solid $dropdown-border-color; + border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); } :host ::ng-deep .dropdown-item.active, :host ::ng-deep .dropdown-item:active, :host ::ng-deep .dropdown-item:focus, :host ::ng-deep .dropdown-item:hover { - color: $dropdown-link-hover-color !important; - background-color: $dropdown-link-hover-bg !important; + color: var(--bs-dropdown-link-hover-color) !important; + background-color: var(--bs-dropdown-link-hover-bg) !important; } .treeview .modal-body { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss index 34c5a842208..021453c74bf 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss @@ -2,25 +2,25 @@ .scrollable-menu { height: auto; - max-height: $dropdown-menu-max-height; + max-height: var(--ds-dropdown-menu-max-height); overflow-x: hidden; } .collection-item { - border-bottom: $dropdown-border-width solid $dropdown-border-color; + border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); } .scrollable-dropdown-loading { - background-color: map-get($theme-colors, primary); + background-color: var(--bs-primary); color: white; - height: $spacer * 2 !important; - line-height: $spacer * 2; + height: calc(var(--bs-spacer) * 2) !important; + line-height: calc(var(--bs-spacer) * 2); position: sticky; bottom: 0; } .scrollable-dropdown-menu { left: 0 !important; - margin-bottom: $spacer; + margin-bottom: var(--bs-spacer); z-index: 1000; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.scss index 032596207a6..e87b8ffdb4f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.scss @@ -10,19 +10,19 @@ :host ::ng-deep .dropdown-menu { width: 100% !important; - max-height: $dropdown-menu-max-height; + max-height: var(--ds-dropdown-menu-max-height); overflow-y: scroll; overflow-x: hidden; left: 0 !important; - margin-top: $spacer !important; + margin-top: var(--bs-spacer) !important; } :host ::ng-deep .dropdown-item.active, :host ::ng-deep .dropdown-item:active, :host ::ng-deep .dropdown-item:focus, :host ::ng-deep .dropdown-item:hover { - color: $dropdown-link-hover-color !important; - background-color: $dropdown-link-hover-bg !important; + color: var(--bs-dropdown-link-hover-color) !important; + background-color: var(--bs-dropdown-link-hover-bg) !important; } .tag-input { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss index 4562a95080c..f73f8b8c34b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss @@ -1,3 +1,3 @@ .position-absolute { - right: $spacer; -} \ No newline at end of file + right: var(--bs-spacer); +} diff --git a/src/app/shared/form/form.component.scss b/src/app/shared/form/form.component.scss index 01cf09576fd..33f34fdc3c5 100644 --- a/src/app/shared/form/form.component.scss +++ b/src/app/shared/form/form.component.scss @@ -5,7 +5,7 @@ } .ds-form-input-btn { - border: $input-btn-border-width solid $input-border-color; + border: var(--bs-input-btn-border-width) solid var(--bs-input-border-color); border-top-left-radius: 0; border-bottom-left-radius: 0; border-left: 0; @@ -36,9 +36,9 @@ /* add padding */ .left-addon input { - padding-left: $spacer * 2.25; + padding-left: calc(var(--bs-spacer) * 2.25); } .right-addon input { - padding-right: $spacer * 2.25; + padding-right: calc(var(--bs-spacer) * 2.25); } diff --git a/src/app/shared/input-suggestions/input-suggestions.component.scss b/src/app/shared/input-suggestions/input-suggestions.component.scss index 28cc7e8a498..fd7f26a29cd 100644 --- a/src/app/shared/input-suggestions/input-suggestions.component.scss +++ b/src/app/shared/input-suggestions/input-suggestions.component.scss @@ -4,7 +4,7 @@ .dropdown-item { white-space: normal; word-break: break-word; - padding: $input-padding-y $input-padding-x; + padding: var(--bs-input-padding-y) var(--bs-input-padding-x); &:focus { outline: none; } diff --git a/src/app/shared/log-in/container/log-in-container.component.scss b/src/app/shared/log-in/container/log-in-container.component.scss index 0255b71dac9..f3e0ab6cf49 100644 --- a/src/app/shared/log-in/container/log-in-container.component.scss +++ b/src/app/shared/log-in/container/log-in-container.component.scss @@ -1,18 +1,18 @@ :host ::ng-deep .card { - margin-bottom: $submission-sections-margin-bottom; + margin-bottom: var(--ds-submission-sections-margin-bottom); overflow: unset; } .section-focus { - border-radius: $border-radius; - box-shadow: $btn-focus-box-shadow; + border-radius: var(--bs-border-radius); + box-shadow: var(--bs-btn-focus-box-shadow); } // TODO to remove the following when upgrading @ng-bootstrap :host ::ng-deep .card:first-of-type { - border-bottom: $card-border-width solid $card-border-color !important; - border-bottom-left-radius: $card-border-radius !important; - border-bottom-right-radius: $card-border-radius !important; + border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color) !important; + border-bottom-left-radius: var(--bs-card-border-radius) !important; + border-bottom-right-radius: var(--bs-card-border-radius) !important; } :host ::ng-deep .card-header button { diff --git a/src/app/shared/metadata-representation/metadata-representation-loader.component.ts b/src/app/shared/metadata-representation/metadata-representation-loader.component.ts index fd094f77971..40fae2e5a67 100644 --- a/src/app/shared/metadata-representation/metadata-representation-loader.component.ts +++ b/src/app/shared/metadata-representation/metadata-representation-loader.component.ts @@ -6,6 +6,7 @@ import { GenericConstructor } from '../../core/shared/generic-constructor'; import { MetadataRepresentationListElementComponent } from '../object-list/metadata-representation-list-element/metadata-representation-list-element.component'; import { MetadataRepresentationDirective } from './metadata-representation.directive'; import { hasValue } from '../empty.util'; +import { ThemeService } from '../theme-support/theme.service'; @Component({ selector: 'ds-metadata-representation-loader', @@ -42,7 +43,10 @@ export class MetadataRepresentationLoaderComponent implements OnInit { */ @ViewChild(MetadataRepresentationDirective, {static: true}) mdRepDirective: MetadataRepresentationDirective; - constructor(private componentFactoryResolver: ComponentFactoryResolver) { + constructor( + private componentFactoryResolver: ComponentFactoryResolver, + private themeService: ThemeService + ) { } /** @@ -64,6 +68,6 @@ export class MetadataRepresentationLoaderComponent implements OnInit { * @returns {string} */ private getComponent(): GenericConstructor { - return getMetadataRepresentationComponent(this.mdRepresentation.itemType, this.mdRepresentation.representationType, this.context); + return getMetadataRepresentationComponent(this.mdRepresentation.itemType, this.mdRepresentation.representationType, this.context, this.themeService.getThemeName()); } } diff --git a/src/app/shared/metadata-representation/metadata-representation.decorator.ts b/src/app/shared/metadata-representation/metadata-representation.decorator.ts index a74c75385d3..30bb507b49a 100644 --- a/src/app/shared/metadata-representation/metadata-representation.decorator.ts +++ b/src/app/shared/metadata-representation/metadata-representation.decorator.ts @@ -6,15 +6,17 @@ export const map = new Map(); export const DEFAULT_ENTITY_TYPE = 'Publication'; export const DEFAULT_REPRESENTATION_TYPE = MetadataRepresentationType.PlainText; -export const DEFAULT_CONTEXT = Context.Undefined; +export const DEFAULT_CONTEXT = Context.Any; +export const DEFAULT_THEME = '*'; /** * Decorator function to store metadata representation mapping * @param entityType The entity type the component represents * @param mdRepresentationType The metadata representation type the component represents * @param context The optional context the component represents + * @param theme The optional theme for the component */ -export function metadataRepresentationComponent(entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context = DEFAULT_CONTEXT) { +export function metadataRepresentationComponent(entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) { return function decorator(component: any) { if (hasNoValue(map.get(entityType))) { map.set(entityType, new Map()); @@ -23,10 +25,14 @@ export function metadataRepresentationComponent(entityType: string, mdRepresenta map.get(entityType).set(mdRepresentationType, new Map()); } - if (hasValue(map.get(entityType).get(mdRepresentationType).get(context))) { + if (hasNoValue(map.get(entityType).get(mdRepresentationType).get(context))) { + map.get(entityType).get(mdRepresentationType).set(context, new Map()); + } + + if (hasValue(map.get(entityType).get(mdRepresentationType).get(context).get(theme))) { throw new Error(`There can't be more than one component to render Entity of type "${entityType}" in MetadataRepresentation "${mdRepresentationType}" with context "${context}"`); } - map.get(entityType).get(mdRepresentationType).set(context, component); + map.get(entityType).get(mdRepresentationType).get(context).set(theme, component); }; } @@ -35,22 +41,32 @@ export function metadataRepresentationComponent(entityType: string, mdRepresenta * @param entityType The entity type to match * @param mdRepresentationType The metadata representation to match * @param context The context to match + * @param theme the theme to match */ -export function getMetadataRepresentationComponent(entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context = DEFAULT_CONTEXT) { +export function getMetadataRepresentationComponent(entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) { const mapForEntity = map.get(entityType); if (hasValue(mapForEntity)) { const entityAndMDRepMap = mapForEntity.get(mdRepresentationType); if (hasValue(entityAndMDRepMap)) { - if (hasValue(entityAndMDRepMap.get(context))) { - return entityAndMDRepMap.get(context); + const contextMap = entityAndMDRepMap.get(context); + if (hasValue(contextMap)) { + if (hasValue(contextMap.get(theme))) { + return contextMap.get(theme); + } + if (hasValue(contextMap.get(DEFAULT_THEME))) { + return contextMap.get(DEFAULT_THEME); + } } - if (hasValue(entityAndMDRepMap.get(DEFAULT_CONTEXT))) { - return entityAndMDRepMap.get(DEFAULT_CONTEXT); + if (hasValue(entityAndMDRepMap.get(DEFAULT_CONTEXT)) && + hasValue(entityAndMDRepMap.get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) { + return entityAndMDRepMap.get(DEFAULT_CONTEXT).get(DEFAULT_THEME); } } - if (hasValue(mapForEntity.get(DEFAULT_REPRESENTATION_TYPE))) { - return mapForEntity.get(DEFAULT_REPRESENTATION_TYPE).get(DEFAULT_CONTEXT); + if (hasValue(mapForEntity.get(DEFAULT_REPRESENTATION_TYPE)) && + hasValue(mapForEntity.get(DEFAULT_REPRESENTATION_TYPE).get(DEFAULT_CONTEXT)) && + hasValue(mapForEntity.get(DEFAULT_REPRESENTATION_TYPE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) { + return mapForEntity.get(DEFAULT_REPRESENTATION_TYPE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME); } } - return map.get(DEFAULT_ENTITY_TYPE).get(DEFAULT_REPRESENTATION_TYPE).get(DEFAULT_CONTEXT); + return map.get(DEFAULT_ENTITY_TYPE).get(DEFAULT_REPRESENTATION_TYPE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME); } diff --git a/src/app/shared/mocks/theme-service.mock.ts b/src/app/shared/mocks/theme-service.mock.ts new file mode 100644 index 00000000000..fd54e80ad2f --- /dev/null +++ b/src/app/shared/mocks/theme-service.mock.ts @@ -0,0 +1,7 @@ +import { ThemeService } from '../theme-support/theme.service'; + +export function getMockThemeService(): ThemeService { + return jasmine.createSpyObj('themeService', { + getThemeName: 'base' + }); +} diff --git a/src/app/shared/ngrx/no-op.action.ts b/src/app/shared/ngrx/no-op.action.ts new file mode 100644 index 00000000000..cd05c9c05fc --- /dev/null +++ b/src/app/shared/ngrx/no-op.action.ts @@ -0,0 +1,14 @@ +import { Action } from '@ngrx/store'; +import { type } from './type'; + +export const NO_OP_ACTION_TYPE = type('dspace/ngrx/NO_OP_ACTION'); + +/** + * An action to use when nothing needs to happen, but you're forced to dispatch an action anyway. + * e.g. an effect that needs to do something if a certain check succeeds, and nothing otherwise. + * + * It should not be used in any reducer or listened for in any effect. + */ +export class NoOpAction implements Action { + public readonly type = NO_OP_ACTION_TYPE; +} diff --git a/src/app/shared/notifications/notification/notification.component.scss b/src/app/shared/notifications/notification/notification.component.scss index a5ebb72b0b4..0321644585f 100644 --- a/src/app/shared/notifications/notification/notification.component.scss +++ b/src/app/shared/notifications/notification/notification.component.scss @@ -1,6 +1,6 @@ .alert { display: inline-block; - min-width: $modal-sm; + min-width: var(--bs-modal-sm); text-align: left; } @@ -19,14 +19,14 @@ } .alert-success .notification-progress-loader span { - background: darken(adjust-hue(map-get($theme-colors, success), -10), 10%); + background: var(--ds-notification-bg-success); } .alert-danger .notification-progress-loader span { - background: darken(adjust-hue(map-get($theme-colors, danger), -10), 10%); + background: var(--ds-notification-bg-danger); } .alert-info .notification-progress-loader span { - background: darken(adjust-hue(map-get($theme-colors, info), -10), 10%); + background: var(--ds-notification-bg-info); } .alert-warning .notification-progress-loader span { - background: darken(adjust-hue(map-get($theme-colors, warning), -10), 10%); + background: var(--ds-notification-bg-warning); } diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.scss b/src/app/shared/notifications/notifications-board/notifications-board.component.scss index 1101393e598..c0b1a917f43 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.scss +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.scss @@ -1,5 +1,5 @@ .notifications-wrapper { - z-index: $zindex-popover; + z-index: var(--bs-zindex-popover); text-align: right; @include word-wrap; .notification { @@ -37,7 +37,7 @@ transform: translate(-50%, -50%); } -@media screen and (max-width: map-get($grid-breakpoints, sm)) { +@media screen and (max-width: var(--ds-grid-breakpoints-sm)) { .notifications-wrapper { width: auto; left: 0; diff --git a/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.ts b/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.ts index 5e367654a77..14a5459a985 100644 --- a/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.ts +++ b/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.ts @@ -1,4 +1,11 @@ -import { Component, ComponentFactoryResolver, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { + Component, + ComponentFactoryResolver, + ElementRef, + Input, + OnInit, + ViewChild +} from '@angular/core'; import { ListableObject } from '../listable-object.model'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { Context } from '../../../../core/shared/context.model'; @@ -7,6 +14,7 @@ import { GenericConstructor } from '../../../../core/shared/generic-constructor' import { ListableObjectDirective } from './listable-object.directive'; import { CollectionElementLinkType } from '../../collection-element-link.type'; import { hasValue } from '../../../empty.util'; +import { ThemeService } from '../../../theme-support/theme.service'; @Component({ selector: 'ds-listable-object-component-loader', @@ -83,7 +91,10 @@ export class ListableObjectComponentLoaderComponent implements OnInit { */ withdrawnBadge = false; - constructor(private componentFactoryResolver: ComponentFactoryResolver) { + constructor( + private componentFactoryResolver: ComponentFactoryResolver, + private themeService: ThemeService + ) { } /** @@ -132,6 +143,6 @@ export class ListableObjectComponentLoaderComponent implements OnInit { * @returns {GenericConstructor} */ private getComponent(): GenericConstructor { - return getListableObjectComponent(this.object.getRenderTypes(), this.viewMode, this.context); + return getListableObjectComponent(this.object.getRenderTypes(), this.viewMode, this.context, this.themeService.getThemeName()); } } diff --git a/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts b/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts index 4e4567eaff9..91140f0ea16 100644 --- a/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts +++ b/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts @@ -1,7 +1,10 @@ import { ViewMode } from '../../../../core/shared/view-mode.model'; import { Context } from '../../../../core/shared/context.model'; import { hasNoValue, hasValue } from '../../../empty.util'; -import { DEFAULT_CONTEXT } from '../../../metadata-representation/metadata-representation.decorator'; +import { + DEFAULT_CONTEXT, + DEFAULT_THEME +} from '../../../metadata-representation/metadata-representation.decorator'; import { GenericConstructor } from '../../../../core/shared/generic-constructor'; import { ListableObject } from '../listable-object.model'; @@ -11,11 +14,12 @@ const map = new Map(); /** * Decorator used for rendering a listable object - * @param type The object type or entity type the component represents + * @param objectType The object type or entity type the component represents * @param viewMode The view mode the component represents * @param context The optional context the component represents + * @param theme The optional theme for the component */ -export function listableObjectComponent(objectType: string | GenericConstructor, viewMode: ViewMode, context: Context = DEFAULT_CONTEXT) { +export function listableObjectComponent(objectType: string | GenericConstructor, viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) { return function decorator(component: any) { if (hasNoValue(objectType)) { return; @@ -26,7 +30,10 @@ export function listableObjectComponent(objectType: string | GenericConstructor< if (hasNoValue(map.get(objectType).get(viewMode))) { map.get(objectType).set(viewMode, new Map()); } - map.get(objectType).get(viewMode).set(context, component); + if (hasNoValue(map.get(objectType).get(viewMode).get(context))) { + map.get(objectType).get(viewMode).set(context, new Map()); + } + map.get(objectType).get(viewMode).get(context).set(theme, component); }; } @@ -35,8 +42,9 @@ export function listableObjectComponent(objectType: string | GenericConstructor< * @param types The types of which one should match the listable component * @param viewMode The view mode that should match the components * @param context The context that should match the components + * @param theme The theme that should match the components */ -export function getListableObjectComponent(types: (string | GenericConstructor)[], viewMode: ViewMode, context: Context = DEFAULT_CONTEXT) { +export function getListableObjectComponent(types: (string | GenericConstructor)[], viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme: string = DEFAULT_THEME) { let bestMatch; let bestMatchValue = 0; for (const type of types) { @@ -44,17 +52,29 @@ export function getListableObjectComponent(types: (string | GenericConstructor img { - height: $card-thumbnail-height; + height: var(--ds-card-thumbnail-height); width: 100%; } } diff --git a/src/app/shared/object-grid/object-grid.component.scss b/src/app/shared/object-grid/object-grid.component.scss index 53d64bc60c0..46675615f0c 100644 --- a/src/app/shared/object-grid/object-grid.component.scss +++ b/src/app/shared/object-grid/object-grid.component.scss @@ -1,8 +1,8 @@ -$ds-wrapper-grid-spacing: $spacer/2; - :host ::ng-deep { + --ds-wrapper-grid-spacing: calc(var(--bs-spacer) / 2); + div.thumbnail > img { - height: $card-thumbnail-height; + height: var(--ds-card-thumbnail-height); width: 100%; display: block; min-width: 100%; @@ -11,19 +11,19 @@ $ds-wrapper-grid-spacing: $spacer/2; object-position: 50% 15%; } div.card { - margin-top: $ds-wrapper-grid-spacing; - margin-bottom: $ds-wrapper-grid-spacing; + margin-top: var(--ds-wrapper-grid-spacing); + margin-bottom: var(--ds-wrapper-grid-spacing); } } .card-columns { - margin-left: -$ds-wrapper-grid-spacing; - margin-right: -$ds-wrapper-grid-spacing; + margin-left: calc(-1 * var(--ds-wrapper-grid-spacing)); + margin-right: calc(-1 * var(--ds-wrapper-grid-spacing)); column-gap: 0; .card-column { - padding-left: $ds-wrapper-grid-spacing; - padding-right: $ds-wrapper-grid-spacing; + padding-left: var(--ds-wrapper-grid-spacing); + padding-right: var(--ds-wrapper-grid-spacing); } } diff --git a/src/app/shared/page-size-selector/page-size-selector.component.scss b/src/app/shared/page-size-selector/page-size-selector.component.scss index cd18456888b..b1a3c083efd 100644 --- a/src/app/shared/page-size-selector/page-size-selector.component.scss +++ b/src/app/shared/page-size-selector/page-size-selector.component.scss @@ -1,3 +1,3 @@ .setting-option { - border: 1px solid map-get($theme-colors, light); + border: 1px solid var(--bs-light); } diff --git a/src/app/shared/search-form/search-form.component.scss b/src/app/shared/search-form/search-form.component.scss index 64b97aebd8c..4576be4b288 100644 --- a/src/app/shared/search-form/search-form.component.scss +++ b/src/app/shared/search-form/search-form.component.scss @@ -1,5 +1,5 @@ // temporary fix for bootstrap 4 beta btn color issue .btn-secondary { - background-color: $input-bg; - color: $input-color; + background-color: var(--bs-input-bg); + color: var(--bs-input-color); } diff --git a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.scss b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.scss index 4bf21aa33f3..8f61f5ebf12 100644 --- a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.scss @@ -1,6 +1,6 @@ .filters { a { - color: $body-color; + color: var(--bs-body-color); &:hover, &focus { text-decoration: none; } @@ -9,7 +9,7 @@ } } .toggle-more-filters a { - color: $link-color; + color: var(--bs-link-color); text-decoration: underline; cursor: pointer; } diff --git a/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.scss b/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.scss index 9bfd380441c..005b9b25814 100644 --- a/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.scss @@ -1,6 +1,6 @@ .filters { .toggle-more-filters a { - color: $link-color; + color: var(--bs-link-color); text-decoration: underline; cursor: pointer; } diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.scss b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.scss index c5a38f24a72..74cede54a86 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.scss @@ -1,9 +1,9 @@ a { - color: $body-color; + color: var(--bs-body-color); &:hover, &focus { text-decoration: none; } span.badge { vertical-align: text-top; } -} \ No newline at end of file +} diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.scss b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.scss index 93d85fcba8f..1e151b14ce1 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.scss @@ -1,11 +1,11 @@ a { - color: $link-color; + color: var(--bs-link-color); &:hover { text-decoration: underline; - color: $link-hover-color; + color: var(--bs-link-hover-color); } span.badge { vertical-align: text-top; } -} \ No newline at end of file +} diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.scss b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.scss index c5a38f24a72..74cede54a86 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.scss @@ -1,9 +1,9 @@ a { - color: $body-color; + color: var(--bs-body-color); &:hover, &focus { text-decoration: none; } span.badge { vertical-align: text-top; } -} \ No newline at end of file +} diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.scss b/src/app/shared/search/search-filters/search-filter/search-filter.component.scss index c94edb01bba..518e7c9d5f9 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.scss @@ -1,10 +1,10 @@ :host .facet-filter { - border: 1px solid map-get($theme-colors, light); + border: 1px solid var(--bs-light); cursor: pointer; .search-filter-wrapper.closed { overflow: hidden; } .filter-toggle { - line-height: $line-height-base; + line-height: var(--bs-line-height-base); } } diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.scss b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.scss index 489b7bab639..4e2df779a2c 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.scss @@ -1,6 +1,6 @@ .filters { .toggle-more-filters a { - color: $link-color; + color: var(--bs-link-color); text-decoration: underline; cursor: pointer; } diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss index 9e536626b04..71d19dbdf86 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss @@ -1,27 +1,28 @@ .filters { .toggle-more-filters a { - color: $link-color; + color: var(--bs-link-color); text-decoration: underline; cursor: pointer; } } -$slider-handle-width: 18px; ::ng-deep { + --ds-slider-handle-width: 18px; + html:not([dir=rtl]) .noUi-horizontal .noUi-handle { - right: -$slider-handle-width/2; + right: calc(var(--ds-slider-handle-width) / -2); } .noUi-horizontal .noUi-handle { - width: $slider-handle-width; + width: var(--ds-slider-handle-width); &:before { - left: ($slider-handle-width - 2)/2 - 2; + left: calc(calc(calc(var(--ds-slider-handle-width) - 2) / 2) - 2); } &:after { - left: ($slider-handle-width - 2)/2 + 2; + left: calc(calc(calc(var(--ds-slider-handle-width) - 2) / 2) + 2); } &:focus { outline: none; } } -} \ No newline at end of file +} diff --git a/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.scss b/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.scss index 1d062960c72..815ff356422 100644 --- a/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.scss @@ -1,7 +1,7 @@ .filters { .toggle-more-filters a { - color: $link-color; + color: var(--bs-link-color); text-decoration: underline; cursor: pointer; } diff --git a/src/app/shared/search/search-settings/search-settings.component.scss b/src/app/shared/search/search-settings/search-settings.component.scss index cd18456888b..b1a3c083efd 100644 --- a/src/app/shared/search/search-settings/search-settings.component.scss +++ b/src/app/shared/search/search-settings/search-settings.component.scss @@ -1,3 +1,3 @@ .setting-option { - border: 1px solid map-get($theme-colors, light); + border: 1px solid var(--bs-light); } diff --git a/src/app/shared/search/search-sidebar/search-sidebar.component.scss b/src/app/shared/search/search-sidebar/search-sidebar.component.scss index 38e742bca41..130ec91aec6 100644 --- a/src/app/shared/search/search-sidebar/search-sidebar.component.scss +++ b/src/app/shared/search/search-sidebar/search-sidebar.component.scss @@ -1,16 +1,16 @@ :host { .results { - line-height: $button-height; + line-height: var(--ds-button-height); } ds-view-mode-switch { - margin-bottom: $spacer; + margin-bottom: var(--bs-spacer); } .sidebar-content > *:not(:last-child):not(ds-search-switch-configuration) { - margin-bottom: 4*$spacer; + margin-bottom: calc(4 * var(--bs-spacer)); display: block; } ds-search-switch-configuration { - margin-bottom: 2*$spacer; + margin-bottom: calc(2 * var(--bs-spacer)); display: block; } } diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss index b4e9cd340cd..30ab8912d32 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss +++ b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss @@ -1,5 +1,5 @@ a { - color: $body-color; + color: var(--bs-body-color); &:hover, &focus { text-decoration: none; diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.scss b/src/app/shared/sidebar/filter/sidebar-filter.component.scss index 68949f34503..bf7a089cb10 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.component.scss +++ b/src/app/shared/sidebar/filter/sidebar-filter.component.scss @@ -1,5 +1,5 @@ :host .facet-filter { - border: 1px solid map-get($theme-colors, light); + border: 1px solid var(--bs-light); cursor: pointer; .sidebar-filter-wrapper.closed { @@ -7,6 +7,6 @@ } .filter-toggle { - line-height: $line-height-base; + line-height: var(--bs-line-height-base); } } diff --git a/src/app/shared/sidebar/page-with-sidebar.component.scss b/src/app/shared/sidebar/page-with-sidebar.component.scss index 8be48cea2be..4949694313f 100644 --- a/src/app/shared/sidebar/page-with-sidebar.component.scss +++ b/src/app/shared/sidebar/page-with-sidebar.component.scss @@ -43,9 +43,9 @@ position: sticky; position: -webkit-sticky; top: 0; - z-index: $zindex-sticky; - padding-top: $content-spacing; - margin-top: -$content-spacing; + z-index: var(--bs-zindex-sticky); + padding-top: var(--ds-content-spacing); + margin-top: calc(-1 * var(--ds-content-spacing)); align-self: flex-start; display: block; } diff --git a/src/app/shared/sidebar/sidebar-dropdown.component.scss b/src/app/shared/sidebar/sidebar-dropdown.component.scss index 1c025095ddb..2da7be11b5d 100644 --- a/src/app/shared/sidebar/sidebar-dropdown.component.scss +++ b/src/app/shared/sidebar/sidebar-dropdown.component.scss @@ -1,3 +1,3 @@ .setting-option { - border: 1px solid map-get($theme-colors, light); + border: 1px solid var(--bs-light); } diff --git a/src/app/shared/starts-with/date/starts-with-date.component.scss b/src/app/shared/starts-with/date/starts-with-date.component.scss index 64b97aebd8c..4576be4b288 100644 --- a/src/app/shared/starts-with/date/starts-with-date.component.scss +++ b/src/app/shared/starts-with/date/starts-with-date.component.scss @@ -1,5 +1,5 @@ // temporary fix for bootstrap 4 beta btn color issue .btn-secondary { - background-color: $input-bg; - color: $input-color; + background-color: var(--bs-input-bg); + color: var(--bs-input-color); } diff --git a/src/app/shared/starts-with/text/starts-with-text.component.scss b/src/app/shared/starts-with/text/starts-with-text.component.scss index 64b97aebd8c..4576be4b288 100644 --- a/src/app/shared/starts-with/text/starts-with-text.component.scss +++ b/src/app/shared/starts-with/text/starts-with-text.component.scss @@ -1,5 +1,5 @@ // temporary fix for bootstrap 4 beta btn color issue .btn-secondary { - background-color: $input-bg; - color: $input-color; + background-color: var(--bs-input-bg); + color: var(--bs-input-color); } diff --git a/src/app/shared/theme-support/theme.actions.ts b/src/app/shared/theme-support/theme.actions.ts new file mode 100644 index 00000000000..dbc885772de --- /dev/null +++ b/src/app/shared/theme-support/theme.actions.ts @@ -0,0 +1,22 @@ +import { Action } from '@ngrx/store'; +import { type } from '../ngrx/type'; + +export const ThemeActionTypes = { + SET: type('dspace/theme/SET'), +}; + +/** + * An action to set the current theme + */ +export class SetThemeAction implements Action { + type = ThemeActionTypes.SET; + payload: { + name: string + }; + + constructor(name: string) { + this.payload = { name }; + } +} + +export type ThemeAction = SetThemeAction; diff --git a/src/app/shared/theme-support/theme.constants.ts b/src/app/shared/theme-support/theme.constants.ts new file mode 100644 index 00000000000..2304461d928 --- /dev/null +++ b/src/app/shared/theme-support/theme.constants.ts @@ -0,0 +1 @@ +export const BASE_THEME_NAME = 'base'; diff --git a/src/app/shared/theme-support/theme.effects.spec.ts b/src/app/shared/theme-support/theme.effects.spec.ts new file mode 100644 index 00000000000..94d16459121 --- /dev/null +++ b/src/app/shared/theme-support/theme.effects.spec.ts @@ -0,0 +1,173 @@ +import { ThemeEffects } from './theme.effects'; +import { Observable } from 'rxjs'; +import { TestBed } from '@angular/core/testing'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { LinkService } from '../../core/cache/builders/link.service'; +import { getMockLinkService } from '../mocks/link-service.mock'; +import { cold, hot } from 'jasmine-marbles'; +import { ROOT_EFFECTS_INIT } from '@ngrx/effects'; +import { SetThemeAction } from './theme.actions'; +import { Theme } from '../../../config/theme.model'; +import { provideMockStore } from '@ngrx/store/testing'; +import { ROUTER_NAVIGATED } from '@ngrx/router-store'; +import { ResolverActionTypes } from '../../core/resolving/resolver.actions'; +import { Community } from '../../core/shared/community.model'; +import { COMMUNITY } from '../../core/shared/community.resource-type'; +import { NoOpAction } from '../ngrx/no-op.action'; +import { ITEM } from '../../core/shared/item.resource-type'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { Item } from '../../core/shared/item.model'; +import { Collection } from '../../core/shared/collection.model'; +import { COLLECTION } from '../../core/shared/collection.resource-type'; +import { BASE_THEME_NAME } from './theme.constants'; + +describe('ThemeEffects', () => { + let themeEffects: ThemeEffects; + let linkService: LinkService; + let initialState; + let actions: Observable; + + function init() { + linkService = getMockLinkService(); + initialState = { + currentTheme: 'custom', + }; + } + + beforeEach(() => { + init(); + TestBed.configureTestingModule({ + providers: [ + ThemeEffects, + { provide: LinkService, useValue: linkService }, + provideMockStore({ initialState }), + provideMockActions(() => actions) + ] + }); + + themeEffects = TestBed.inject(ThemeEffects); + }); + + describe('initTheme$', () => { + it('should set the default theme', () => { + actions = hot('--a-', { + a: { + type: ROOT_EFFECTS_INIT + } + }); + + const expected = cold('--b-', { + b: new SetThemeAction(BASE_THEME_NAME) + }); + + expect(themeEffects.initTheme$).toBeObservable(expected); + }); + }); + + // TODO: Fix test + xdescribe('updateThemeOnRouteChange$', () => { + it('test', () => { + const url = '/test/route'; + const dso = Object.assign(new Community(), { + type: COMMUNITY.value, + uuid: '0958c910-2037-42a9-81c7-dca80e3892b4', + }); + actions = hot('--ab-', { + a: { + type: ROUTER_NAVIGATED, + payload: { routerState: { url } }, + }, + b: { + type: ResolverActionTypes.RESOLVED, + payload: { url, dso }, + } + }); + + const expected = cold('--b-', { + b: new SetThemeAction('Publications community (uuid)') + }); + + expect(themeEffects.initTheme$).toBeObservable(expected); + }); + }); + + describe('getActionForMatch', () => { + it('should return a SET action if the name doesn\'t match', () => { + const theme = new Theme({ name: 'new-theme' }); + expect((themeEffects as any).getActionForMatch(theme, 'not-matching')).toEqual(new SetThemeAction('new-theme')); + }); + + it('should return an empty action if the name matches', () => { + const theme = new Theme({ name: 'new-theme' }); + expect((themeEffects as any).getActionForMatch(theme, 'new-theme')).toEqual(new NoOpAction()); + }); + }); + + describe('matchThemeToDSOs', () => { + let themes: Theme[]; + let nonMatchingTheme: Theme; + let itemMatchingTheme: Theme; + let communityMatchingTheme: Theme; + let dsos: DSpaceObject[]; + + beforeEach(() => { + nonMatchingTheme = Object.assign(new Theme({ name: 'non-matching-theme' }), { + matches: () => false + }); + itemMatchingTheme = Object.assign(new Theme({ name: 'item-matching-theme' }), { + matches: (url, dso) => (dso as any).type === ITEM.value + }); + communityMatchingTheme = Object.assign(new Theme({ name: 'community-matching-theme' }), { + matches: (url, dso) => (dso as any).type === COMMUNITY.value + }); + dsos = [ + Object.assign(new Item(), { + type: ITEM.value, + uuid: 'item-uuid', + }), + Object.assign(new Collection(), { + type: COLLECTION.value, + uuid: 'collection-uuid', + }), + Object.assign(new Community(), { + type: COMMUNITY.value, + uuid: 'community-uuid', + }), + ]; + }); + + describe('when no themes match any of the DSOs', () => { + beforeEach(() => { + themes = [ nonMatchingTheme ]; + themeEffects.themes = themes; + }); + + it('should return undefined', () => { + expect((themeEffects as any).matchThemeToDSOs(dsos, '')).toBeUndefined(); + }); + }); + + describe('when one of the themes match a DSOs', () => { + beforeEach(() => { + themes = [ nonMatchingTheme, itemMatchingTheme ]; + themeEffects.themes = themes; + }); + + it('should return the matching theme', () => { + expect((themeEffects as any).matchThemeToDSOs(dsos, '')).toEqual(itemMatchingTheme); + }); + }); + + describe('when multiple themes match some of the DSOs', () => { + it('should return the first matching theme', () => { + themes = [ nonMatchingTheme, itemMatchingTheme, communityMatchingTheme ]; + themeEffects.themes = themes; + expect((themeEffects as any).matchThemeToDSOs(dsos, '')).toEqual(itemMatchingTheme); + + themes = [ nonMatchingTheme, communityMatchingTheme, itemMatchingTheme ]; + themeEffects.themes = themes; + expect((themeEffects as any).matchThemeToDSOs(dsos, '')).toEqual(communityMatchingTheme); + }); + }); + }); +}); diff --git a/src/app/shared/theme-support/theme.effects.ts b/src/app/shared/theme-support/theme.effects.ts new file mode 100644 index 00000000000..894cfeca750 --- /dev/null +++ b/src/app/shared/theme-support/theme.effects.ts @@ -0,0 +1,185 @@ +import { Injectable } from '@angular/core'; +import { createEffect, Actions, ofType, ROOT_EFFECTS_INIT } from '@ngrx/effects'; +import { ROUTER_NAVIGATED, RouterNavigatedAction } from '@ngrx/router-store'; +import { map, withLatestFrom, expand, switchMap, toArray, startWith, filter } from 'rxjs/operators'; +import { SetThemeAction } from './theme.actions'; +import { environment } from '../../../environments/environment'; +import { ThemeConfig, themeFactory, Theme, } from '../../../config/theme.model'; +import { hasValue, isNotEmpty, hasNoValue } from '../empty.util'; +import { NoOpAction } from '../ngrx/no-op.action'; +import { Store, select } from '@ngrx/store'; +import { ThemeState } from './theme.reducer'; +import { currentThemeSelector } from './theme.service'; +import { of as observableOf, EMPTY, Observable } from 'rxjs'; +import { ResolverActionTypes, ResolvedAction } from '../../core/resolving/resolver.actions'; +import { followLink } from '../utils/follow-link-config.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { LinkService } from '../../core/cache/builders/link.service'; +import { BASE_THEME_NAME } from './theme.constants'; + +export const DEFAULT_THEME_CONFIG = environment.themes.find((themeConfig: any) => + hasNoValue(themeConfig.regex) && + hasNoValue(themeConfig.handle) && + hasNoValue(themeConfig.uuid) +); + +@Injectable() +export class ThemeEffects { + /** + * The list of configured themes + */ + themes: Theme[]; + + /** + * True if at least one theme depends on the route + */ + hasDynamicTheme: boolean; + + /** + * Initialize with a theme that doesn't depend on the route. + */ + initTheme$ = createEffect(() => + this.actions$.pipe( + ofType(ROOT_EFFECTS_INIT), + map(() => { + if (hasValue(DEFAULT_THEME_CONFIG)) { + return new SetThemeAction(DEFAULT_THEME_CONFIG.name); + } else { + return new SetThemeAction(BASE_THEME_NAME); + } + }) + ) + ); + + /** + * An effect that fires when a route change completes, + * and determines whether or not the theme should change + */ + updateThemeOnRouteChange$ = createEffect(() => this.actions$.pipe( + // Listen for when a route change ends + ofType(ROUTER_NAVIGATED), + withLatestFrom( + // Pull in the latest resolved action, or undefined if none was dispatched yet + this.actions$.pipe(ofType(ResolverActionTypes.RESOLVED), startWith(undefined)), + // and the current theme from the store + this.store.pipe(select(currentThemeSelector)) + ), + switchMap(([navigatedAction, resolvedAction, currentTheme]: [RouterNavigatedAction, ResolvedAction, string]) => { + if (this.hasDynamicTheme === true && isNotEmpty(this.themes)) { + const currentRouteUrl = navigatedAction.payload.routerState.url; + // If resolvedAction exists, and deals with the current url + if (hasValue(resolvedAction) && resolvedAction.payload.url === currentRouteUrl) { + // Start with the resolved dso and go recursively through its parents until you reach the top-level community + return observableOf(resolvedAction.payload.dso).pipe( + this.getAncestorDSOs(), + map((dsos: DSpaceObject[]) => { + const dsoMatch = this.matchThemeToDSOs(dsos, currentRouteUrl); + return this.getActionForMatch(dsoMatch, currentTheme); + }) + ); + } + + // check whether the route itself matches + const routeMatch = this.themes.find((theme: Theme) => theme.matches(currentRouteUrl, undefined)); + + return [this.getActionForMatch(routeMatch, currentTheme)]; + } + + // If there are no themes configured, do nothing + return [new NoOpAction()]; + }) + ) + ); + + /** + * return the action to dispatch based on the given matching theme + * + * @param newTheme The theme to create an action for + * @param currentThemeName The name of the currently active theme + * @private + */ + private getActionForMatch(newTheme: Theme, currentThemeName: string): SetThemeAction | NoOpAction { + if (hasValue(newTheme) && newTheme.config.name !== currentThemeName) { + // If we have a match, and it isn't already the active theme, set it as the new theme + return new SetThemeAction(newTheme.config.name); + } else { + // Otherwise, do nothing + return new NoOpAction(); + } + } + + /** + * Check the given DSpaceObjects in order to see if they match the configured themes in order. + * If a match is found, the matching theme is returned + * + * @param dsos The DSpaceObjects to check + * @param currentRouteUrl The url for the current route + * @private + */ + private matchThemeToDSOs(dsos: DSpaceObject[], currentRouteUrl: string): Theme { + // iterate over the themes in order, and return the first one that matches + return this.themes.find((theme: Theme) => { + // iterate over the dsos's in order (most specific one first, so Item, Collection, + // Community), and return the first one that matches the current theme + const match = dsos.find((dso: DSpaceObject) => theme.matches(currentRouteUrl, dso)); + return hasValue(match); + }); + + } + + /** + * An rxjs operator that will return an array of all the ancestors of the DSpaceObject used as + * input. The initial DSpaceObject will be the first element of the output array, followed by + * its parent, its grandparent etc + * + * @private + */ + private getAncestorDSOs() { + return (source: Observable): Observable => + source.pipe( + expand((dso: DSpaceObject) => { + // Check if the dso exists and has a parent link + if (hasValue(dso) && typeof (dso as any).getParentLinkKey === 'function') { + const linkName = (dso as any).getParentLinkKey(); + // If it does, retrieve it. + return this.linkService.resolveLinkWithoutAttaching(dso, followLink(linkName)).pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + if (hasValue(rd.payload)) { + // If there's a parent, use it for the next iteration + return rd.payload; + } else { + // If there's no parent, or an error, return null, which will stop recursion + // in the next iteration + return null; + } + }), + ); + } + + // The current dso has no value, or no parent. Return EMPTY to stop recursion + return EMPTY; + }), + // only allow through DSOs that have a value + filter((dso: DSpaceObject) => hasValue(dso)), + // Wait for recursion to complete, and emit all results at once, in an array + toArray() + ); + } + + constructor( + private actions$: Actions, + private store: Store, + private linkService: LinkService, + ) { + // Create objects from the theme configs in the environment file + this.themes = environment.themes.map((themeConfig: ThemeConfig) => themeFactory(themeConfig)); + this.hasDynamicTheme = environment.themes.some((themeConfig: any) => + hasValue(themeConfig.regex) || + hasValue(themeConfig.handle) || + hasValue(themeConfig.uuid) + ); + } +} diff --git a/src/app/shared/theme-support/theme.reducer.spec.ts b/src/app/shared/theme-support/theme.reducer.spec.ts new file mode 100644 index 00000000000..8733b88cda5 --- /dev/null +++ b/src/app/shared/theme-support/theme.reducer.spec.ts @@ -0,0 +1,18 @@ +import { SetThemeAction } from './theme.actions'; +import { themeReducer } from './theme.reducer'; + +describe('themeReducer', () => { + const testState = { + currentTheme: 'test' + }; + + it('should set the current theme in response to the SET action', () => { + const expectedState = { + currentTheme: 'newTheme' + }; + const action = new SetThemeAction('newTheme'); + const newState = themeReducer(testState, action); + + expect(newState).toEqual(expectedState); + }); +}); diff --git a/src/app/shared/theme-support/theme.reducer.ts b/src/app/shared/theme-support/theme.reducer.ts new file mode 100644 index 00000000000..26083a0cef5 --- /dev/null +++ b/src/app/shared/theme-support/theme.reducer.ts @@ -0,0 +1,22 @@ +import { ThemeAction, ThemeActionTypes } from './theme.actions'; + +export interface ThemeState { + currentTheme: string; +} + +const initialState: ThemeState = { + currentTheme: null +}; + +export function themeReducer(state: ThemeState = initialState, action: ThemeAction): ThemeState { + switch (action.type) { + case ThemeActionTypes.SET: { + return { + currentTheme: action.payload.name + }; + } + default: { + return state; + } + } +} diff --git a/src/app/shared/theme-support/theme.service.ts b/src/app/shared/theme-support/theme.service.ts new file mode 100644 index 00000000000..7b0af93e049 --- /dev/null +++ b/src/app/shared/theme-support/theme.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { Store, createFeatureSelector, createSelector, select } from '@ngrx/store'; +import { Observable } from 'rxjs/internal/Observable'; +import { ThemeState } from './theme.reducer'; +import { SetThemeAction } from './theme.actions'; +import { take } from 'rxjs/operators'; +import { hasValue } from '../empty.util'; + +export const themeStateSelector = createFeatureSelector('theme'); + +export const currentThemeSelector = createSelector( + themeStateSelector, + (state: ThemeState): string => hasValue(state) ? state.currentTheme : undefined +); + +@Injectable({ + providedIn: 'root' +}) +export class ThemeService { + constructor( + private store: Store, + ) { + } + + setTheme(newName: string) { + this.store.dispatch(new SetThemeAction(newName)); + } + + getThemeName(): string { + let currentTheme: string; + this.store.pipe( + select(currentThemeSelector), + take(1) + ).subscribe((name: string) => + currentTheme = name + ); + return currentTheme; + } + + getThemeName$(): Observable { + return this.store.pipe( + select(currentThemeSelector) + ); + } + +} diff --git a/src/app/shared/theme-support/themed.component.html b/src/app/shared/theme-support/themed.component.html new file mode 100644 index 00000000000..4256518a105 --- /dev/null +++ b/src/app/shared/theme-support/themed.component.html @@ -0,0 +1 @@ + diff --git a/src/app/shared/theme-support/themed.component.scss b/src/app/shared/theme-support/themed.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/shared/theme-support/themed.component.ts b/src/app/shared/theme-support/themed.component.ts new file mode 100644 index 00000000000..1a413272096 --- /dev/null +++ b/src/app/shared/theme-support/themed.component.ts @@ -0,0 +1,116 @@ +import { + Component, + ViewChild, + ViewContainerRef, + ComponentRef, + SimpleChanges, + OnInit, + OnDestroy, + ComponentFactoryResolver, + ChangeDetectorRef, + OnChanges +} from '@angular/core'; +import { hasValue, isNotEmpty } from '../empty.util'; +import { Subscription } from 'rxjs'; +import { ThemeService } from './theme.service'; +import { fromPromise } from 'rxjs/internal-compatibility'; +import { catchError, switchMap, map } from 'rxjs/operators'; +import { GenericConstructor } from '../../core/shared/generic-constructor'; + +@Component({ + selector: 'ds-themed', + styleUrls: ['./themed.component.scss'], + templateUrl: './themed.component.html', +}) +export abstract class ThemedComponent implements OnInit, OnDestroy, OnChanges { + @ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef; + protected compRef: ComponentRef; + + protected lazyLoadSub: Subscription; + protected themeSub: Subscription; + + protected inAndOutputNames: (keyof T & keyof this)[] = []; + + constructor( + protected resolver: ComponentFactoryResolver, + protected cdr: ChangeDetectorRef, + protected themeService: ThemeService + ) { + } + + protected abstract getComponentName(): string; + + protected abstract importThemedComponent(themeName: string): Promise; + protected abstract importUnthemedComponent(): Promise; + + ngOnChanges(changes: SimpleChanges): void { + // if an input or output has changed + if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) { + this.connectInputsAndOutputs(); + } + } + + ngOnInit(): void { + this.destroyComponentInstance(); + this.themeSub = this.themeService.getThemeName$().subscribe(() => { + this.renderComponentInstance(); + }); + } + + ngOnDestroy(): void { + [this.themeSub, this.lazyLoadSub].filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + this.destroyComponentInstance(); + } + + protected renderComponentInstance(): void { + this.destroyComponentInstance(); + + if (hasValue(this.lazyLoadSub)) { + this.lazyLoadSub.unsubscribe(); + } + + this.lazyLoadSub = + fromPromise(this.importThemedComponent(this.themeService.getThemeName())).pipe( + // if there is no themed version of the component an exception is thrown, + // catch it and return null instead + catchError(() => [null]), + switchMap((themedFile: any) => { + if (hasValue(themedFile) && hasValue(themedFile[this.getComponentName()])) { + // if the file is not null, and exports a component with the specified name, + // return that component + return [themedFile[this.getComponentName()]]; + } else { + // otherwise import and return the default component + return fromPromise(this.importUnthemedComponent()).pipe( + map((unthemedFile: any) => { + return unthemedFile[this.getComponentName()]; + }) + ); + } + }), + ).subscribe((constructor: GenericConstructor) => { + const factory = this.resolver.resolveComponentFactory(constructor); + this.compRef = this.vcr.createComponent(factory); + this.connectInputsAndOutputs(); + this.cdr.markForCheck(); + }); + } + + protected destroyComponentInstance(): void { + if (hasValue(this.compRef)) { + this.compRef.destroy(); + this.compRef = null; + } + if (hasValue(this.vcr)) { + this.vcr.clear(); + } + } + + protected connectInputsAndOutputs(): void { + if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) { + this.inAndOutputNames.forEach((name: any) => { + this.compRef.instance[name] = this[name]; + }); + } + } +} diff --git a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss index d2ddfc73834..186bb9be486 100644 --- a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss +++ b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss @@ -1,3 +1,4 @@ +//TODO switch to css variables @mixin clamp($lines, $bg, $size-factor: 1, $line-height: $line-height-base) { $height: $line-height * $font-size-base * $size-factor; &.fixedHeight { diff --git a/src/app/shared/uploader/uploader.component.scss b/src/app/shared/uploader/uploader.component.scss index 8835b87b1e5..634b91c0e5f 100644 --- a/src/app/shared/uploader/uploader.component.scss +++ b/src/app/shared/uploader/uploader.component.scss @@ -1,14 +1,14 @@ .ds-base-drop-zone { - border: 2px dashed $gray-600; + border: 2px dashed var(--bs-gray-600); } /* Default class applied to drop zones on over */ .ds-base-drop-zone-file-over { - border: 2px dashed map-get($theme-colors, primary); + border: 2px dashed var(--bs-primary); } .ds-base-drop-zone p { - min-height: $drop-zone-area-height; + min-height: var(--ds-drop-zone-area-height); } .ds-document-drop-zone { @@ -18,21 +18,21 @@ } .ds-document-drop-zone-active { - z-index: $drop-zone-area-z-index !important; + z-index: var(--ds-drop-zone-area-z-index) !important; } .ds-document-drop-zone-inner { - background-color: rgba($white, 0.7); - z-index: $drop-zone-area-inner-z-index; + background-color: rgba(255, 255, 255, 0.7); + z-index: var(--ds-drop-zone-area-inner-z-index); top: 0; left: 0; } .ds-document-drop-zone-inner-content { - border: 4px dashed map-get($theme-colors, primary); - z-index: $drop-zone-area-inner-z-index; + border: 4px dashed var(--bs-primary); + z-index: var(--ds-drop-zone-area-inner-z-index); } .ds-document-drop-zone-inner-content p { - font-size: ($font-size-lg * 2.5); + font-size: calc(var(--bs-font-size-lg) * 2.5); } diff --git a/src/app/store.actions.ts b/src/app/store.actions.ts index be36012ce5a..aaeb319107c 100644 --- a/src/app/store.actions.ts +++ b/src/app/store.actions.ts @@ -4,7 +4,7 @@ import { AppState } from './app.reducer'; export const StoreActionTypes = { REHYDRATE: type('dspace/ngrx/REHYDRATE'), - REPLAY: type('dspace/ngrx/REPLAY') + REPLAY: type('dspace/ngrx/REPLAY'), }; export class StoreAction implements Action { diff --git a/src/app/submission/form/collection/submission-form-collection.component.scss b/src/app/submission/form/collection/submission-form-collection.component.scss index deecc395103..907a111900c 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.scss +++ b/src/app/submission/form/collection/submission-form-collection.component.scss @@ -1,15 +1,15 @@ .scrollable-menu { height: auto; - max-height: $dropdown-menu-max-height; + max-height: var(--ds-dropdown-menu-max-height); overflow-x: hidden; } .collection-item { - border-bottom: $dropdown-border-width solid $dropdown-border-color; + border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); } #collectionControlsDropdownMenu { outline: 0; left: 0 !important; - box-shadow: $btn-focus-box-shadow; + box-shadow: var(--bs-btn-focus-box-shadow); } diff --git a/src/app/submission/form/section-add/submission-form-section-add.component.scss b/src/app/submission/form/section-add/submission-form-section-add.component.scss index c8fd2073d16..36e7d8e5dd5 100644 --- a/src/app/submission/form/section-add/submission-form-section-add.component.scss +++ b/src/app/submission/form/section-add/submission-form-section-add.component.scss @@ -3,5 +3,5 @@ } .sections-dropdown-menu { - z-index: $submission-header-z-index; + z-index: var(--ds-submission-header-z-index); } diff --git a/src/app/submission/form/submission-form.component.scss b/src/app/submission/form/submission-form.component.scss index 44551162cc1..56d62887647 100644 --- a/src/app/submission/form/submission-form.component.scss +++ b/src/app/submission/form/submission-form.component.scss @@ -1,8 +1,8 @@ .submission-form-header { - background-color: rgba($white, .97); - padding: ($spacer / 2) 0 ($spacer / 2) 0; + background-color: rgba(255, 255, 255, .97); + padding: calc(var(--bs-spacer) / 2) 0 calc(var(--bs-spacer) / 2) 0; top: 0; - z-index: $submission-header-z-index; + z-index: var(--ds-submission-header-z-index); } .submission-form-header-item { @@ -10,10 +10,10 @@ } .submission-form-footer { - border-radius: $card-border-radius; + border-radius: var(--bs-card-border-radius); bottom: 0; - background-color: $gray-400; - padding: $spacer / 2; - z-index: $submission-footer-z-index; + background-color: var(--bs-gray-400); + padding: calc(var(--bs-spacer) / 2); + z-index: var(--ds-submission-footer-z-index); } diff --git a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.scss b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.scss index 136bf12e0e7..97f49459c7c 100644 --- a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.scss +++ b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.scss @@ -13,15 +13,15 @@ .scrollable-menu { height: auto; - max-height: $dropdown-menu-max-height / 2; + max-height: calc(var(--ds-dropdown-menu-max-height) / 2); overflow-x: hidden; } .scrollable-dropdown-loading { - background-color: map-get($theme-colors, primary); + background-color: var(--bs-primary); color: white; - height: $spacer * 2 !important; - line-height: $spacer * 2; + height: calc(var(--bs-spacer) * 2) !important; + line-height: calc(var(--bs-spacer) * 2); position: sticky; bottom: 0; } diff --git a/src/app/submission/sections/container/section-container.component.scss b/src/app/submission/sections/container/section-container.component.scss index 0255b71dac9..f3e0ab6cf49 100644 --- a/src/app/submission/sections/container/section-container.component.scss +++ b/src/app/submission/sections/container/section-container.component.scss @@ -1,18 +1,18 @@ :host ::ng-deep .card { - margin-bottom: $submission-sections-margin-bottom; + margin-bottom: var(--ds-submission-sections-margin-bottom); overflow: unset; } .section-focus { - border-radius: $border-radius; - box-shadow: $btn-focus-box-shadow; + border-radius: var(--bs-border-radius); + box-shadow: var(--bs-btn-focus-box-shadow); } // TODO to remove the following when upgrading @ng-bootstrap :host ::ng-deep .card:first-of-type { - border-bottom: $card-border-width solid $card-border-color !important; - border-bottom-left-radius: $card-border-radius !important; - border-bottom-right-radius: $card-border-radius !important; + border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color) !important; + border-bottom-left-radius: var(--bs-card-border-radius) !important; + border-bottom-right-radius: var(--bs-card-border-radius) !important; } :host ::ng-deep .card-header button { diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.scss b/src/app/submission/sections/upload/file/section-upload-file.component.scss index 7596652dc21..256775eb66e 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.scss +++ b/src/app/submission/sections/upload/file/section-upload-file.component.scss @@ -1,6 +1,6 @@ .sticky-buttons { position: sticky; - top: $dropdown-item-padding-x * 3; - z-index: $submission-footer-z-index; - background-color: rgba($white, .97); + top: calc(var(--bs-dropdown-item-padding-x) * 3); + z-index: var(--ds-submission-footer-z-index); + background-color: rgba(255, 255, 255, .97); } diff --git a/src/config/global-config.interface.ts b/src/config/global-config.interface.ts index 5c6e56babb1..c197a1407ca 100644 --- a/src/config/global-config.interface.ts +++ b/src/config/global-config.interface.ts @@ -5,12 +5,12 @@ import { UniversalConfig } from './universal-config.interface'; import { INotificationBoardOptions } from './notifications-config.interfaces'; import { SubmissionConfig } from './submission-config.interface'; import { FormConfig } from './form-config.interfaces'; -import {LangConfig} from './lang-config.interface'; +import { LangConfig } from './lang-config.interface'; import { BrowseByConfig } from './browse-by-config.interface'; import { ItemPageConfig } from './item-page-config.interface'; import { CollectionPageConfig } from './collection-page-config.interface'; -import { Theme } from './theme.inferface'; -import {AuthConfig} from './auth-config.interfaces'; +import { ThemeConfig } from './theme.model'; +import { AuthConfig } from './auth-config.interfaces'; import { UIServerConfig } from './ui-server-config.interface'; export interface GlobalConfig extends Config { @@ -30,6 +30,6 @@ export interface GlobalConfig extends Config { browseBy: BrowseByConfig; item: ItemPageConfig; collection: CollectionPageConfig; - theme: Theme; + themes: ThemeConfig[]; rewriteDownloadUrls: boolean; } diff --git a/src/config/theme.inferface.ts b/src/config/theme.inferface.ts deleted file mode 100644 index d0cf1d77d88..00000000000 --- a/src/config/theme.inferface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Config } from './config.interface'; - -export interface Theme extends Config { - name: string; -} diff --git a/src/config/theme.model.spec.ts b/src/config/theme.model.spec.ts new file mode 100644 index 00000000000..fce61bd7188 --- /dev/null +++ b/src/config/theme.model.spec.ts @@ -0,0 +1,122 @@ +import { HandleTheme, RegExTheme, Theme, UUIDTheme } from './theme.model'; +import { getCommunityModuleRoute } from '../app/+community-page/community-page-routing-paths'; +import { Community } from '../app/core/shared/community.model'; +import { COMMUNITY } from '../app/core/shared/community.resource-type'; +import { getCollectionModuleRoute } from '../app/+collection-page/collection-page-routing-paths'; +import { COLLECTION } from '../app/core/shared/collection.resource-type'; +import { Collection } from '../app/core/shared/collection.model'; +import { Item } from '../app/core/shared/item.model'; +import { ITEM } from '../app/core/shared/item.resource-type'; +import { getItemModuleRoute } from '../app/+item-page/item-page-routing-paths'; + +describe('Theme Models', () => { + let theme: Theme; + + describe('RegExTheme', () => { + it('should return true when the regex matches the community\'s DSO route', () => { + theme = new RegExTheme({ + name: 'community', + regex: getCommunityModuleRoute() + '/.*', + }); + const dso = Object.assign(new Community(), { + type: COMMUNITY.value, + uuid: 'community-uuid', + }); + expect(theme.matches('', dso)).toEqual(true); + }); + + it('should return true when the regex matches the collection\'s DSO route', () => { + theme = new RegExTheme({ + name: 'collection', + regex: getCollectionModuleRoute() + '/.*', + }); + const dso = Object.assign(new Collection(), { + type: COLLECTION.value, + uuid: 'collection-uuid', + }); + expect(theme.matches('', dso)).toEqual(true); + }); + + it('should return true when the regex matches the item\'s DSO route', () => { + theme = new RegExTheme({ + name: 'item', + regex: getItemModuleRoute() + '/.*', + }); + const dso = Object.assign(new Item(), { + type: ITEM.value, + uuid: 'item-uuid', + }); + expect(theme.matches('', dso)).toEqual(true); + }); + + it('should return true when the regex matches the url', () => { + theme = new RegExTheme({ + name: 'url', + regex: '.*partial.*', + }); + expect(theme.matches('theme/partial/url/match', null)).toEqual(true); + }); + + it('should return false when the regex matches neither the url, nor the DSO route', () => { + theme = new RegExTheme({ + name: 'no-match', + regex: '.*no/match.*', + }); + expect(theme.matches('theme/partial/url/match', null)).toEqual(false); + }); + }); + + describe('HandleTheme', () => { + it('should return true when the DSO\'s handle matches the theme\'s handle', () => { + theme = new HandleTheme({ + name: 'matching-handle', + handle: '1234/5678', + }); + const dso = Object.assign(new Item(), { + type: ITEM.value, + uuid: 'item-uuid', + handle: '1234/5678', + }); + expect(theme.matches('', dso)).toEqual(true); + }); + + it('should return false when the handles don\'t match', () => { + theme = new HandleTheme({ + name: 'no-matching-handle', + handle: '1234/5678', + }); + const dso = Object.assign(new Item(), { + type: ITEM.value, + uuid: 'item-uuid', + handle: '1234/6789', + }); + expect(theme.matches('', dso)).toEqual(false); + }); + }); + + describe('UUIDTheme', () => { + it('should return true when the DSO\'s UUID matches the theme\'s UUID', () => { + theme = new UUIDTheme({ + name: 'matching-uuid', + uuid: 'item-uuid', + }); + const dso = Object.assign(new Item(), { + type: ITEM.value, + uuid: 'item-uuid', + }); + expect(theme.matches('', dso)).toEqual(true); + }); + + it('should return true when the UUIDs don\'t match', () => { + theme = new UUIDTheme({ + name: 'matching-uuid', + uuid: 'item-uuid', + }); + const dso = Object.assign(new Collection(), { + type: COLLECTION.value, + uuid: 'collection-uuid', + }); + expect(theme.matches('', dso)).toEqual(false); + }); + }); +}); diff --git a/src/config/theme.model.ts b/src/config/theme.model.ts new file mode 100644 index 00000000000..908589c71c6 --- /dev/null +++ b/src/config/theme.model.ts @@ -0,0 +1,92 @@ +import { Config } from './config.interface'; +import { hasValue, hasNoValue, isNotEmpty } from '../app/shared/empty.util'; +import { DSpaceObject } from '../app/core/shared/dspace-object.model'; +import { getDSORoute } from '../app/app-routing-paths'; + +// tslint:disable:max-classes-per-file +export interface NamedThemeConfig extends Config { + name: string; +} + +export interface RegExThemeConfig extends NamedThemeConfig { + regex: string; +} + +export interface HandleThemeConfig extends NamedThemeConfig { + handle: string; +} + +export interface UUIDThemeConfig extends NamedThemeConfig { + uuid: string; +} + +export class Theme { + constructor(public config: NamedThemeConfig) { + } + + matches(url: string, dso: DSpaceObject): boolean { + return true; + } +} + +export class RegExTheme extends Theme { + regex: RegExp; + + constructor(public config: RegExThemeConfig) { + super(config); + this.regex = new RegExp(this.config.regex); + } + + matches(url: string, dso: DSpaceObject): boolean { + let match; + const route = getDSORoute(dso); + + if (isNotEmpty(route)) { + match = route.match(this.regex); + } + + if (hasNoValue(match)) { + match = url.match(this.regex); + } + + return hasValue(match); + } +} + +export class HandleTheme extends Theme { + constructor(public config: HandleThemeConfig) { + super(config); + } + + matches(url: string, dso: any): boolean { + return hasValue(dso) && hasValue(dso.handle) && dso.handle.includes(this.config.handle); + } +} + +export class UUIDTheme extends Theme { + constructor(public config: UUIDThemeConfig) { + super(config); + } + + matches(url: string, dso: DSpaceObject): boolean { + return hasValue(dso) && dso.uuid === this.config.uuid; + } +} + +export const themeFactory = (config: ThemeConfig): Theme => { + if (hasValue((config as RegExThemeConfig).regex)) { + return new RegExTheme(config as RegExThemeConfig); + } else if (hasValue((config as HandleThemeConfig).handle)) { + return new HandleTheme(config as HandleThemeConfig); + } else if (hasValue((config as UUIDThemeConfig).uuid)) { + return new UUIDTheme(config as UUIDThemeConfig); + } else { + return new Theme(config as NamedThemeConfig); + } +}; + +export type ThemeConfig + = NamedThemeConfig + | RegExThemeConfig + | HandleThemeConfig + | UUIDThemeConfig; diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 4a87be38d3c..36d1a06c43f 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -2,6 +2,7 @@ import { GlobalConfig } from '../config/global-config.interface'; import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; import { BrowseByType } from '../app/+browse-by/+browse-by-switcher/browse-by-decorator'; import { RestRequestMethod } from '../app/core/data/rest-request-method'; +import { BASE_THEME_NAME } from '../app/shared/theme-support/theme.constants'; export const environment: GlobalConfig = { production: true, @@ -222,9 +223,40 @@ export const environment: GlobalConfig = { undoTimeout: 10000 // 10 seconds } }, - theme: { - name: 'default', - }, + themes: [ + // Add additional themes here. In the case where multiple themes match a route, the first one + // in this list will get priority. It is advisable to always have a theme that matches + // every route as the last one + + // { + // // A theme with a handle property will match the community, collection or item with the given + // // handle, and all collections and/or items within it + // name: 'custom', + // handle: '10673/1233' + // }, + // { + // // A theme with a regex property will match the route using a regular expression. If it + // // matches the route for a community or collection it will also apply to all collections + // // and/or items within it + // name: 'custom', + // regex: 'collections\/e8043bc2.*' + // }, + // { + // // A theme with a uuid property will match the community, collection or item with the given + // // ID, and all collections and/or items within it + // name: 'custom', + // uuid: '0958c910-2037-42a9-81c7-dca80e3892b4' + // }, + // { + // // A theme with only a name will match every route + // name: 'custom' + // }, + + { + // This theme will use the default bootstrap styling for DSpace components + name: BASE_THEME_NAME + }, + ], // Whether the UI should rewrite file download URLs to match its domain. Only necessary to enable when running UI and REST API on separate domains rewriteDownloadUrls: false, }; diff --git a/src/environments/mock-environment.ts b/src/environments/mock-environment.ts index ef3eb86cc24..116834df4e3 100644 --- a/src/environments/mock-environment.ts +++ b/src/environments/mock-environment.ts @@ -192,7 +192,36 @@ export const environment: Partial = { undoTimeout: 10000 // 10 seconds } }, - theme: { - name: 'default', - }, + themes: [ + { + // name: 'default', + name: 'full-item-page-theme', + regex: 'items/aa6c6c83-3a83-4953-95d1-2bc2e67854d2/full' + }, + { + // name: 'default', + name: 'error-theme', + regex: 'collections/aaaa.*' + }, + { + // name: 'default', + name: 'Item (handle)', + handle: '10673/1233' // Item inside publications community + }, + { + // name: 'mantis', + name: 'blue', + regex: 'collections\/e8043bc2.*' // Publications/Thesis collections + }, + { + // name: 'mantis', + name: 'Publications community (uuid)', + uuid: '0958c910-2037-42a9-81c7-dca80e3892b4' // Publications community + }, + { + // name: 'default', + name: 'base', + // name: 'blue', + }, + ], }; diff --git a/src/index.html b/src/index.html index 491a2d319ce..072938b5ef0 100644 --- a/src/index.html +++ b/src/index.html @@ -7,6 +7,7 @@ DSpace + diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index e0bd7b5ca1d..165b3729864 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -2,7 +2,6 @@ import { HttpClient, HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NoPreloading, RouterModule } from '@angular/router'; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; @@ -30,7 +29,7 @@ import { LocationToken } from '../../app/core/services/browser-hard-redirect.service'; import { LocaleService } from '../../app/core/locale/locale.service'; -import {GoogleAnalyticsService} from '../../app/statistics/google-analytics.service'; +import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; export const REQ_KEY = makeStateKey('req'); @@ -51,12 +50,12 @@ export function getRequest(transferState: TransferState): any { HttpClientModule, // forRoot ensures the providers are only created once IdlePreloadModule.forRoot(), - RouterModule.forRoot([], { - // enableTracing: true, - useHash: false, - scrollPositionRestoration: 'enabled', - preloadingStrategy: NoPreloading - }), + // RouterModule.forRoot([], { + // // enableTracing: true, + // useHash: false, + // scrollPositionRestoration: 'enabled', + // preloadingStrategy: NoPreloading + // }), StatisticsModule.forRoot(), Angulartics2RouterlessModule.forRoot(), BrowserAnimationsModule, diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index fbe7333a900..5539bab23fd 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ServerModule } from '@angular/platform-server'; -import { RouterModule } from '@angular/router'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; @@ -42,9 +41,9 @@ export function createTranslateLoader() { BrowserModule.withServerTransition({ appId: 'dspace-angular' }), - RouterModule.forRoot([], { - useHash: false - }), + // RouterModule.forRoot([], { + // useHash: false + // }), NoopAnimationsModule, DSpaceServerTransferStateModule, TranslateModule.forRoot({ diff --git a/src/styles.scss b/src/styles.scss deleted file mode 100644 index 90d4ee0072c..00000000000 --- a/src/styles.scss +++ /dev/null @@ -1 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ diff --git a/src/styles/_bootstrap_variables_mapping.scss b/src/styles/_bootstrap_variables_mapping.scss new file mode 100644 index 00000000000..5a64be7e2a5 --- /dev/null +++ b/src/styles/_bootstrap_variables_mapping.scss @@ -0,0 +1,826 @@ +// this file maps all bootstrap variables to css variables. + +:root { + // Variables + // + // Variables should follow the `$component-state-property-size` formula for + // consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs. + + // Color system + --bs-white: #{$white}; + --bs-gray-100: #{$gray-100}; + --bs-gray-200: #{$gray-200}; + --bs-gray-300: #{$gray-300}; + --bs-gray-400: #{$gray-400}; + --bs-gray-500: #{$gray-500}; + --bs-gray-600: #{$gray-600}; + --bs-gray-700: #{$gray-700}; + --bs-gray-800: #{$gray-800}; + --bs-gray-900: #{$gray-900}; + --bs-black: #{$black}; + --bs-blue: #{$blue}; + --bs-indigo: #{$indigo}; + --bs-purple: #{$purple}; + --bs-pink: #{$pink}; + --bs-red: #{$red}; + --bs-orange: #{$orange}; + --bs-yellow: #{$yellow}; + --bs-green: #{$green}; + --bs-teal: #{$teal}; + --bs-cyan: #{$cyan}; + --bs-primary: #{$primary}; + --bs-secondary: #{$secondary}; + --bs-success: #{$success}; + --bs-info: #{$info}; + --bs-warning: #{$warning}; + --bs-danger: #{$danger}; + --bs-light: #{$light}; + --bs-dark: #{$dark}; + + + // Set a specific jump point for requesting color jumps + --bs-theme-color-interval: #{$theme-color-interval}; + + // The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255. + --bs-yiq-contrasted-threshold: #{$yiq-contrasted-threshold}; + + // Customize the light and dark text colors for use in our YIQ color contrast function. + --bs-yiq-text-dark: #{$yiq-text-dark}; + --bs-yiq-text-light: #{$yiq-text-light}; + + + // Options + // + // Quickly modify global styling by enabling or disabling optional features. + --bs-enable-caret: #{$enable-caret}; + --bs-enable-rounded: #{$enable-rounded}; + --bs-enable-shadows: #{$enable-shadows}; + --bs-enable-gradients: #{$enable-gradients}; + --bs-enable-transitions: #{$enable-transitions}; + --bs-enable-prefers-reduced-motion-media-query: #{$enable-prefers-reduced-motion-media-query}; + --bs-enable-grid-classes: #{$enable-grid-classes}; + --bs-enable-pointer-cursor-for-buttons: #{$enable-pointer-cursor-for-buttons}; + --bs-enable-print-styles: #{$enable-print-styles}; + --bs-enable-responsive-font-sizes: #{$enable-responsive-font-sizes}; + --bs-enable-validation-icons: #{$enable-validation-icons}; + --bs-enable-deprecation-messages: #{$enable-deprecation-messages}; + --bs-spacer: #{$spacer}; + --bs-spacer-0: #{map-get($spacers, 0)}; + --bs-spacer-1: #{map-get($spacers, 1)}; + --bs-spacer-2: #{map-get($spacers, 2)}; + --bs-spacer-3: #{map-get($spacers, 3)}; + --bs-spacer-4: #{map-get($spacers, 4)}; + --bs-spacer-5: #{map-get($spacers, 5)}; + --bs-sizes-25: #{map-get($sizes, 25)}; + --bs-sizes-50: #{map-get($sizes, 50)}; + --bs-sizes-75: #{map-get($sizes, 75)}; + --bs-sizes-100: #{map-get($sizes, 100)}; + --bs-sizes-auto: #{map-get($sizes, auto)}; + + // Body + // + // Settings for the `` element. + --bsbody-bg: #{$body-bg}; + --bs-body-color: #{$body-color}; + + + // Links + // + // Style anchor elements. + --bs-link-color: #{$link-color}; + --bs-link-decoration: #{$link-decoration}; + --bs-link-hover-color: #{$link-hover-color}; + --bs-link-hover-decoration: #{$link-hover-decoration}; + // Darken percentage for links with `.text-*` class (e.g. `.text-success`) + --bs-emphasized-link-hover-darken-percentage: #{$emphasized-link-hover-darken-percentage}; + + // Paragraphs + // + // Style p element. + --bs-paragraph-margin-bottom: #{$paragraph-margin-bottom}; + + + // Grid breakpoints + // + // Define the minimum dimensions at which your layout will change, + // adapting to different screen sizes, for use in media queries. + + --bs-xs-min: #{map-get($grid-breakpoints, xs)}; + --bs-sm-min: #{map-get($grid-breakpoints, sm)}; + --bs-md-min: #{map-get($grid-breakpoints, md)}; + --bs-lg-min: #{map-get($grid-breakpoints, lg)}; + --bs-xl-min: #{map-get($grid-breakpoints, xl)}; + + --bs-xs-max: #{map-get($container-max-widths, xs)}; + --bs-sm-max: #{map-get($container-max-widths, sm)}; + --bs-md-max: #{map-get($container-max-widths, md)}; + --bs-lg-max: #{map-get($container-max-widths, lg)}; + --bs-xl-max: #{map-get($container-max-widths, xl)}; + + // Grid columns + // + // Set the number of columns and specify the width of the gutters. + --bs-grid-columns: #{$grid-columns}; + --bs-grid-gutter-width: #{$grid-gutter-width}; + + + // Components + // + // Define common padding and border radius sizes and more. + --bs-line-height-lg: #{$line-height-lg}; + --bs-line-height-sm: #{$line-height-sm}; + --bs-border-width: #{$border-width}; + --bs-border-color: #{$border-color}; + --bs-border-radius: #{$border-radius}; + --bs-border-radius-lg: #{$border-radius-lg}; + --bs-border-radius-sm: #{$border-radius-sm}; + --bs-rounded-pill: #{$rounded-pill}; + --bs-box-shadow-sm: #{$box-shadow-sm}; + --bs-box-shadow: #{$box-shadow}; + --bs-box-shadow-lg: #{$box-shadow-lg}; + --bs-component-active-color: #{$component-active-color}; + --bs-component-active-bg: #{$component-active-bg}; + --bs-caret-width: #{$caret-width}; + --bs-caret-vertical-align: #{$caret-vertical-align}; + --bs-caret-spacing: #{$caret-spacing}; + --bs-transition-base: #{$transition-base}; + --bs-transition-fade: #{$transition-fade}; + --bs-transition-collapse: #{$transition-collapse}; + + --bs-font-family-sans-serif: #{$font-family-sans-serif}; + --bs-font-family-monospace: #{$font-family-monospace}; + --bs-font-family-base: #{$font-family-base}; + // stylelint-enable value-keyword-case + --bs-font-size-base: #{$font-size-base}; + --bs-font-size-lg: #{$font-size-lg}; + --bs-font-size-sm: #{$font-size-sm}; + --bs-font-weight-lighter: #{$font-weight-lighter}; + --bs-font-weight-light: #{$font-weight-light}; + --bs-font-weight-normal: #{$font-weight-normal}; + --bs-font-weight-bold: #{$font-weight-bold}; + --bs-font-weight-bolder: #{$font-weight-bolder}; + --bs-font-weight-base: #{$font-weight-base}; + --bs-line-height-base: #{$line-height-base}; + --bs-h1-font-size: #{$h1-font-size}; + --bs-h2-font-size: #{$h2-font-size}; + --bs-h3-font-size: #{$h3-font-size}; + --bs-h4-font-size: #{$h4-font-size}; + --bs-h5-font-size: #{$h5-font-size}; + --bs-h6-font-size: #{$h6-font-size}; + --bs-headings-margin-bottom: #{$headings-margin-bottom}; + --bs-headings-font-family: #{$headings-font-family}; + --bs-headings-font-weight: #{$headings-font-weight}; + --bs-headings-line-height: #{$headings-line-height}; + --bs-headings-color: #{$headings-color}; + --bs-display1-size: #{$display1-size}; + --bs-display2-size: #{$display2-size}; + --bs-display3-size: #{$display3-size}; + --bs-display4-size: #{$display4-size}; + --bs-display1-weight: #{$display1-weight}; + --bs-display2-weight: #{$display2-weight}; + --bs-display3-weight: #{$display3-weight}; + --bs-display4-weight: #{$display4-weight}; + --bs-display-line-height: #{$display-line-height}; + --bs-lead-font-size: #{$lead-font-size}; + --bs-lead-font-weight: #{$lead-font-weight}; + --bs-small-font-size: #{$small-font-size}; + --bs-text-muted: #{$text-muted}; + --bs-blockquote-small-color: #{$blockquote-small-color}; + --bs-blockquote-small-font-size: #{$blockquote-small-font-size}; + --bs-blockquote-font-size: #{$blockquote-font-size}; + --bs-hr-border-color: #{$hr-border-color}; + --bs-hr-border-width: #{$hr-border-width}; + --bs-mark-padding: #{$mark-padding}; + --bs-dt-font-weight: #{$dt-font-weight}; + --bs-kbd-box-shadow: #{$kbd-box-shadow}; + --bs-nested-kbd-font-weight: #{$nested-kbd-font-weight}; + --bs-list-inline-padding: #{$list-inline-padding}; + --bs-mark-bg: #{$mark-bg}; + --bs-hr-margin-y: #{$hr-margin-y}; + + + // Tables + // + // Customizes the `.table` component with basic values, each used across all table variations. + --bs-table-cell-padding: #{$table-cell-padding}; + --bs-table-cell-padding-sm: #{$table-cell-padding-sm}; + --bs-table-color: #{$table-color}; + --bs-table-bg: #{$table-bg}; + --bs-table-accent-bg: #{$table-accent-bg}; + --bs-table-hover-color: #{$table-hover-color}; + --bs-table-hover-bg: #{$table-hover-bg}; + --bs-table-active-bg: #{$table-active-bg}; + --bs-table-border-width: #{$table-border-width}; + --bs-table-border-color: #{$table-border-color}; + --bs-table-head-bg: #{$table-head-bg}; + --bs-table-head-color: #{$table-head-color}; + --bs-table-dark-color: #{$table-dark-color}; + --bs-table-dark-bg: #{$table-dark-bg}; + --bs-table-dark-accent-bg: #{$table-dark-accent-bg}; + --bs-table-dark-hover-color: #{$table-dark-hover-color}; + --bs-table-dark-hover-bg: #{$table-dark-hover-bg}; + --bs-table-dark-border-color: #{$table-dark-border-color}; + --bs-table-dark-color: #{$table-dark-color}; + --bs-table-striped-order: #{$table-striped-order}; + --bs-table-caption-color: #{$table-caption-color}; + --bs-table-bg-level: #{$table-bg-level}; + --bs-table-border-level: #{$table-border-level}; + + + // Buttons + Forms + // + // Shared variables that are reassigned to `$input-` and `$btn-` specific variables. + --bs-input-btn-padding-y: #{$input-btn-padding-y}; + --bs-input-btn-padding-x: #{$input-btn-padding-x}; + --bs-input-btn-font-family: #{$input-btn-font-family}; + --bs-input-btn-font-size: #{$input-btn-font-size}; + --bs-input-btn-line-height: #{$input-btn-line-height}; + --bs-input-btn-focus-width: #{$input-btn-focus-width}; + --bs-input-btn-focus-color: #{$input-btn-focus-color}; + --bs-input-btn-focus-box-shadow: #{$input-btn-focus-box-shadow}; + --bs-input-btn-padding-y-sm: #{$input-btn-padding-y-sm}; + --bs-input-btn-padding-x-sm: #{$input-btn-padding-x-sm}; + --bs-input-btn-font-size-sm: #{$input-btn-font-size-sm}; + --bs-input-btn-line-height-sm: #{$input-btn-line-height-sm}; + --bs-input-btn-padding-y-lg: #{$input-btn-padding-y-lg}; + --bs-input-btn-padding-x-lg: #{$input-btn-padding-x-lg}; + --bs-input-btn-font-size-lg: #{$input-btn-font-size-lg}; + --bs-input-btn-line-height-lg: #{$input-btn-line-height-lg}; + --bs-input-btn-border-width: #{$input-btn-border-width}; + + + // Buttons + // + // For each of Bootstrap's buttons, define text, background, and border color. + --bs-btn-padding-y: #{$btn-padding-y}; + --bs-btn-padding-x: #{$btn-padding-x}; + --bs-btn-font-family: #{$btn-font-family}; + --bs-btn-font-size: #{$btn-font-size}; + --bs-btn-line-height: #{$btn-line-height}; + --bs-btn-padding-y-sm: #{$btn-padding-y-sm}; + --bs-btn-padding-x-sm: #{$btn-padding-x-sm}; + --bs-btn-font-size-sm: #{$btn-font-size-sm}; + --bs-btn-line-height-sm: #{$btn-line-height-sm}; + --bs-btn-padding-y-lg: #{$btn-padding-y-lg}; + --bs-btn-padding-x-lg: #{$btn-padding-x-lg}; + --bs-btn-font-size-lg: #{$btn-font-size-lg}; + --bs-btn-line-height-lg: #{$btn-line-height-lg}; + --bs-btn-border-width: #{$btn-border-width}; + --bs-btn-font-weight: #{$btn-font-weight}; + --bs-btn-box-shadow: #{$btn-box-shadow}; + --bs-btn-focus-width: #{$btn-focus-width}; + --bs-btn-focus-box-shadow: #{$btn-focus-box-shadow}; + --bs-btn-disabled-opacity: #{$btn-disabled-opacity}; + --bs-btn-active-box-shadow: #{$btn-active-box-shadow}; + --bs-btn-link-disabled-color: #{$btn-link-disabled-color}; + --bs-btn-block-spacing-y: #{$btn-block-spacing-y}; + + // Allows for customizing button radius independently from global border radius + --bs-btn-border-radius: #{$btn-border-radius}; + --bs-btn-border-radius-lg: #{$btn-border-radius-lg}; + --bs-btn-border-radius-sm: #{$btn-border-radius-sm}; + --bs-btn-transition: #{$btn-transition}; + + + // Forms + --bs-label-margin-bottom: #{$label-margin-bottom}; + --bs-input-padding-y: #{$input-padding-y}; + --bs-input-padding-x: #{$input-padding-x}; + --bs-input-font-family: #{$input-font-family}; + --bs-input-font-size: #{$input-font-size}; + --bs-input-font-weight: #{$input-font-weight}; + --bs-input-line-height: #{$input-line-height}; + --bs-input-padding-y-sm: #{$input-padding-y-sm}; + --bs-input-padding-x-sm: #{$input-padding-x-sm}; + --bs-input-font-size-sm: #{$input-font-size-sm}; + --bs-input-line-height-sm: #{$input-line-height-sm}; + --bs-input-padding-y-lg: #{$input-padding-y-lg}; + --bs-input-padding-x-lg: #{$input-padding-x-lg}; + --bs-input-font-size-lg: #{$input-font-size-lg}; + --bs-input-line-height-lg: #{$input-line-height-lg}; + --bs-input-bg: #{$input-bg}; + --bs-input-disabled-bg: #{$input-disabled-bg}; + --bs-input-color: #{$input-color}; + --bs-input-border-color: #{$input-border-color}; + --bs-input-border-width: #{$input-border-width}; + --bs-input-box-shadow: #{$input-box-shadow}; + --bs-input-border-radius: #{$input-border-radius}; + --bs-input-border-radius-lg: #{$input-border-radius-lg}; + --bs-input-border-radius-sm: #{$input-border-radius-sm}; + --bs-input-focus-bg: #{$input-focus-bg}; + --bs-input-focus-border-color: #{$input-focus-border-color}; + --bs-input-focus-color: #{$input-focus-color}; + --bs-input-focus-width: #{$input-focus-width}; + --bs-input-focus-box-shadow: #{$input-focus-box-shadow}; + --bs-input-placeholder-color: #{$input-placeholder-color}; + --bs-input-plaintext-color: #{$input-plaintext-color}; + --bs-input-height-border: #{$input-height-border}; + --bs-input-height-inner: #{$input-height-inner}; + --bs-input-height-inner-half: #{$input-height-inner-half}; + --bs-input-height-inner-quarter: #{$input-height-inner-quarter}; + --bs-input-height: #{$input-height}; + --bs-input-height-sm: #{$input-height-sm}; + --bs-input-height-lg: #{$input-height-lg}; + --bs-input-transition: #{$input-transition}; + --bs-form-text-margin-top: #{$form-text-margin-top}; + --bs-form-check-input-gutter: #{$form-check-input-gutter}; + --bs-form-check-input-margin-y: #{$form-check-input-margin-y}; + --bs-form-check-input-margin-x: #{$form-check-input-margin-x}; + --bs-form-check-inline-margin-x: #{$form-check-inline-margin-x}; + --bs-form-check-inline-input-margin-x: #{$form-check-inline-input-margin-x}; + --bs-form-grid-gutter-width: #{$form-grid-gutter-width}; + --bs-form-group-margin-bottom: #{$form-group-margin-bottom}; + --bs-input-group-addon-color: #{$input-group-addon-color}; + --bs-input-group-addon-bg: #{$input-group-addon-bg}; + --bs-input-group-addon-border-color: #{$input-group-addon-border-color}; + --bs-custom-forms-transition: #{$custom-forms-transition}; + --bs-custom-control-gutter: #{$custom-control-gutter}; + --bs-custom-control-spacer-x: #{$custom-control-spacer-x}; + --bs-custom-control-indicator-size: #{$custom-control-indicator-size}; + --bs-custom-control-indicator-bg: #{$custom-control-indicator-bg}; + --bs-custom-control-indicator-bg-size: #{$custom-control-indicator-bg-size}; + --bs-custom-control-indicator-box-shadow: #{$custom-control-indicator-box-shadow}; + --bs-custom-control-indicator-border-color: #{$custom-control-indicator-border-color}; + --bs-custom-control-indicator-border-width: #{$custom-control-indicator-border-width}; + --bs-custom-control-indicator-disabled-bg: #{$custom-control-indicator-disabled-bg}; + --bs-custom-control-label-disabled-color: #{$custom-control-label-disabled-color}; + --bs-custom-control-indicator-checked-color: #{$custom-control-indicator-checked-color}; + --bs-custom-control-indicator-checked-bg: #{$custom-control-indicator-checked-bg}; + --bs-custom-control-indicator-checked-disabled-bg: #{$custom-control-indicator-checked-disabled-bg}; + --bs-custom-control-indicator-checked-box-shadow: #{$custom-control-indicator-checked-box-shadow}; + --bs-custom-control-indicator-checked-border-color: #{$custom-control-indicator-checked-border-color}; + --bs-custom-control-indicator-focus-box-shadow: #{$custom-control-indicator-focus-box-shadow}; + --bs-custom-control-indicator-focus-border-color: #{$custom-control-indicator-focus-border-color}; + --bs-custom-control-indicator-active-color: #{$custom-control-indicator-active-color}; + --bs-custom-control-indicator-active-bg: #{$custom-control-indicator-active-bg}; + --bs-custom-control-indicator-active-box-shadow: #{$custom-control-indicator-active-box-shadow}; + --bs-custom-control-indicator-active-border-color: #{$custom-control-indicator-active-border-color}; + --bs-custom-checkbox-indicator-border-radius: #{$custom-checkbox-indicator-border-radius}; + --bs-custom-checkbox-indicator-icon-checked: #{$custom-checkbox-indicator-icon-checked}; + --bs-custom-checkbox-indicator-indeterminate-bg: #{$custom-checkbox-indicator-indeterminate-bg}; + --bs-custom-checkbox-indicator-indeterminate-color: #{$custom-checkbox-indicator-indeterminate-color}; + --bs-custom-checkbox-indicator-icon-indeterminate: #{$custom-checkbox-indicator-icon-indeterminate}; + --bs-custom-checkbox-indicator-indeterminate-box-shadow: #{$custom-checkbox-indicator-indeterminate-box-shadow}; + --bs-custom-checkbox-indicator-indeterminate-border-color: #{$custom-checkbox-indicator-indeterminate-border-color}; + --bs-custom-radio-indicator-border-radius: #{$custom-radio-indicator-border-radius}; + --bs-custom-radio-indicator-icon-checked: #{$custom-radio-indicator-icon-checked}; + --bs-custom-switch-width: #{$custom-switch-width}; + --bs-custom-switch-indicator-border-radius: #{$custom-switch-indicator-border-radius}; + --bs-custom-switch-indicator-size: #{$custom-switch-indicator-size}; + --bs-custom-select-padding-y: #{$custom-select-padding-y}; + --bs-custom-select-padding-x: #{$custom-select-padding-x}; + --bs-custom-select-font-family: #{$custom-select-font-family}; + --bs-custom-select-font-size: #{$custom-select-font-size}; + --bs-custom-select-height: #{$custom-select-height}; + --bs-custom-select-indicator-padding: #{$custom-select-indicator-padding}; + --bs-custom-select-font-weight: #{$custom-select-font-weight}; + --bs-custom-select-line-height: #{$custom-select-line-height}; + --bs-custom-select-color: #{$custom-select-color}; + --bs-custom-select-disabled-color: #{$custom-select-disabled-color}; + --bs-custom-select-bg: #{$custom-select-bg}; + --bs-custom-select-disabled-bg: #{$custom-select-disabled-bg}; + --bs-custom-select-bg-size: #{$custom-select-bg-size}; + --bs-custom-select-indicator-color: #{$custom-select-indicator-color}; + --bs-custom-select-indicator: #{$custom-select-indicator}; + --bs-custom-select-background: #{$custom-select-background}; + --bs-custom-select-feedback-icon-padding-right: #{$custom-select-feedback-icon-padding-right}; + --bs-custom-select-feedback-icon-position: #{$custom-select-feedback-icon-position}; + --bs-custom-select-feedback-icon-size: #{$custom-select-feedback-icon-size}; + --bs-custom-select-border-width: #{$custom-select-border-width}; + --bs-custom-select-border-color: #{$custom-select-border-color}; + --bs-custom-select-border-radius: #{$custom-select-border-radius}; + --bs-custom-select-box-shadow: #{$custom-select-box-shadow}; + --bs-custom-select-focus-border-color: #{$custom-select-focus-border-color}; + --bs-custom-select-focus-width: #{$custom-select-focus-width}; + --bs-custom-select-focus-box-shadow: #{$custom-select-focus-box-shadow}; + --bs-custom-select-padding-y-sm: #{$custom-select-padding-y-sm}; + --bs-custom-select-padding-x-sm: #{$custom-select-padding-x-sm}; + --bs-custom-select-font-size-sm: #{$custom-select-font-size-sm}; + --bs-custom-select-height-sm: #{$custom-select-height-sm}; + --bs-custom-select-padding-y-lg: #{$custom-select-padding-y-lg}; + --bs-custom-select-padding-x-lg: #{$custom-select-padding-x-lg}; + --bs-custom-select-font-size-lg: #{$custom-select-font-size-lg}; + --bs-custom-select-height-lg: #{$custom-select-height-lg}; + --bs-custom-range-track-width: #{$custom-range-track-width}; + --bs-custom-range-track-height: #{$custom-range-track-height}; + --bs-custom-range-track-cursor: #{$custom-range-track-cursor}; + --bs-custom-range-track-bg: #{$custom-range-track-bg}; + --bs-custom-range-track-border-radius: #{$custom-range-track-border-radius}; + --bs-custom-range-track-box-shadow: #{$custom-range-track-box-shadow}; + --bs-custom-range-thumb-width: #{$custom-range-thumb-width}; + --bs-custom-range-thumb-height: #{$custom-range-thumb-height}; + --bs-custom-range-thumb-bg: #{$custom-range-thumb-bg}; + --bs-custom-range-thumb-border: #{$custom-range-thumb-border}; + --bs-custom-range-thumb-border-radius: #{$custom-range-thumb-border-radius}; + --bs-custom-range-thumb-box-shadow: #{$custom-range-thumb-box-shadow}; + --bs-custom-range-thumb-focus-box-shadow: #{$custom-range-thumb-focus-box-shadow}; + --bs-custom-range-thumb-focus-box-shadow-width: #{$custom-range-thumb-focus-box-shadow-width}; + --bs-custom-range-thumb-active-bg: #{$custom-range-thumb-active-bg}; + --bs-custom-range-thumb-disabled-bg: #{$custom-range-thumb-disabled-bg}; + --bs-custom-file-height: #{$custom-file-height}; + --bs-custom-file-height-inner: #{$custom-file-height-inner}; + --bs-custom-file-focus-border-color: #{$custom-file-focus-border-color}; + --bs-custom-file-focus-box-shadow: #{$custom-file-focus-box-shadow}; + --bs-custom-file-disabled-bg: #{$custom-file-disabled-bg}; + --bs-custom-file-padding-y: #{$custom-file-padding-y}; + --bs-custom-file-padding-x: #{$custom-file-padding-x}; + --bs-custom-file-line-height: #{$custom-file-line-height}; + --bs-custom-file-font-family: #{$custom-file-font-family}; + --bs-custom-file-font-weight: #{$custom-file-font-weight}; + --bs-custom-file-color: #{$custom-file-color}; + --bs-custom-file-bg: #{$custom-file-bg}; + --bs-custom-file-border-width: #{$custom-file-border-width}; + --bs-custom-file-border-color: #{$custom-file-border-color}; + --bs-custom-file-border-radius: #{$custom-file-border-radius}; + --bs-custom-file-box-shadow: #{$custom-file-box-shadow}; + --bs-custom-file-button-color: #{$custom-file-button-color}; + --bs-custom-file-button-bg: #{$custom-file-button-bg}; + //--bs-custom-file-text: #{$custom-file-text}; //mapping doesn't work + + + // Form validation + --bs-form-feedback-margin-top: #{$form-feedback-margin-top}; + --bs-form-feedback-font-size: #{$form-feedback-font-size}; + --bs-form-feedback-valid-color: #{$form-feedback-valid-color}; + --bs-form-feedback-invalid-color: #{$form-feedback-invalid-color}; + --bs-form-feedback-icon-valid-color: #{$form-feedback-icon-valid-color}; + --bs-form-feedback-icon-valid: #{$form-feedback-icon-valid}; + --bs-form-feedback-icon-invalid-color: #{$form-feedback-icon-invalid-color}; + --bs-form-feedback-icon-invalid: #{$form-feedback-icon-invalid}; + // Z-index master list + // + // Warning: Avoid customizing these values. They're used for a bird's eye view + // of components dependent on the z-axis and are designed to all work together. + --bs-zindex-dropdown: #{$zindex-dropdown}; + --bs-zindex-sticky: #{$zindex-sticky}; + --bs-zindex-fixed: #{$zindex-fixed}; + --bs-zindex-modal-backdrop: #{$zindex-modal-backdrop}; + --bs-zindex-modal: #{$zindex-modal}; + --bs-zindex-popover: #{$zindex-popover}; + --bs-zindex-tooltip: #{$zindex-tooltip}; + + + // Navs + --bs-nav-link-padding-y: #{$nav-link-padding-y}; + --bs-nav-link-padding-x: #{$nav-link-padding-x}; + --bs-nav-link-disabled-color: #{$nav-link-disabled-color}; + --bs-nav-tabs-border-color: #{$nav-tabs-border-color}; + --bs-nav-tabs-border-width: #{$nav-tabs-border-width}; + --bs-nav-tabs-border-radius: #{$nav-tabs-border-radius}; + --bs-nav-tabs-link-hover-border-color: #{$nav-tabs-link-hover-border-color}; + --bs-nav-tabs-link-active-color: #{$nav-tabs-link-active-color}; + --bs-nav-tabs-link-active-bg: #{$nav-tabs-link-active-bg}; + --bs-nav-tabs-link-active-border-color: #{$nav-tabs-link-active-border-color}; + --bs-nav-pills-border-radius: #{$nav-pills-border-radius}; + --bs-nav-pills-link-active-color: #{$nav-pills-link-active-color}; + --bs-nav-pills-link-active-bg: #{$nav-pills-link-active-bg}; + --bs-nav-divider-color: #{$nav-divider-color}; + --bs-nav-divider-margin-y: #{$nav-divider-margin-y}; + + + // Navbar + --bs-navbar-padding-y: #{$navbar-padding-y}; + --bs-navbar-padding-x: #{$navbar-padding-x}; + --bs-navbar-nav-link-padding-x: #{$navbar-nav-link-padding-x}; + --bs-navbar-brand-font-size: #{$navbar-brand-font-size}; + // Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link + --bs-nav-link-height: #{$nav-link-height}; + --bs-navbar-brand-height: #{$navbar-brand-height}; + --bs-navbar-brand-padding-y: #{$navbar-brand-padding-y}; + --bs-navbar-toggler-padding-y: #{$navbar-toggler-padding-y}; + --bs-navbar-toggler-padding-x: #{$navbar-toggler-padding-x}; + --bs-navbar-toggler-font-size: #{$navbar-toggler-font-size}; + --bs-navbar-toggler-border-radius: #{$navbar-toggler-border-radius}; + --bs-navbar-dark-color: #{$navbar-dark-color}; + --bs-navbar-dark-hover-color: #{$navbar-dark-hover-color}; + --bs-navbar-dark-active-color: #{$navbar-dark-active-color}; + --bs-navbar-dark-disabled-color: #{$navbar-dark-disabled-color}; + --bs-navbar-dark-toggler-icon-bg: #{$navbar-dark-toggler-icon-bg}; + --bs-navbar-dark-toggler-border-color: #{$navbar-dark-toggler-border-color}; + --bs-navbar-light-color: #{$navbar-light-color}; + --bs-navbar-light-hover-color: #{$navbar-light-hover-color}; + --bs-navbar-light-active-color: #{$navbar-light-active-color}; + --bs-navbar-light-disabled-color: #{$navbar-light-disabled-color}; + --bs-navbar-light-toggler-icon-bg: #{$navbar-light-toggler-icon-bg}; + --bs-navbar-light-toggler-border-color: #{$navbar-light-toggler-border-color}; + --bs-navbar-light-brand-color: #{$navbar-light-brand-color}; + --bs-navbar-light-brand-hover-color: #{$navbar-light-brand-hover-color}; + --bs-navbar-dark-brand-color: #{$navbar-dark-brand-color}; + --bs-navbar-dark-brand-hover-color: #{$navbar-dark-brand-hover-color}; + + + // Dropdowns + // + // Dropdown menu container and contents. + --bs-dropdown-min-width: #{$dropdown-min-width}; + --bs-dropdown-padding-y: #{$dropdown-padding-y}; + --bs-dropdown-spacer: #{$dropdown-spacer}; + --bs-dropdown-font-size: #{$dropdown-font-size}; + --bs-dropdown-color: #{$dropdown-color}; + --bs-dropdown-bg: #{$dropdown-bg}; + --bs-dropdown-border-color: #{$dropdown-border-color}; + --bs-dropdown-border-radius: #{$dropdown-border-radius}; + --bs-dropdown-border-width: #{$dropdown-border-width}; + --bs-dropdown-inner-border-radius: #{$dropdown-inner-border-radius}; + --bs-dropdown-divider-bg: #{$dropdown-divider-bg}; + --bs-dropdown-divider-margin-y: #{$dropdown-divider-margin-y}; + --bs-dropdown-box-shadow: #{$dropdown-box-shadow}; + --bs-dropdown-link-color: #{$dropdown-link-color}; + --bs-dropdown-link-hover-color: #{$dropdown-link-hover-color}; + --bs-dropdown-link-hover-bg: #{$dropdown-link-hover-bg}; + --bs-dropdown-link-active-color: #{$dropdown-link-active-color}; + --bs-dropdown-link-active-bg: #{$dropdown-link-active-bg}; + --bs-dropdown-link-disabled-color: #{$dropdown-link-disabled-color}; + --bs-dropdown-item-padding-y: #{$dropdown-item-padding-y}; + --bs-dropdown-item-padding-x: #{$dropdown-item-padding-x}; + --bs-dropdown-header-color: #{$dropdown-header-color}; + + + // Pagination + --bs-pagination-padding-y: #{$pagination-padding-y}; + --bs-pagination-padding-x: #{$pagination-padding-x}; + --bs-pagination-padding-y-sm: #{$pagination-padding-y-sm}; + --bs-pagination-padding-x-sm: #{$pagination-padding-x-sm}; + --bs-pagination-padding-y-lg: #{$pagination-padding-y-lg}; + --bs-pagination-padding-x-lg: #{$pagination-padding-x-lg}; + --bs-pagination-line-height: #{$pagination-line-height}; + --bs-pagination-color: #{$pagination-color}; + --bs-pagination-bg: #{$pagination-bg}; + --bs-pagination-border-width: #{$pagination-border-width}; + --bs-pagination-border-color: #{$pagination-border-color}; + --bs-pagination-focus-box-shadow: #{$pagination-focus-box-shadow}; + --bs-pagination-focus-outline: #{$pagination-focus-outline}; + --bs-pagination-hover-color: #{$pagination-hover-color}; + --bs-pagination-hover-bg: #{$pagination-hover-bg}; + --bs-pagination-hover-border-color: #{$pagination-hover-border-color}; + --bs-pagination-active-color: #{$pagination-active-color}; + --bs-pagination-active-bg: #{$pagination-active-bg}; + --bs-pagination-active-border-color: #{$pagination-active-border-color}; + --bs-pagination-disabled-color: #{$pagination-disabled-color}; + --bs-pagination-disabled-bg: #{$pagination-disabled-bg}; + --bs-pagination-disabled-border-color: #{$pagination-disabled-border-color}; + + + // Jumbotron + --bs-jumbotron-padding: #{$jumbotron-padding}; + --bs-jumbotron-color: #{$jumbotron-color}; + --bs-jumbotron-bg: #{$jumbotron-bg}; + + + // Cards + --bs-card-spacer-y: #{$card-spacer-y}; + --bs-card-spacer-x: #{$card-spacer-x}; + --bs-card-border-width: #{$card-border-width}; + --bs-card-border-radius: #{$card-border-radius}; + --bs-card-border-color: #{$card-border-color}; + --bs-card-inner-border-radius: #{$card-inner-border-radius}; + --bs-card-cap-bg: #{$card-cap-bg}; + --bs-card-cap-color: #{$card-cap-color}; + --bs-card-color: #{$card-color}; + --bs-card-bg: #{$card-bg}; + --bs-card-img-overlay-padding: #{$card-img-overlay-padding}; + --bs-card-group-margin: #{$card-group-margin}; + --bs-card-deck-margin: #{$card-deck-margin}; + --bs-card-columns-count: #{$card-columns-count}; + --bs-card-columns-gap: #{$card-columns-gap}; + --bs-card-columns-margin: #{$card-columns-margin}; + + + // Tooltips + --bs-tooltip-font-size: #{$tooltip-font-size}; + --bs-tooltip-max-width: #{$tooltip-max-width}; + --bs-tooltip-color: #{$tooltip-color}; + --bs-tooltip-bg: #{$tooltip-bg}; + --bs-tooltip-border-radius: #{$tooltip-border-radius}; + --bs-tooltip-opacity: #{$tooltip-opacity}; + --bs-tooltip-padding-y: #{$tooltip-padding-y}; + --bs-tooltip-padding-x: #{$tooltip-padding-x}; + --bs-tooltip-margin: #{$tooltip-margin}; + --bs-tooltip-arrow-width: #{$tooltip-arrow-width}; + --bs-tooltip-arrow-height: #{$tooltip-arrow-height}; + --bs-tooltip-arrow-color: #{$tooltip-arrow-color}; + + // Form tooltips must come after regular tooltips + --bs-form-feedback-tooltip-padding-y: #{$form-feedback-tooltip-padding-y}; + --bs-form-feedback-tooltip-padding-x: #{$form-feedback-tooltip-padding-x}; + --bs-form-feedback-tooltip-font-size: #{$form-feedback-tooltip-font-size}; + --bs-form-feedback-tooltip-line-height: #{$form-feedback-tooltip-line-height}; + --bs-form-feedback-tooltip-opacity: #{$form-feedback-tooltip-opacity}; + --bs-form-feedback-tooltip-border-radius: #{$form-feedback-tooltip-border-radius}; + + + // Popovers + --bs-popover-font-size: #{$popover-font-size}; + --bs-popover-bg: #{$popover-bg}; + --bs-popover-max-width: #{$popover-max-width}; + --bs-popover-border-width: #{$popover-border-width}; + --bs-popover-border-color: #{$popover-border-color}; + --bs-popover-border-radius: #{$popover-border-radius}; + --bs-popover-box-shadow: #{$popover-box-shadow}; + --bs-popover-header-bg: #{$popover-header-bg}; + --bs-popover-header-color: #{$popover-header-color}; + --bs-popover-header-padding-y: #{$popover-header-padding-y}; + --bs-popover-header-padding-x: #{$popover-header-padding-x}; + --bs-popover-body-color: #{$popover-body-color}; + --bs-popover-body-padding-y: #{$popover-body-padding-y}; + --bs-popover-body-padding-x: #{$popover-body-padding-x}; + --bs-popover-arrow-width: #{$popover-arrow-width}; + --bs-popover-arrow-height: #{$popover-arrow-height}; + --bs-popover-arrow-color: #{$popover-arrow-color}; + --bs-popover-arrow-outer-color: #{$popover-arrow-outer-color}; + + + // Toasts + --bs-toast-max-width: #{$toast-max-width}; + --bs-toast-padding-x: #{$toast-padding-x}; + --bs-toast-padding-y: #{$toast-padding-y}; + --bs-toast-font-size: #{$toast-font-size}; + --bs-toast-color: #{$toast-color}; + --bs-toast-background-color: #{$toast-background-color}; + --bs-toast-border-width: #{$toast-border-width}; + --bs-toast-border-color: #{$toast-border-color}; + --bs-toast-border-radius: #{$toast-border-radius}; + --bs-toast-box-shadow: #{$toast-box-shadow}; + --bs-toast-header-color: #{$toast-header-color}; + --bs-toast-header-background-color: #{$toast-header-background-color}; + --bs-toast-header-border-color: #{$toast-header-border-color}; + + + // Badges + --bs-badge-font-size: #{$badge-font-size}; + --bs-badge-font-weight: #{$badge-font-weight}; + --bs-badge-padding-y: #{$badge-padding-y}; + --bs-badge-padding-x: #{$badge-padding-x}; + --bs-badge-border-radius: #{$badge-border-radius}; + --bs-badge-transition: #{$badge-transition}; + --bs-badge-focus-width: #{$badge-focus-width}; + --bs-badge-pill-padding-x: #{$badge-pill-padding-x}; + // Use a higher than normal value to ensure completely rounded edges when + // customizing padding or font-size on labels. + --bs-badge-pill-border-radius: #{$badge-pill-border-radius}; + + + // Modals + + // Padding applied to the modal body + --bs-modal-inner-padding: #{$modal-inner-padding}; + --bs-modal-dialog-margin: #{$modal-dialog-margin}; + --bs-modal-dialog-margin-y-sm-up: #{$modal-dialog-margin-y-sm-up}; + --bs-modal-title-line-height: #{$modal-title-line-height}; + --bs-modal-content-color: #{$modal-content-color}; + --bs-modal-content-bg: #{$modal-content-bg}; + --bs-modal-content-border-color: #{$modal-content-border-color}; + --bs-modal-content-border-width: #{$modal-content-border-width}; + --bs-modal-content-border-radius: #{$modal-content-border-radius}; + --bs-modal-content-box-shadow-xs: #{$modal-content-box-shadow-xs}; + --bs-modal-content-box-shadow-sm-up: #{$modal-content-box-shadow-sm-up}; + --bs-modal-backdrop-bg: #{$modal-backdrop-bg}; + --bs-modal-backdrop-opacity: #{$modal-backdrop-opacity}; + --bs-modal-header-border-color: #{$modal-header-border-color}; + --bs-modal-footer-border-color: #{$modal-footer-border-color}; + --bs-modal-header-border-width: #{$modal-header-border-width}; + --bs-modal-footer-border-width: #{$modal-footer-border-width}; + --bs-modal-header-padding-y: #{$modal-header-padding-y}; + --bs-modal-header-padding-x: #{$modal-header-padding-x}; + --bs-modal-header-padding: #{$modal-header-padding}; + --bs-modal-xl: #{$modal-xl}; + --bs-modal-lg: #{$modal-lg}; + --bs-modal-md: #{$modal-md}; + --bs-modal-sm: #{$modal-sm}; + --bs-modal-fade-transform: #{$modal-fade-transform}; + --bs-modal-show-transform: #{$modal-show-transform}; + --bs-modal-transition: #{$modal-transition}; + + + // Alerts + // + // Define alert colors, border radius, and padding. + --bs-alert-padding-y: #{$alert-padding-y}; + --bs-alert-padding-x: #{$alert-padding-x}; + --bs-alert-margin-bottom: #{$alert-margin-bottom}; + --bs-alert-border-radius: #{$alert-border-radius}; + --bs-alert-link-font-weight: #{$alert-link-font-weight}; + --bs-alert-border-width: #{$alert-border-width}; + --bs-alert-bg-level: #{$alert-bg-level}; + --bs-alert-border-level: #{$alert-border-level}; + --bs-alert-color-level: #{$alert-color-level}; + + + // Progress bars + --bs-progress-height: #{$progress-height}; + --bs-progress-font-size: #{$progress-font-size}; + --bs-progress-bg: #{$progress-bg}; + --bs-progress-border-radius: #{$progress-border-radius}; + --bs-progress-box-shadow: #{$progress-box-shadow}; + --bs-progress-bar-color: #{$progress-bar-color}; + --bs-progress-bar-bg: #{$progress-bar-bg}; + --bs-progress-bar-animation-timing: #{$progress-bar-animation-timing}; + --bs-progress-bar-transition: #{$progress-bar-transition}; + + + // List group + --bs-list-group-color: #{$list-group-color}; + --bs-list-group-bg: #{$list-group-bg}; + --bs-list-group-border-color: #{$list-group-border-color}; + --bs-list-group-border-width: #{$list-group-border-width}; + --bs-list-group-border-radius: #{$list-group-border-radius}; + --bs-list-group-item-padding-y: #{$list-group-item-padding-y}; + --bs-list-group-item-padding-x: #{$list-group-item-padding-x}; + --bs-list-group-hover-bg: #{$list-group-hover-bg}; + --bs-list-group-active-color: #{$list-group-active-color}; + --bs-list-group-active-bg: #{$list-group-active-bg}; + --bs-list-group-active-border-color: #{$list-group-active-border-color}; + --bs-list-group-disabled-color: #{$list-group-disabled-color}; + --bs-list-group-disabled-bg: #{$list-group-disabled-bg}; + --bs-list-group-action-color: #{$list-group-action-color}; + --bs-list-group-action-hover-color: #{$list-group-action-hover-color}; + --bs-list-group-action-active-color: #{$list-group-action-active-color}; + --bs-list-group-action-active-bg: #{$list-group-action-active-bg}; + + + // Image thumbnails + --bs-thumbnail-padding: #{$thumbnail-padding}; + --bs-thumbnail-bg: #{$thumbnail-bg}; + --bs-thumbnail-border-width: #{$thumbnail-border-width}; + --bs-thumbnail-border-color: #{$thumbnail-border-color}; + --bs-thumbnail-border-radius: #{$thumbnail-border-radius}; + --bs-thumbnail-box-shadow: #{$thumbnail-box-shadow}; + + + // Figures + --bs-figure-caption-font-size: #{$figure-caption-font-size}; + --bs-figure-caption-color: #{$figure-caption-color}; + + + // Breadcrumbs + --bs-breadcrumb-padding-y: #{$breadcrumb-padding-y}; + --bs-breadcrumb-padding-x: #{$breadcrumb-padding-x}; + --bs-breadcrumb-item-padding: #{$breadcrumb-item-padding}; + --bs-breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom}; + --bs-breadcrumb-bg: #{$breadcrumb-bg}; + --bs-breadcrumb-divider-color: #{$breadcrumb-divider-color}; + --bs-breadcrumb-active-color: #{$breadcrumb-active-color}; + --bs-breadcrumb-divider: #{$breadcrumb-divider}; + --bs-breadcrumb-border-radius: #{$breadcrumb-border-radius}; + + + // Carousel + --bs-carousel-control-color: #{$carousel-control-color}; + --bs-carousel-control-width: #{$carousel-control-width}; + --bs-carousel-control-opacity: #{$carousel-control-opacity}; + --bs-carousel-control-hover-opacity: #{$carousel-control-hover-opacity}; + --bs-carousel-control-transition: #{$carousel-control-transition}; + --bs-carousel-indicator-width: #{$carousel-indicator-width}; + --bs-carousel-indicator-height: #{$carousel-indicator-height}; + --bs-carousel-indicator-hit-area-height: #{$carousel-indicator-hit-area-height}; + --bs-carousel-indicator-spacer: #{$carousel-indicator-spacer}; + --bs-carousel-indicator-active-bg: #{$carousel-indicator-active-bg}; + --bs-carousel-indicator-transition: #{$carousel-indicator-transition}; + --bs-carousel-caption-width: #{$carousel-caption-width}; + --bs-carousel-caption-color: #{$carousel-caption-color}; + --bs-carousel-control-icon-width: #{$carousel-control-icon-width}; + --bs-carousel-control-prev-icon-bg: #{$carousel-control-prev-icon-bg}; + --bs-carousel-control-next-icon-bg: #{$carousel-control-next-icon-bg}; + --bs-carousel-transition-duration: #{$carousel-transition-duration}; + --bs-carousel-transition: #{$carousel-transition}; + + + // Spinners + --bs-spinner-width: #{$spinner-width}; + --bs-spinner-height: #{$spinner-height}; + --bs-spinner-border-width: #{$spinner-border-width}; + --bs-spinner-width-sm: #{$spinner-width-sm}; + --bs-spinner-height-sm: #{$spinner-height-sm}; + --bs-spinner-border-width-sm: #{$spinner-border-width-sm}; + + + // Close + --bs-close-font-size: #{$close-font-size}; + --bs-close-font-weight: #{$close-font-weight}; + --bs-close-color: #{$close-color}; + --bs-close-text-shadow: #{$close-text-shadow}; + + + // Code + --bs-code-font-size: #{$code-font-size}; + --bs-code-color: #{$code-color}; + --bs-kbd-padding-y: #{$kbd-padding-y}; + --bs-kbd-padding-x: #{$kbd-padding-x}; + --bs-kbd-font-size: #{$kbd-font-size}; + --bs-kbd-color: #{$kbd-color}; + --bs-kbd-bg: #{$kbd-bg}; + --bs-pre-color: #{$pre-color}; + --bs-pre-scrollable-max-height: #{$pre-scrollable-max-height}; + + + // Utilities + --bs-displays: #{$displays}; + --bs-overflows: #{$overflows}; + --bs-positions: #{$positions}; + + + // Printing + --bs-print-page-size: #{$print-page-size}; + --bs-print-body-min-width: #{$print-body-min-width}; + +} diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index dd52adcfbe8..04db184a3dc 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -1,43 +1,57 @@ -// @import '_themed_custom_variables.scss'; - -$content-spacing: $spacer * 1.5 !default; - -$button-height: $input-btn-padding-y * 2 + $input-btn-line-height + calculateRem($input-btn-border-width*2) !default; - -$card-height-percentage:98% !default; -$card-thumbnail-height:240px !default; -$dropdown-menu-max-height: 200px !default; -$drop-zone-area-height: 44px !default; -$drop-zone-area-z-index: 1025 !default; -$drop-zone-area-inner-z-index: 1021 !default; -$login-logo-height:72px !default; -$login-logo-width:72px !default; -$submission-header-z-index: 1001 !default; -$submission-footer-z-index: 999 !default; - -$main-z-index: 0 !default; -$nav-z-index: 10 !default; -$sidebar-z-index: 20 !default; - -$header-logo-height: 80px !default; -$header-logo-height-xs: 50px !default; -$header-icon-color: $link-color !default; - -$admin-sidebar-bg: darken(#2B4E72, 17%) !default; -$admin-sidebar-active-bg: darken($admin-sidebar-bg, 3%) !default; -$admin-sidebar-header-bg: darken($admin-sidebar-bg, 7%) !default; - -$dark-scrollbar-background: $admin-sidebar-active-bg !default; -$dark-scrollbar-foreground: #47495d !default; - -$submission-sections-margin-bottom: .5rem !default; - -$edit-item-button-min-width: 100px !default; -$edit-item-metadata-field-width: 190px !default; -$edit-item-language-field-width: 43px !default; - -$thumbnail-max-width: 175px !default; - -$dso-selector-list-max-height: 475px !default; -$dso-selector-current-background-color: #eeeeee; -$dso-selector-current-background-hover-color: darken($dso-selector-current-background-color, 10%); +:root { + --ds-content-spacing: #{$spacer * 1.5}; + + --ds-button-height: #{$input-btn-padding-y * 2 + $input-btn-line-height + calculateRem($input-btn-border-width*2)}; + + --ds-card-height-percentage:98%; + --ds-card-thumbnail-height:240px; + --ds-dropdown-menu-max-height: 200px; + --ds-drop-zone-area-height: 44px; + --ds-drop-zone-area-z-index: 1025; + --ds-drop-zone-area-inner-z-index: 1021; + --ds-login-logo-height:72px; + --ds-login-logo-width:72px; + --ds-submission-header-z-index: 1001; + --ds-submission-footer-z-index: 999; + + --ds-main-z-index: 0; + --ds-nav-z-index: 10; + --ds-sidebar-z-index: 20; + + --ds-header-logo-height: 80px; + --ds-header-logo-height-xs: 50px; + --ds-header-icon-color: #{$link-color}; + --ds-header-icon-color-hover: #{darken($link-color, 15%)}; + + $admin-sidebar-bg: darken(#2B4E72, 17%); + $admin-sidebar-active-bg: darken($admin-sidebar-bg, 3%); + --ds-admin-sidebar-bg: #{$admin-sidebar-bg}; + --ds-admin-sidebar-active-bg: #{$admin-sidebar-active-bg}; + --ds-admin-sidebar-header-bg: #{darken($admin-sidebar-bg, 7%)}; + + --ds-dark-scrollbar-bg: #{$admin-sidebar-active-bg}; + --ds-dark-scrollbar-alt-bg: #{lighten($admin-sidebar-active-bg, 2%)}; + --ds-dark-scrollbar-fg: #47495d; + + --ds-submission-sections-margin-bottom: .5rem; + + --ds-edit-item-button-min-width: 100px; + --ds-edit-item-metadata-field-width: 190px; + --ds-edit-item-language-field-width: 43px; + + --ds-thumbnail-max-width: 175px; + + --ds-dso-selector-list-max-height: 475px; + --ds-dso-selector-current-background-color: #eeeeee; + --ds-dso-selector-current-background-hover-color: #{darken(#eeeeee, 10%)}; + --ds-notification-bg-success: #{darken(adjust-hue($success, -10), 10%)}; + --ds-notification-bg-danger: #{darken(adjust-hue($danger, -10), 10%)}; + --ds-notification-bg-info: #{darken(adjust-hue($info, -10), 10%)}; + --ds-notification-bg-warning: #{darken(adjust-hue($warning, -10), 10%)}; + + --ds-fa-fixed-width: #{$fa-fixed-width}; + --ds-icon-padding: #{$icon-padding}; + --ds-collapsed-sidebar-width: #{$collapsed-sidebar-width}; + --ds-sidebar-items-width: #{$sidebar-items-width}; + --ds-total-sidebar-width: #{$total-sidebar-width}; +} diff --git a/src/styles/_exposed_variables.scss b/src/styles/_exposed_variables.scss index 1ab67e709d5..d01544649bf 100644 --- a/src/styles/_exposed_variables.scss +++ b/src/styles/_exposed_variables.scss @@ -1,12 +1,14 @@ @import '_variables.scss'; +// These CSS variables are picked up by CSSVariableService to and made available as JS variables as +// well. :export { - xlMin: map-get($grid-breakpoints, xl); - mdMin: map-get($grid-breakpoints, md); - lgMin: map-get($grid-breakpoints, lg); - smMin: map-get($grid-breakpoints, sm); - adminSidebarActiveBg: $admin-sidebar-active-bg; - sidebarItemsWidth: $sidebar-items-width; - collapsedSidebarWidth: $collapsed-sidebar-width; - totalSidebarWidth: $total-sidebar-width; + xlMin: var(--bs-xl); + mdMin: var(--bs-md); + lgMin: var(--bs-lg); + smMin: var(--bs-sm); + adminSidebarActiveBg: var(--ds-admin-sidebar-active-bg); + sidebarItemsWidth: var(--ds-sidebar-items-width); + collapsedSidebarWidth: var(--ds-collapsed-sidebar-width); + totalSidebarWidth: var(--ds-total-sidebar-width); } diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss new file mode 100644 index 00000000000..b79cf52fbbf --- /dev/null +++ b/src/styles/_global-styles.scss @@ -0,0 +1,49 @@ +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: var(--ds-main-z-index); + flex: 1 1 100%; + margin-top: var(--ds-content-spacing); + margin-bottom: var(--ds-content-spacing); +} + +.alert.hide { + padding: 0; + margin: 0; +} + +ds-header-navbar-wrapper { + z-index: var(--ds-nav-z-index); +} + +ds-admin-sidebar { + position: fixed; + z-index: var(--ds-sidebar-z-index); +} + +.ds-full-screen-loader { + height: 100vh; +} diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index e72af304cd3..04347b31312 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -19,23 +19,23 @@ height: 3px; } &::-webkit-scrollbar-button { - background-color: $dark-scrollbar-background; + background-color: var(--ds-dark-scrollbar-bg); } &::-webkit-scrollbar-track { - background-color: lighten($dark-scrollbar-background, 2%); + background-color: var(--ds-dark-scrollbar-alt-bg); } &::-webkit-scrollbar-track-piece { - background-color: $dark-scrollbar-background; + background-color: var(--ds-dark-scrollbar-bg); } &::-webkit-scrollbar-thumb { height: 50px; - background-color: $dark-scrollbar-foreground; + background-color: var(--ds-dark-scrollbar-fg); border-radius: 3px; } &::-webkit-scrollbar-corner { - background-color: lighten($dark-scrollbar-background, 2%); + background-color: var(--ds-dark-scrollbar-alt-bg); } &::-webkit-resizer { - background-color: $dark-scrollbar-background; + background-color: var(--ds-dark-scrollbar-bg); } } diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 2632dbfd09f..badc159747a 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -3,5 +3,3 @@ @import '_bootstrap_variables.scss'; @import '../../node_modules/bootstrap/scss/variables.scss'; - -@import '_custom_variables.scss'; diff --git a/src/styles/base-theme.scss b/src/styles/base-theme.scss new file mode 100644 index 00000000000..068c2ece263 --- /dev/null +++ b/src/styles/base-theme.scss @@ -0,0 +1,6 @@ +@import './helpers/font_awesome_imports.scss'; +@import '../../node_modules/bootstrap/scss/bootstrap.scss'; +@import '../../node_modules/nouislider/distribute/nouislider.min'; +@import './_custom_variables.scss'; +@import './bootstrap_variables_mapping.scss'; +@import './_global-styles.scss'; diff --git a/src/themes/custom/app/+home-page/home-news/home-news.component.html b/src/themes/custom/app/+home-page/home-news/home-news.component.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/+home-page/home-news/home-news.component.scss b/src/themes/custom/app/+home-page/home-news/home-news.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/+home-page/home-news/home-news.component.ts b/src/themes/custom/app/+home-page/home-news/home-news.component.ts new file mode 100644 index 00000000000..eaf788e5992 --- /dev/null +++ b/src/themes/custom/app/+home-page/home-news/home-news.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { HomeNewsComponent as BaseComponent } from '../../../../../app/+home-page/home-news/home-news.component'; + +@Component({ + selector: 'ds-home-news', + // styleUrls: ['./home-news.component.scss'], + styleUrls: ['../../../../../app/+home-page/home-news/home-news.component.scss'], + // templateUrl: './home-news.component.html' + templateUrl: '../../../../../app/+home-page/home-news/home-news.component.html' +}) + +/** + * Component to render the news section on the home page + */ +export class HomeNewsComponent extends BaseComponent {} + diff --git a/src/themes/custom/app/+home-page/home-page.component.html b/src/themes/custom/app/+home-page/home-page.component.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/+home-page/home-page.component.scss b/src/themes/custom/app/+home-page/home-page.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/+home-page/home-page.component.ts b/src/themes/custom/app/+home-page/home-page.component.ts new file mode 100644 index 00000000000..602492e9bda --- /dev/null +++ b/src/themes/custom/app/+home-page/home-page.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { HomePageComponent as BaseComponent } from '../../../../app/+home-page/home-page.component'; + +@Component({ + selector: 'ds-home-page', + // styleUrls: ['./home-page.component.scss'], + styleUrls: ['../../../../app/+home-page/home-page.component.scss'], + // templateUrl: './home-page.component.html' + templateUrl: '../../../../app/+home-page/home-page.component.html' +}) +export class HomePageComponent extends BaseComponent { + +} diff --git a/src/themes/custom/app/+item-page/simple/item-types/publication/publication.component.html b/src/themes/custom/app/+item-page/simple/item-types/publication/publication.component.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/+item-page/simple/item-types/publication/publication.component.scss b/src/themes/custom/app/+item-page/simple/item-types/publication/publication.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/+item-page/simple/item-types/publication/publication.component.ts b/src/themes/custom/app/+item-page/simple/item-types/publication/publication.component.ts new file mode 100644 index 00000000000..4eda82a10ae --- /dev/null +++ b/src/themes/custom/app/+item-page/simple/item-types/publication/publication.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ViewMode } from '../../../../../../../app/core/shared/view-mode.model'; +import { listableObjectComponent } from '../../../../../../../app/shared/object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../../../../app/core/shared/context.model'; +import { PublicationComponent as BaseComponent } from '../../../../../../../app/+item-page/simple/item-types/publication/publication.component'; + +/** + * Component that represents a publication Item page + */ + +@listableObjectComponent('Publication', ViewMode.StandalonePage, Context.Any, 'custom') +@Component({ + selector: 'ds-publication', + // styleUrls: ['./publication.component.scss'], + styleUrls: ['../../../../../../../app/+item-page/simple/item-types/publication/publication.component.scss'], + // templateUrl: './publication.component.html', + templateUrl: '../../../../../../../app/+item-page/simple/item-types/publication/publication.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PublicationComponent extends BaseComponent { + +} diff --git a/src/themes/custom/app/root/root.component.html b/src/themes/custom/app/root/root.component.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/root/root.component.scss b/src/themes/custom/app/root/root.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/root/root.component.ts b/src/themes/custom/app/root/root.component.ts new file mode 100644 index 00000000000..6b5b0c106fa --- /dev/null +++ b/src/themes/custom/app/root/root.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { slideSidebarPadding } from '../../../../app/shared/animations/slide'; +import { RootComponent as BaseComponent } from '../../../../app/root/root.component'; + +@Component({ + selector: 'ds-root', + // styleUrls: ['./root.component.scss'], + styleUrls: ['../../../../app/root/root.component.scss'], + // templateUrl: './root.component.html', + templateUrl: '../../../../app/root/root.component.html', + animations: [slideSidebarPadding], +}) +export class RootComponent extends BaseComponent { + +} diff --git a/src/themes/custom/entry-components.ts b/src/themes/custom/entry-components.ts new file mode 100644 index 00000000000..d859b563338 --- /dev/null +++ b/src/themes/custom/entry-components.ts @@ -0,0 +1,5 @@ +import { PublicationComponent } from './app/+item-page/simple/item-types/publication/publication.component'; + +export const ENTRY_COMPONENTS = [ + PublicationComponent +]; diff --git a/src/themes/custom/styles/_global-styles.scss b/src/themes/custom/styles/_global-styles.scss new file mode 100644 index 00000000000..3857f1ad0ac --- /dev/null +++ b/src/themes/custom/styles/_global-styles.scss @@ -0,0 +1,4 @@ +// Add any global css for the theme here + +// imports the base global style +@import '../../../styles/_global-styles.scss'; diff --git a/src/themes/custom/styles/_theme_css_variable_overrides.scss b/src/themes/custom/styles/_theme_css_variable_overrides.scss new file mode 100644 index 00000000000..0f06bf2daa4 --- /dev/null +++ b/src/themes/custom/styles/_theme_css_variable_overrides.scss @@ -0,0 +1,8 @@ +// Override or add CSS variables for your theme here + +:root { + //--ds-header-logo-height: 80px; + //--ds-header-logo-height-xs: 50px; + --ds-header-icon-color: #{$link-color}; + //--ds-header-icon-color-hover: #{darken($link-color, 15%)}; +} diff --git a/src/themes/custom/styles/_theme_sass_variable_overrides.scss b/src/themes/custom/styles/_theme_sass_variable_overrides.scss new file mode 100644 index 00000000000..1e5d0f0520c --- /dev/null +++ b/src/themes/custom/styles/_theme_sass_variable_overrides.scss @@ -0,0 +1,16 @@ +// DSpace works with CSS variables for its own components, and has a mapping of all bootstrap Sass +// variables to CSS equivalents (see src/styles/_bootstrap_variables_mapping.scss). However Bootstrap +// still uses Sass variables internally. So if you want to override bootstrap (or other sass +// variables) you can do so here. Their CSS counterparts will include the changes you make here + +// $blue: #007bff !default; +// $indigo: #6610f2 !default; +// $purple: #6f42c1 !default; +// $pink: #e83e8c !default; +// $red: #dc3545 !default; +// $orange: #fd7e14 !default; +// $yellow: #ffc107 !default; +// $green: #28a745 !default; +// $teal: #20c997 !default; +// $cyan: #17a2b8 !default; + diff --git a/src/themes/custom/styles/theme.scss b/src/themes/custom/styles/theme.scss new file mode 100644 index 00000000000..e4cc9e45ed6 --- /dev/null +++ b/src/themes/custom/styles/theme.scss @@ -0,0 +1,12 @@ +// This file combines the other scss files in to one. You usually shouldn't edit this file directly + +@import './_theme_sass_variable_overrides.scss'; +@import '../../../styles/_variables.scss'; +@import '../../../styles/_mixins.scss'; +@import '../../../styles/helpers/font_awesome_imports.scss'; +@import '../../../../node_modules/bootstrap/scss/bootstrap.scss'; +@import '../../../../node_modules/nouislider/distribute/nouislider.min'; +@import '../../../styles/_custom_variables.scss'; +@import './_theme_css_variable_overrides.scss'; +@import '../../../styles/bootstrap_variables_mapping.scss'; +@import './_global-styles.scss'; diff --git a/src/themes/custom/theme.module.ts b/src/themes/custom/theme.module.ts new file mode 100644 index 00000000000..1f451df1bca --- /dev/null +++ b/src/themes/custom/theme.module.ts @@ -0,0 +1,96 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AdminAccessControlModule } from '../../app/+admin/admin-access-control/admin-access-control.module'; +import { AdminRegistriesModule } from '../../app/+admin/admin-registries/admin-registries.module'; +import { AdminSearchModule } from '../../app/+admin/admin-search-page/admin-search.module'; +import { AdminWorkflowModuleModule } from '../../app/+admin/admin-workflow-page/admin-workflow.module'; +import { BitstreamFormatsModule } from '../../app/+admin/admin-registries/bitstream-formats/bitstream-formats.module'; +import { BrowseByModule } from '../../app/+browse-by/browse-by.module'; +import { CollectionFormModule } from '../../app/+collection-page/collection-form/collection-form.module'; +import { CommunityFormModule } from '../../app/+community-page/community-form/community-form.module'; +import { CoreModule } from '../../app/core/core.module'; +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { EditItemPageModule } from '../../app/+item-page/edit-item-page/edit-item-page.module'; +import { FormsModule } from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; +import { IdlePreloadModule } from 'angular-idle-preload'; +import { JournalEntitiesModule } from '../../app/entity-groups/journal-entities/journal-entities.module'; +import { MyDspaceSearchModule } from '../../app/+my-dspace-page/my-dspace-search.module'; +import { MenuModule } from '../../app/shared/menu/menu.module'; +import { NavbarModule } from '../../app/navbar/navbar.module'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ProfilePageModule } from '../../app/profile-page/profile-page.module'; +import { RegisterEmailFormModule } from '../../app/register-email-form/register-email-form.module'; +import { ResearchEntitiesModule } from '../../app/entity-groups/research-entities/research-entities.module'; +import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; +import { SearchPageModule } from '../../app/+search-page/search-page.module'; +import { SharedModule } from '../../app/shared/shared.module'; +import { StatisticsModule } from '../../app/statistics/statistics.module'; +import { StoreModule } from '@ngrx/store'; +import { StoreRouterConnectingModule } from '@ngrx/router-store'; +import { TranslateModule } from '@ngx-translate/core'; +import { HomeNewsComponent } from './app/+home-page/home-news/home-news.component'; +import { HomePageComponent } from './app/+home-page/home-page.component'; +import { HomePageModule } from '../../app/+home-page/home-page.module'; +import { RootComponent } from './app/root/root.component'; +import { AppModule } from '../../app/app.module'; +import { PublicationComponent } from './app/+item-page/simple/item-types/publication/publication.component'; +import { ItemPageModule } from '../../app/+item-page/item-page.module'; +import { RouterModule } from '@angular/router'; + +const DECLARATIONS = [ + HomePageComponent, + HomeNewsComponent, + RootComponent, + PublicationComponent +]; + +@NgModule({ + imports: [ + AdminAccessControlModule, + AdminRegistriesModule, + AdminSearchModule, + AdminWorkflowModuleModule, + AppModule, + BitstreamFormatsModule, + BrowseByModule, + CollectionFormModule, + CommonModule, + CommunityFormModule, + CoreModule, + DragDropModule, + ItemPageModule, + EditItemPageModule, + FormsModule, + HomePageModule, + HttpClientModule, + IdlePreloadModule, + JournalEntitiesModule, + MenuModule, + MyDspaceSearchModule, + NavbarModule, + NgbModule, + ProfilePageModule, + RegisterEmailFormModule, + ResearchEntitiesModule, + RouterModule, + ScrollToModule, + SearchPageModule, + SharedModule, + StatisticsModule, + StoreModule, + StoreRouterConnectingModule, + TranslateModule, + ], + declarations: DECLARATIONS +}) + +/** + * This module serves as an index for all the components in this theme. + * It should import all other modules, so the compiler knows where to find any components referenced + * from a component in this theme + * It is purposefully not exported, it should never be imported anywhere else, its only purpose is + * to give lazily loaded components a context in which they can be compiled successfully + */ +class ThemeModule { +} diff --git a/themes/mantis/app/+home-page/home-news/home-news.component.html b/src/themes/mantis/app/+home-page/home-news/home-news.component.html similarity index 100% rename from themes/mantis/app/+home-page/home-news/home-news.component.html rename to src/themes/mantis/app/+home-page/home-news/home-news.component.html diff --git a/src/themes/mantis/app/+home-page/home-news/home-news.component.scss b/src/themes/mantis/app/+home-page/home-news/home-news.component.scss new file mode 100644 index 00000000000..b82d84a71ec --- /dev/null +++ b/src/themes/mantis/app/+home-page/home-news/home-news.component.scss @@ -0,0 +1,17 @@ +@import 'src/app/+home-page/home-news/home-news.component.scss'; +:host { + --ds-home-news-link-color: #{$green}; + --ds-home-news-link-hover-color: #{darken($green, 15%)}; + + .jumbotron { + background-color: transparent; + } + + a { + color: var(--ds-home-news-link-color); + + @include hover { + color: var(--ds-home-news-link-hover-color); + } + } +} diff --git a/themes/mantis/app/+home-page/home-page.component.html b/src/themes/mantis/app/+home-page/home-page.component.html similarity index 100% rename from themes/mantis/app/+home-page/home-page.component.html rename to src/themes/mantis/app/+home-page/home-page.component.html diff --git a/themes/mantis/app/+home-page/home-page.component.scss b/src/themes/mantis/app/+home-page/home-page.component.scss similarity index 56% rename from themes/mantis/app/+home-page/home-page.component.scss rename to src/themes/mantis/app/+home-page/home-page.component.scss index 64bd786cd59..64d2ba66ca5 100644 --- a/themes/mantis/app/+home-page/home-page.component.scss +++ b/src/themes/mantis/app/+home-page/home-page.component.scss @@ -2,12 +2,12 @@ div.background-image { color: white; - background-color: $info; + background-color: var(--bs-info); position: relative; background-position-y: -200px; background-image: url('/assets/images/banner.jpg'); background-size: cover; - @media screen and (max-width: map-get($grid-breakpoints, lg)) { + @media screen and (max-width: var(--bs-lg)) { background-position-y: 0; } @@ -18,24 +18,24 @@ div.background-image { &:before, &:after { content: ''; display: block; - width: $banner-background-gradient-width; + width: var(--ds-banner-background-gradient-width); height: 100%; top: 0; position: absolute; } &:before { - background: linear-gradient(to left, $banner-text-background, transparent); - left: -$banner-background-gradient-width; + background: linear-gradient(to left, var(--ds-banner-text-background), transparent); + left: calc(-1 * var(--ds-banner-background-gradient-width)); } &:after { - background: linear-gradient(to right, $banner-text-background, transparent); - right: -$banner-background-gradient-width; + background: linear-gradient(to right, var(--ds-banner-text-background), transparent); + right: calc(-1 * var(--ds-banner-background-gradient-width)); } - background-color: $banner-text-background; + background-color: var(--ds-banner-text-background); } @@ -46,7 +46,7 @@ div.background-image { opacity: 0.3; position: absolute; - right: $spacer; + right: var(--bs-spacer); bottom: 0; } } diff --git a/themes/mantis/app/+item-page/simple/item-page.component.html b/src/themes/mantis/app/+item-page/simple/item-page.component.html similarity index 100% rename from themes/mantis/app/+item-page/simple/item-page.component.html rename to src/themes/mantis/app/+item-page/simple/item-page.component.html diff --git a/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html b/src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html similarity index 100% rename from themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html rename to src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html diff --git a/src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss b/src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss new file mode 100644 index 00000000000..501e2a75623 --- /dev/null +++ b/src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss @@ -0,0 +1,30 @@ +@import 'src/app/+item-page/simple/item-types/publication/publication.component.scss'; + +:host { + > * { + display: block; + padding-top: var(--ds-content-spacing); + padding-bottom: var(--ds-content-spacing); + } + + .top-item-page { + background-color: var(--bs-gray-100); + margin-top: calc(-1 * var(--ds-content-spacing)); + } + + .relationships-item-page { + padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer)); + } + + ds-metadata-field-wrapper { + @media screen and (max-width: var(--bs-md)) { + flex: 1; + padding-right: calc(var(--bs-spacer) / 2); + } + + ds-thumbnail { + display: block; + max-width: var(--ds-thumbnail-max-width); + } + } +} diff --git a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html similarity index 100% rename from themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html rename to src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html diff --git a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss new file mode 100644 index 00000000000..4e0acbeccb7 --- /dev/null +++ b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss @@ -0,0 +1,30 @@ +@import 'src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss'; + +:host { + > * { + display: block; + padding-top: var(--ds-content-spacing); + padding-bottom: var(--ds-content-spacing); + } + + .top-item-page { + background-color: var(--bs-gray-100); + margin-top: calc(-1 * var(--ds-content-spacing)); + } + + .relationships-item-page { + padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer)); + } + + ds-metadata-field-wrapper { + @media screen and (max-width: var(--bs-md)) { + flex: 1; + padding-right: calc(var(--bs-spacer) / 2); + } + + ds-thumbnail { + display: block; + max-width: var(--ds-thumbnail-max-width); + } + } +} diff --git a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html similarity index 100% rename from themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html rename to src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html diff --git a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss new file mode 100644 index 00000000000..8142b1d09dc --- /dev/null +++ b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss @@ -0,0 +1,30 @@ +@import 'src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss'; + +:host { + > * { + display: block; + padding-top: var(--ds-content-spacing); + padding-bottom: var(--ds-content-spacing); + } + + .top-item-page { + background-color: var(--bs-gray-100); + margin-top: calc(-1 * var(--ds-content-spacing)); + } + + .relationships-item-page { + padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer)); + } + + ds-metadata-field-wrapper { + @media screen and (max-width: var(--bs-md)) { + flex: 1; + padding-right: calc(var(--bs-spacer) / 2); + } + + ds-thumbnail { + display: block; + max-width: var(--ds-thumbnail-max-width); + } + } +} diff --git a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html similarity index 100% rename from themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html rename to src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html diff --git a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss new file mode 100644 index 00000000000..28664b17948 --- /dev/null +++ b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss @@ -0,0 +1,38 @@ +@import 'src/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss'; + +:host { + > * { + display: block; + padding-top: var(--ds-content-spacing); + padding-bottom: var(--ds-content-spacing); + } + + .top-item-page { + background-color: var(--bs-gray-100); + margin-top: calc(-1 * var(--ds-content-spacing)); + } + + .relationships-item-page { + padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer)); + } + + ds-metadata-field-wrapper { + @media screen and (max-width: var(--bs-md)) { + flex: 1; + padding-right: calc(var(--bs-spacer) / 2); + } + + ds-thumbnail { + display: block; + max-width: var(--ds-thumbnail-max-width); + } + } + + .search-container { + margin-bottom: var(--bs-spacer); + @media screen and (max-width: var(--bs-lg)) { + width: 100%; + max-width: none; + } + } +} diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html similarity index 100% rename from themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html rename to src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html diff --git a/src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss b/src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss new file mode 100644 index 00000000000..89c637760db --- /dev/null +++ b/src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss @@ -0,0 +1,30 @@ +@import 'src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss'; + +:host { + > * { + display: block; + padding-top: var(--ds-content-spacing); + padding-bottom: var(--ds-content-spacing); + } + + .top-item-page { + background-color: var(--bs-gray-100); + margin-top: calc(-1 * var(--ds-content-spacing)); + } + + .relationships-item-page { + padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer)); + } + + ds-metadata-field-wrapper { + @media screen and (max-width: var(--bs-md)) { + flex: 1; + padding-right: calc(var(--bs-spacer) / 2); + } + + ds-thumbnail { + display: block; + max-width: var(--ds-thumbnail-max-width); + } + } +} diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html similarity index 100% rename from themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html rename to src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html diff --git a/src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss b/src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss new file mode 100644 index 00000000000..c6049438dd6 --- /dev/null +++ b/src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss @@ -0,0 +1,38 @@ +@import 'src/app/entity-groups/research-entities/item-pages/person/person.component.scss'; + +:host { + > * { + display: block; + padding-top: var(--ds-content-spacing); + padding-bottom: var(--ds-content-spacing); + } + + .top-item-page { + background-color: var(--bs-gray-100); + margin-top: calc(-1 * var(--ds-content-spacing)); + } + + .relationships-item-page { + padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer)); + } + + ds-metadata-field-wrapper { + @media screen and (max-width: var(--bs-md)) { + flex: 1; + padding-right: calc(var(--bs-spacer) / 2); + } + + ds-thumbnail { + display: block; + max-width: var(--ds-thumbnail-max-width); + } + } + + .search-container { + margin-bottom: var(--bs-spacer); + @media screen and (max-width: var(--bs-lg)) { + width: 100%; + max-width: none; + } + } +} diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html b/src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html similarity index 100% rename from themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html rename to src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html diff --git a/src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss b/src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss new file mode 100644 index 00000000000..502b5102ca7 --- /dev/null +++ b/src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss @@ -0,0 +1,30 @@ +@import 'src/app/entity-groups/research-entities/item-pages/project/project.component.scss'; + +:host { + > * { + display: block; + padding-top: var(--ds-content-spacing); + padding-bottom: var(--ds-content-spacing); + } + + .top-item-page { + background-color: var(--bs-gray-100); + margin-top: calc(-1 * var(--ds-content-spacing)); + } + + .relationships-item-page { + padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer)); + } + + ds-metadata-field-wrapper { + @media screen and (max-width: var(--bs-md)) { + flex: 1; + padding-right: calc(var(--bs-spacer) / 2); + } + + ds-thumbnail { + display: block; + max-width: var(--ds-thumbnail-max-width); + } + } +} diff --git a/themes/mantis/app/navbar/navbar.component.html b/src/themes/mantis/app/navbar/navbar.component.html similarity index 100% rename from themes/mantis/app/navbar/navbar.component.html rename to src/themes/mantis/app/navbar/navbar.component.html diff --git a/themes/mantis/app/navbar/navbar.component.scss b/src/themes/mantis/app/navbar/navbar.component.scss similarity index 100% rename from themes/mantis/app/navbar/navbar.component.scss rename to src/themes/mantis/app/navbar/navbar.component.scss diff --git a/themes/mantis/app/shared/search-form/search-form.component.html b/src/themes/mantis/app/shared/search-form/search-form.component.html similarity index 100% rename from themes/mantis/app/shared/search-form/search-form.component.html rename to src/themes/mantis/app/shared/search-form/search-form.component.html diff --git a/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html similarity index 100% rename from themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html rename to src/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html diff --git a/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html similarity index 100% rename from themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html rename to src/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html diff --git a/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.html b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.html similarity index 100% rename from themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.html rename to src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.html diff --git a/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss similarity index 63% rename from themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss rename to src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss index 4d2d29ae41a..0e78c646296 100644 --- a/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss +++ b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss @@ -1,8 +1,8 @@ @import 'src/app/shared/search/search-filters/search-filter/search-filter.component.scss'; .facet-filter { - background-color: map-get($theme-colors, light); - border-radius: $border-radius; + background-color: var(--bs-light); + border-radius: var(--bs-border-radius); h5 { font-size: 1.1rem diff --git a/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss similarity index 82% rename from themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss rename to src/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss index 7edcb8f063f..158a0d3b4e2 100644 --- a/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss +++ b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss @@ -1,5 +1,5 @@ @import 'src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss'; ::ng-deep .noUi-connect { - background: $info; + background: var(--bs-info); } diff --git a/themes/mantis/app/shared/search/search-filters/search-filters.component.html b/src/themes/mantis/app/shared/search/search-filters/search-filters.component.html similarity index 100% rename from themes/mantis/app/shared/search/search-filters/search-filters.component.html rename to src/themes/mantis/app/shared/search/search-filters/search-filters.component.html diff --git a/themes/mantis/app/shared/search/search-settings/search-settings.component.html b/src/themes/mantis/app/shared/search/search-settings/search-settings.component.html similarity index 100% rename from themes/mantis/app/shared/search/search-settings/search-settings.component.html rename to src/themes/mantis/app/shared/search/search-settings/search-settings.component.html diff --git a/themes/mantis/app/shared/search/search-settings/search-settings.component.scss b/src/themes/mantis/app/shared/search/search-settings/search-settings.component.scss similarity index 61% rename from themes/mantis/app/shared/search/search-settings/search-settings.component.scss rename to src/themes/mantis/app/shared/search/search-settings/search-settings.component.scss index 073039dae85..b3ee0ba60e8 100644 --- a/themes/mantis/app/shared/search/search-settings/search-settings.component.scss +++ b/src/themes/mantis/app/shared/search/search-settings/search-settings.component.scss @@ -1,8 +1,8 @@ @import 'src/app/shared/search/search-settings/search-settings.component.scss'; .setting-option { - background-color: map-get($theme-colors, light); - border-radius: $border-radius; + background-color: var(--bs-light); + border-radius: var(--bs-border-radius); h5 { font-size: 1.1rem } diff --git a/src/themes/mantis/readme.md b/src/themes/mantis/readme.md new file mode 100644 index 00000000000..93bdf36f47d --- /dev/null +++ b/src/themes/mantis/readme.md @@ -0,0 +1,2 @@ +#Note +For now the existing mantis theme has only been moved to the new themes folder, it has not yet been adapted to work as a dynamic theme. \ No newline at end of file diff --git a/themes/mantis/styles/_themed_bootstrap_variables.scss b/src/themes/mantis/styles/_themed_bootstrap_variables.scss similarity index 100% rename from themes/mantis/styles/_themed_bootstrap_variables.scss rename to src/themes/mantis/styles/_themed_bootstrap_variables.scss diff --git a/src/themes/mantis/styles/_themed_custom_variables.scss b/src/themes/mantis/styles/_themed_custom_variables.scss new file mode 100644 index 00000000000..60d248c4aee --- /dev/null +++ b/src/themes/mantis/styles/_themed_custom_variables.scss @@ -0,0 +1,4 @@ +:root { + --ds-banner-text-background: rgba(0, 0, 0, 0.35); + --ds-banner-background-gradient-width: 300px; +} diff --git a/src/themes/themed-entry-component.module.ts b/src/themes/themed-entry-component.module.ts new file mode 100644 index 00000000000..41cdf622692 --- /dev/null +++ b/src/themes/themed-entry-component.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { ENTRY_COMPONENTS as CUSTOM } from './custom/entry-components'; + +const ENTRY_COMPONENTS = [ + ...CUSTOM, +]; + + +/** + * This module only serves to ensure themed entry components are discoverable. It's kept separate + * from the theme modules to ensure only the minimal number of theme components is loaded ahead of + * time + */ +@NgModule() +export class ThemedEntryComponentModule { + static withEntryComponents() { + return { + ngModule: ThemedEntryComponentModule, + providers: ENTRY_COMPONENTS.map((component) => ({provide: component})) + }; + } + +} diff --git a/themes/mantis/app/+home-page/home-news/home-news.component.scss b/themes/mantis/app/+home-page/home-news/home-news.component.scss deleted file mode 100644 index c693e9a4938..00000000000 --- a/themes/mantis/app/+home-page/home-news/home-news.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -@import 'src/app/+home-page/home-news/home-news.component.scss'; -$home-news-link-color: $green !default; -$home-news-link-color: darken($home-news-link-color, 15%) !default; - -.jumbotron { - background-color: transparent; -} - -a { - color: $home-news-link-color; - - @include hover { - color: $home-news-link-color; - } -} diff --git a/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss b/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss deleted file mode 100644 index f65d9a00d52..00000000000 --- a/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss +++ /dev/null @@ -1,30 +0,0 @@ -@import 'src/app/+item-page/simple/item-types/publication/publication.component.scss'; - -:host { - > * { - display: block; - padding-top: $content-spacing; - padding-bottom: $content-spacing; - } - - .top-item-page { - background-color: $gray-100; - margin-top: -$content-spacing; - } - - .relationships-item-page { - padding-bottom: $content-spacing - $spacer; - } - - ds-metadata-field-wrapper { - @media screen and (max-width: map-get($grid-breakpoints, md)) { - flex: 1; - padding-right: $spacer/2; - } - - ds-thumbnail { - display: block; - max-width: $thumbnail-max-width; - } - } -} diff --git a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss deleted file mode 100644 index 3caa55f5335..00000000000 --- a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss +++ /dev/null @@ -1,30 +0,0 @@ -@import 'src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss'; - -:host { - > * { - display: block; - padding-top: $content-spacing; - padding-bottom: $content-spacing; - } - - .top-item-page { - background-color: $gray-100; - margin-top: -$content-spacing; - } - - .relationships-item-page { - padding-bottom: $content-spacing - $spacer; - } - - ds-metadata-field-wrapper { - @media screen and (max-width: map-get($grid-breakpoints, md)) { - flex: 1; - padding-right: $spacer/2; - } - - ds-thumbnail { - display: block; - max-width: $thumbnail-max-width; - } - } -} diff --git a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss deleted file mode 100644 index 5c2534b3182..00000000000 --- a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss +++ /dev/null @@ -1,30 +0,0 @@ -@import 'src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss'; - -:host { - > * { - display: block; - padding-top: $content-spacing; - padding-bottom: $content-spacing; - } - - .top-item-page { - background-color: $gray-100; - margin-top: -$content-spacing; - } - - .relationships-item-page { - padding-bottom: $content-spacing - $spacer; - } - - ds-metadata-field-wrapper { - @media screen and (max-width: map-get($grid-breakpoints, md)) { - flex: 1; - padding-right: $spacer/2; - } - - ds-thumbnail { - display: block; - max-width: $thumbnail-max-width; - } - } -} diff --git a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss deleted file mode 100644 index 5c0d1c44b82..00000000000 --- a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss +++ /dev/null @@ -1,38 +0,0 @@ -@import 'src/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss'; - -:host { - > * { - display: block; - padding-top: $content-spacing; - padding-bottom: $content-spacing; - } - - .top-item-page { - background-color: $gray-100; - margin-top: -$content-spacing; - } - - .relationships-item-page { - padding-bottom: $content-spacing - $spacer; - } - - ds-metadata-field-wrapper { - @media screen and (max-width: map-get($grid-breakpoints, md)) { - flex: 1; - padding-right: $spacer/2; - } - - ds-thumbnail { - display: block; - max-width: $thumbnail-max-width; - } - } - - .search-container { - margin-bottom: $spacer; - @media screen and (max-width: map-get($grid-breakpoints, lg)) { - width: 100%; - max-width: none; - } - } -} diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss b/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss deleted file mode 100644 index 4a1d2516da0..00000000000 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss +++ /dev/null @@ -1,30 +0,0 @@ -@import 'src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss'; - -:host { - > * { - display: block; - padding-top: $content-spacing; - padding-bottom: $content-spacing; - } - - .top-item-page { - background-color: $gray-100; - margin-top: -$content-spacing; - } - - .relationships-item-page { - padding-bottom: $content-spacing - $spacer; - } - - ds-metadata-field-wrapper { - @media screen and (max-width: map-get($grid-breakpoints, md)) { - flex: 1; - padding-right: $spacer/2; - } - - ds-thumbnail { - display: block; - max-width: $thumbnail-max-width; - } - } -} diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss b/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss deleted file mode 100644 index 48571b05b23..00000000000 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss +++ /dev/null @@ -1,38 +0,0 @@ -@import 'src/app/entity-groups/research-entities/item-pages/person/person.component.scss'; - -:host { - > * { - display: block; - padding-top: $content-spacing; - padding-bottom: $content-spacing; - } - - .top-item-page { - background-color: $gray-100; - margin-top: -$content-spacing; - } - - .relationships-item-page { - padding-bottom: $content-spacing - $spacer; - } - - ds-metadata-field-wrapper { - @media screen and (max-width: map-get($grid-breakpoints, md)) { - flex: 1; - padding-right: $spacer/2; - } - - ds-thumbnail { - display: block; - max-width: $thumbnail-max-width; - } - } - - .search-container { - margin-bottom: $spacer; - @media screen and (max-width: map-get($grid-breakpoints, lg)) { - width: 100%; - max-width: none; - } - } -} diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss b/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss deleted file mode 100644 index d2707d30ccb..00000000000 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss +++ /dev/null @@ -1,30 +0,0 @@ -@import 'src/app/entity-groups/research-entities/item-pages/project/project.component.scss'; - -:host { - > * { - display: block; - padding-top: $content-spacing; - padding-bottom: $content-spacing; - } - - .top-item-page { - background-color: $gray-100; - margin-top: -$content-spacing; - } - - .relationships-item-page { - padding-bottom: $content-spacing - $spacer; - } - - ds-metadata-field-wrapper { - @media screen and (max-width: map-get($grid-breakpoints, md)) { - flex: 1; - padding-right: $spacer/2; - } - - ds-thumbnail { - display: block; - max-width: $thumbnail-max-width; - } - } -} diff --git a/themes/mantis/styles/_themed_custom_variables.scss b/themes/mantis/styles/_themed_custom_variables.scss deleted file mode 100644 index 1be25e953f2..00000000000 --- a/themes/mantis/styles/_themed_custom_variables.scss +++ /dev/null @@ -1,2 +0,0 @@ -$banner-text-background: rgba(0, 0, 0, 0.35); -$banner-background-gradient-width: 300px; diff --git a/tsconfig.app.json b/tsconfig.app.json index 097a41d2b67..8700bbe0b3f 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -9,7 +9,8 @@ "src/polyfills.ts" ], "include": [ - "src/**/*.d.ts" + "src/**/*.d.ts", + "src/themes/**/*.module.ts" ], "exclude": [ "src/test.ts", diff --git a/webpack/helpers.ts b/webpack/helpers.ts index 66580832acf..532b170bf3a 100644 --- a/webpack/helpers.ts +++ b/webpack/helpers.ts @@ -1,79 +1,18 @@ const path = require('path'); -const fs = require('fs'); -const environment = require('../src/environments/environment.ts').environment; export const projectRoot = (relativePath) => { return path.resolve(__dirname, '..', relativePath); }; -export const srcPath = projectRoot('src'); - -export const buildRoot = (relativePath) => { - if (environment.aot) { - return path.resolve(projectRoot('./build'), relativePath); - } else { - return path.resolve(projectRoot('src'), relativePath); - } -}; - -export const theme = environment.theme.name; - -export let themePath; - -if (theme !== null && theme !== undefined) { - themePath = path.normalize(path.join(__dirname, '..', 'themes', theme)); -} else { - themePath = srcPath; -} - export const globalCSSImports = () => { return [ - buildRoot('styles/_variables.scss'), - buildRoot('styles/_mixins.scss'), + projectRoot('src/styles/_variables.scss'), + projectRoot('src/styles/_mixins.scss'), ]; }; -const getThemedPath = (componentPath, ext) => { - const parsedPath = path.parse(componentPath); - const relativePath = path.relative(srcPath, parsedPath.dir); - return path.join(themePath, relativePath, `${parsedPath.name}.${ext}`); -}; - -export const themedTest = (origPath, extension) => { - if (/\.component.ts$/.test(origPath)) { // only match components - const themedPath = getThemedPath(origPath, extension); - return fs.existsSync(themedPath); - } else { - return false; - } -}; - -export const themedUse = (resource, extension) => { - const origPath = path.parse(resource); - let themedPath = getThemedPath(resource, extension); - - /* Make sure backslashes are escaped twice, because the replace unescapes those again */ - themedPath = themedPath.replace(/\\/g, '\\\\'); - - return [ - { - loader: 'string-replace-loader', - options: { - search: `\.\/${origPath.name}\.${extension}`, - replace: themedPath, - flags: 'g' - } - } - ]; -}; module.exports = { projectRoot, - buildRoot, - theme: theme, - themePath, - getThemedPath, - themedTest, - themedUse, globalCSSImports }; diff --git a/webpack/webpack.common.ts b/webpack/webpack.common.ts index 48fb0808bcc..f0179ad2ea3 100644 --- a/webpack/webpack.common.ts +++ b/webpack/webpack.common.ts @@ -1,4 +1,4 @@ -import { buildRoot, globalCSSImports, projectRoot, themedTest, themedUse, themePath } from './helpers'; +import { globalCSSImports, projectRoot } from './helpers'; const CopyWebpackPlugin = require('copy-webpack-plugin'); const path = require('path'); @@ -26,11 +26,27 @@ export const copyWebpackOptions = [ } ]; +const SCSS_LOADERS = [{ + loader: 'postcss-loader', + options: { + sourceMap: true + } +}, + { + loader: 'sass-loader', + options: { + sourceMap: true, + sassOptions: { + includePaths: [projectRoot('./')] + } + } + },] + export const commonExports = { plugins: [ new CopyWebpackPlugin(copyWebpackOptions), new HtmlWebpackPlugin({ - template: buildRoot('./index.html', ), + template: projectRoot('./src/index.html', ), output: projectRoot('dist'), inject: 'head' }), @@ -40,14 +56,6 @@ export const commonExports = { ], module: { rules: [ - { - test: (filePath) => themedTest(filePath, 'scss'), - use: (info) => themedUse(info.resource, 'scss') - }, - { - test: (filePath) => themedTest(filePath, 'html'), - use: (info) => themedUse(info.resource, 'html') - }, { test: /\.ts$/, loader: '@ngtools/webpack' @@ -56,19 +64,10 @@ export const commonExports = { test: /\.scss$/, exclude: [ /node_modules/, - buildRoot('styles/_exposed_variables.scss'), - buildRoot('styles/_variables.scss') + /(_exposed)?_variables.scss$|\/src\/themes\/[^/]+\/styles\/.+.scss$/ ], use: [ - { - loader: 'sass-loader', - options: { - sourceMap: true, - sassOptions: { - includePaths: [projectRoot('./'), path.join(themePath, 'styles')] - } - } - }, + ...SCSS_LOADERS, { loader: 'sass-resources-loader', options: { @@ -78,24 +77,10 @@ export const commonExports = { ] }, { - test: /(_exposed)?_variables.scss$/, + test: /(_exposed)?_variables.scss$|\/src\/themes\/[^/]+\/styles\/.+.scss$/, exclude: [/node_modules/], use: [ - { - loader: 'postcss-loader', - options: { - sourceMap: true - } - }, - { - loader: 'sass-loader', - options: { - sourceMap: true, - sassOptions: { - includePaths: [projectRoot('./'), path.join(themePath, 'styles')] - } - } - } + ...SCSS_LOADERS, ] }, ], diff --git a/webpack/webpack.prod.ts b/webpack/webpack.prod.ts index e9fba2e65ef..40e551610b8 100644 --- a/webpack/webpack.prod.ts +++ b/webpack/webpack.prod.ts @@ -1,5 +1,5 @@ import { commonExports } from './webpack.common'; -import { buildRoot, projectRoot } from './helpers'; +import { projectRoot } from './helpers'; const webpack = require('webpack'); const nodeExternals = require('webpack-node-externals'); @@ -16,7 +16,7 @@ module.exports = Object.assign({}, commonExports, { ], mode: 'production', recordsOutputPath: projectRoot('webpack.records.json'), - entry: buildRoot('./main.server.ts'), + entry: projectRoot('./src/main.server.ts'), target: 'node', externals: [nodeExternals({ whitelist: [ diff --git a/yarn.lock b/yarn.lock index 29a03624ab0..e4356157b7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -252,9 +252,9 @@ tslib "^2.0.0" "@angular/language-service@~10.2.3": - version "10.2.3" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-10.2.3.tgz#bb678c11822b9bf7ca007703a31dae090dc2c0ab" - integrity sha512-8rtNG3HjBdUMlKcakh6gDfFvYSS5X16ymbVR0i2L/Nc4d9HuqgKrIrsNY4We/jSBoAjo/CGS8AvbscMa8oW4Eg== + version "10.2.4" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-10.2.4.tgz#0c533149c110445ae67a7fd833c5aa03182fcec0" + integrity sha512-WmaX2lst7LOPVdCLdgR2Yddvy+DrQ5fhaXexZ1mYFnWBwW8gDXcRP+sHAkvTu752mF5c0C4OEMR9UJdwrHntrg== "@angular/localize@10.2.3": version "10.2.3" @@ -1373,6 +1373,11 @@ dependencies: mkdirp "^1.0.4" +"@polka/url@^1.0.0-next.9": + version "1.0.0-next.11" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71" + integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA== + "@scarf/scarf@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.1.0.tgz#b84b4a91cd938a688d36245b7a7db6fbc476a499" @@ -1840,6 +1845,11 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3" + integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A== + acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" @@ -1850,6 +1860,11 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.0.4: + version "8.0.5" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.0.5.tgz#a3bfb872a74a6a7f661bc81b9849d9cac12601b7" + integrity sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg== + adjust-sourcemap-loader@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz#5ae12fb5b7b1c585e80bbb5a63ec163a1a45e61e" @@ -2371,16 +2386,6 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" -bfj@^6.1.1: - version "6.1.2" - resolved "https://registry.yarnpkg.com/bfj/-/bfj-6.1.2.tgz#325c861a822bcb358a41c78a33b8e6e2086dde7f" - integrity sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw== - dependencies: - bluebird "^3.5.5" - check-types "^8.0.3" - hoopy "^0.1.4" - tryer "^1.0.1" - big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" @@ -2958,11 +2963,6 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= -check-types@^8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552" - integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ== - "chokidar@>=2.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.4.1, chokidar@^3.4.2: version "3.4.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" @@ -3237,7 +3237,7 @@ command-line-usage@^6.1.0: table-layout "^1.0.1" typical "^5.2.0" -commander@^2.11.0, commander@^2.12.1, commander@^2.18.0, commander@^2.2.0, commander@^2.20.0: +commander@^2.11.0, commander@^2.12.1, commander@^2.2.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -4320,7 +4320,7 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -duplexer@^0.1.1: +duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== @@ -4362,11 +4362,6 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -ejs@^2.6.1: - version "2.7.4" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" - integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== - electron-to-chromium@^1.3.621: version "1.3.622" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.622.tgz#9726bd2e67a5462154750ce9701ca6af07d07877" @@ -4801,7 +4796,7 @@ express-rate-limit@^5.1.3: resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.3.tgz#ae73b3dc723decd697797611bd96e9b34a912f6c" integrity sha512-cjQH+oDrEPXxc569XvxhHC6QXqJiuBT6BhZ70X3bdAImcnHnTNMVuMAJaT0TXPoRiEErUrVPRcOTpZpM36VbOQ== -express@^4.16.3, express@^4.17.1: +express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== @@ -4985,11 +4980,6 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== -filesize@^3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" - integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg== - filesize@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00" @@ -5448,13 +5438,12 @@ guess-parser@^0.4.12: dependencies: "@wessberg/ts-evaluator" "0.0.27" -gzip-size@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" - integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== dependencies: - duplexer "^0.1.1" - pify "^4.0.1" + duplexer "^0.1.2" handle-thing@^2.0.0: version "2.0.1" @@ -5587,11 +5576,6 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoopy@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" - integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== - hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" @@ -7389,6 +7373,11 @@ mime@1.6.0, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^2.3.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.0.tgz#2b4af934401779806ee98026bb42e8c1ae1876b1" + integrity sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag== + mime@^2.4.4, mime@^2.4.5: version "2.4.6" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" @@ -8083,7 +8072,7 @@ opencollective-postinstall@^2.0.2: resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== -opener@^1.5.1: +opener@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== @@ -10425,6 +10414,15 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sirv@^1.0.7: + version "1.0.11" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.11.tgz#81c19a29202048507d6ec0d8ba8910fda52eb5a4" + integrity sha512-SR36i3/LSWja7AJNRBz4fF/Xjpn7lQFI30tZ434dIy+bitLYSP+ZEenHg36i23V2SGEz+kqjksg0uOGZ5LPiqg== + dependencies: + "@polka/url" "^1.0.0-next.9" + mime "^2.3.1" + totalist "^1.0.0" + sisteransi@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -11370,6 +11368,11 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +totalist@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" + integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== + touch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" @@ -11406,11 +11409,6 @@ tree-kill@1.2.2, tree-kill@^1.2.1: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -tryer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" - integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== - ts-loader@^5.2.0: version "5.4.5" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-5.4.5.tgz#a0c1f034b017a9344cef0961bfd97cc192492b8b" @@ -11971,24 +11969,20 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== -webpack-bundle-analyzer@^3.3.2: - version "3.9.0" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.0.tgz#f6f94db108fb574e415ad313de41a2707d33ef3c" - integrity sha512-Ob8amZfCm3rMB1ScjQVlbYYUEJyEjdEtQ92jqiFUYt5VkEeO2v5UMbv49P/gnmCZm3A6yaFQzCBvpZqN4MUsdA== +webpack-bundle-analyzer@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz#74013106e7e2b07cbd64f3a5ae847f7e814802c7" + integrity sha512-9DhNa+aXpqdHk8LkLPTBU/dMfl84Y+WE2+KnfI6rSpNRNVKa0VGLjPd2pjFubDeqnWmulFggxmWBxhfJXZnR0g== dependencies: - acorn "^7.1.1" - acorn-walk "^7.1.1" - bfj "^6.1.1" - chalk "^2.4.1" - commander "^2.18.0" - ejs "^2.6.1" - express "^4.16.3" - filesize "^3.6.1" - gzip-size "^5.0.0" - lodash "^4.17.19" - mkdirp "^0.5.1" - opener "^1.5.1" - ws "^6.0.0" + acorn "^8.0.4" + acorn-walk "^8.0.0" + chalk "^4.1.0" + commander "^6.2.0" + gzip-size "^6.0.0" + lodash "^4.17.20" + opener "^1.5.2" + sirv "^1.0.7" + ws "^7.3.1" webpack-cli@^4.2.0: version "4.2.0" @@ -12294,7 +12288,7 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^6.0.0, ws@^6.2.1: +ws@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== @@ -12306,6 +12300,11 @@ ws@^7.1.2, ws@^7.2.3: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.1.tgz#a333be02696bd0e54cea0434e21dcc8a9ac294bb" integrity sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ== +ws@^7.3.1: + version "7.4.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" + integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== + ws@~3.3.1: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"