mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'origin/main' into CST-5535
# Conflicts: # src/app/admin/admin-sidebar/admin-sidebar.component.ts
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -37,3 +37,5 @@ package-lock.json
|
|||||||
|
|
||||||
.env
|
.env
|
||||||
/nbproject/
|
/nbproject/
|
||||||
|
|
||||||
|
junit.xml
|
||||||
|
@@ -330,8 +330,11 @@ All E2E tests must be created under the `./cypress/integration/` folder, and mus
|
|||||||
* In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_.
|
* In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_.
|
||||||
* From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page.
|
* From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page.
|
||||||
* Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector
|
* Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector
|
||||||
|
* It's generally best not to rely on attributes like `class` and `id` in tests, as those are likely to change later on. Instead, you can add a `data-test` attribute to makes it clear that it's required for a test.
|
||||||
* Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc.
|
* Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc.
|
||||||
* Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions.
|
* When running with server-side rendering enabled, the client first receives HTML without the JS; only once the page is rendered client-side do some elements (e.g. a button that toggles a Bootstrap dropdown) become fully interactive. This can trip up Cypress in some cases as it may try to `click` or `type` in an element that's not fully loaded yet, causing tests to fail.
|
||||||
|
* To work around this issue, define the attributes you use for Cypress selectors as `[attr.data-test]="'button' | ngBrowserOnly"`. This will only show the attribute in CSR HTML, forcing Cypress to wait until CSR is complete before interacting with the element.
|
||||||
|
* Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions.
|
||||||
* Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly.
|
* Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly.
|
||||||
* Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests.
|
* Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests.
|
||||||
|
|
||||||
|
@@ -65,7 +65,7 @@ describe('My DSpace page', () => {
|
|||||||
cy.visit('/mydspace');
|
cy.visit('/mydspace');
|
||||||
|
|
||||||
// Open the New Submission dropdown
|
// Open the New Submission dropdown
|
||||||
cy.get('#dropdownSubmission').click();
|
cy.get('button[data-test="submission-dropdown"]').click();
|
||||||
// Click on the "Item" type in that dropdown
|
// Click on the "Item" type in that dropdown
|
||||||
cy.get('#entityControlsDropdownMenu button[title="none"]').click();
|
cy.get('#entityControlsDropdownMenu button[title="none"]').click();
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ describe('My DSpace page', () => {
|
|||||||
const id = subpaths[2];
|
const id = subpaths[2];
|
||||||
|
|
||||||
// Click the "Save for Later" button to save this submission
|
// Click the "Save for Later" button to save this submission
|
||||||
cy.get('button#saveForLater').click();
|
cy.get('ds-submission-form-footer [data-test="save-for-later"]').click();
|
||||||
|
|
||||||
// "Save for Later" should send us to MyDSpace
|
// "Save for Later" should send us to MyDSpace
|
||||||
cy.url().should('include', '/mydspace');
|
cy.url().should('include', '/mydspace');
|
||||||
@@ -122,7 +122,7 @@ describe('My DSpace page', () => {
|
|||||||
cy.url().should('include', '/workspaceitems/' + id + '/edit');
|
cy.url().should('include', '/workspaceitems/' + id + '/edit');
|
||||||
|
|
||||||
// Discard our new submission by clicking Discard in Submission form & confirming
|
// Discard our new submission by clicking Discard in Submission form & confirming
|
||||||
cy.get('button#discard').click();
|
cy.get('ds-submission-form-footer [data-test="discard"]').click();
|
||||||
cy.get('button#discard_submit').click();
|
cy.get('button#discard_submit').click();
|
||||||
|
|
||||||
// Discarding should send us back to MyDSpace
|
// Discarding should send us back to MyDSpace
|
||||||
@@ -135,7 +135,7 @@ describe('My DSpace page', () => {
|
|||||||
cy.visit('/mydspace');
|
cy.visit('/mydspace');
|
||||||
|
|
||||||
// Open the New Import dropdown
|
// Open the New Import dropdown
|
||||||
cy.get('#dropdownImport').click();
|
cy.get('button[data-test="import-dropdown"]').click();
|
||||||
// Click on the "Item" type in that dropdown
|
// Click on the "Item" type in that dropdown
|
||||||
cy.get('#importControlsDropdownMenu button[title="none"]').click();
|
cy.get('#importControlsDropdownMenu button[title="none"]').click();
|
||||||
|
|
||||||
|
@@ -24,7 +24,7 @@ describe('Search Page', () => {
|
|||||||
|
|
||||||
// Click each filter toggle to open *every* filter
|
// Click each filter toggle to open *every* filter
|
||||||
// (As we want to scan filter section for accessibility issues as well)
|
// (As we want to scan filter section for accessibility issues as well)
|
||||||
cy.get('.filter-toggle').click({ multiple: true });
|
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
|
||||||
|
|
||||||
// Analyze <ds-search-page> for accessibility issues
|
// Analyze <ds-search-page> for accessibility issues
|
||||||
testA11y(
|
testA11y(
|
||||||
|
@@ -21,7 +21,7 @@ import './commands';
|
|||||||
import 'cypress-axe';
|
import 'cypress-axe';
|
||||||
|
|
||||||
// Runs once before the first test in each "block"
|
// Runs once before the first test in each "block"
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
|
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
|
||||||
// This just ensures it doesn't get in the way of matching other objects in the page.
|
// This just ensures it doesn't get in the way of matching other objects in the page.
|
||||||
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}');
|
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}');
|
||||||
|
@@ -155,7 +155,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "5.11.0",
|
"@typescript-eslint/eslint-plugin": "5.11.0",
|
||||||
"@typescript-eslint/parser": "5.11.0",
|
"@typescript-eslint/parser": "5.11.0",
|
||||||
"axe-core": "^4.3.3",
|
"axe-core": "^4.3.3",
|
||||||
"compression-webpack-plugin": "^3.0.1",
|
"compression-webpack-plugin": "^9.2.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^6.2.0",
|
"css-loader": "^6.2.0",
|
||||||
@@ -171,6 +171,7 @@
|
|||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"eslint-plugin-jsdoc": "^38.0.6",
|
"eslint-plugin-jsdoc": "^38.0.6",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
|
"express-static-gzip": "^2.1.5",
|
||||||
"fork-ts-checker-webpack-plugin": "^6.0.3",
|
"fork-ts-checker-webpack-plugin": "^6.0.3",
|
||||||
"html-loader": "^1.3.2",
|
"html-loader": "^1.3.2",
|
||||||
"jasmine-core": "^3.8.0",
|
"jasmine-core": "^3.8.0",
|
||||||
|
18
server.ts
18
server.ts
@@ -26,6 +26,7 @@ import * as morgan from 'morgan';
|
|||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as bodyParser from 'body-parser';
|
import * as bodyParser from 'body-parser';
|
||||||
import * as compression from 'compression';
|
import * as compression from 'compression';
|
||||||
|
import * as expressStaticGzip from 'express-static-gzip';
|
||||||
|
|
||||||
import { existsSync, readFileSync } from 'fs';
|
import { existsSync, readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
@@ -75,11 +76,15 @@ export function app() {
|
|||||||
/*
|
/*
|
||||||
* If production mode is enabled in the environment file:
|
* If production mode is enabled in the environment file:
|
||||||
* - Enable Angular's production mode
|
* - Enable Angular's production mode
|
||||||
* - Enable compression for response bodies. See [compression](https://github.com/expressjs/compression)
|
* - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression)
|
||||||
*/
|
*/
|
||||||
if (environment.production) {
|
if (environment.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
server.use(compression());
|
server.use(compression({
|
||||||
|
// only compress responses we've marked as SSR
|
||||||
|
// otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin
|
||||||
|
filter: (_, res) => res.locals.ssr,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -151,8 +156,14 @@ export function app() {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Serve static resources (images, i18n messages, …)
|
* Serve static resources (images, i18n messages, …)
|
||||||
|
* Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip)
|
||||||
*/
|
*/
|
||||||
server.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false }));
|
server.get('*.*', cacheControl, expressStaticGzip(DIST_FOLDER, {
|
||||||
|
index: false,
|
||||||
|
enableBrotli: true,
|
||||||
|
orderPreference: ['br', 'gzip'],
|
||||||
|
}));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Fallthrough to the IIIF viewer (must be included in the build).
|
* Fallthrough to the IIIF viewer (must be included in the build).
|
||||||
*/
|
*/
|
||||||
@@ -186,6 +197,7 @@ function ngApp(req, res) {
|
|||||||
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
|
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
|
||||||
}, (err, data) => {
|
}, (err, data) => {
|
||||||
if (hasNoValue(err) && hasValue(data)) {
|
if (hasNoValue(err) && hasValue(data)) {
|
||||||
|
res.locals.ssr = true; // mark response as SSR
|
||||||
res.send(data);
|
res.send(data);
|
||||||
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
||||||
// When this error occurs we can't fall back to CSR because the response has already been
|
// When this error occurs we can't fall back to CSR because the response has already been
|
||||||
|
@@ -182,176 +182,4 @@ describe('AdminSidebarComponent', () => {
|
|||||||
expect(menuService.collapseMenuPreview).toHaveBeenCalled();
|
expect(menuService.collapseMenuPreview).toHaveBeenCalled();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('menu', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(menuService, 'addSection');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for regular user', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake(() => {
|
|
||||||
return observableOf(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.createMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show site admin section', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'admin_search', visible: false,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'registries', visible: false,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
parentID: 'registries', visible: false,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'curation_tasks', visible: false,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'workflow', visible: false,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show edit_community', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'edit_community', visible: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show edit_collection', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'edit_collection', visible: false,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show access control section', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'access_control', visible: false,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
parentID: 'access_control', visible: false,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// We check that the menu section has not been called with visible set to true
|
|
||||||
// The reason why we don't check if it has been called with visible set to false
|
|
||||||
// Is because the function does not get called unless a user is authorised
|
|
||||||
it('should not show the import section', () => {
|
|
||||||
expect(menuService.addSection).not.toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'import', visible: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// We check that the menu section has not been called with visible set to true
|
|
||||||
// The reason why we don't check if it has been called with visible set to false
|
|
||||||
// Is because the function does not get called unless a user is authorised
|
|
||||||
it('should not show the export section', () => {
|
|
||||||
expect(menuService.addSection).not.toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'export', visible: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for site admin', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
|
||||||
return observableOf(featureID === FeatureID.AdministratorOf);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.createMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain site admin section', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'admin_search', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'registries', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
parentID: 'registries', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'curation_tasks', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'workflow', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'workflow', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'import', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'export', visible: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for community admin', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
|
||||||
return observableOf(featureID === FeatureID.IsCommunityAdmin);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.createMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show edit_community', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'edit_community', visible: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for collection admin', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
|
||||||
return observableOf(featureID === FeatureID.IsCollectionAdmin);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.createMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show edit_collection', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'edit_collection', visible: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for group admin', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
|
||||||
return observableOf(featureID === FeatureID.CanManageGroups);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.createMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show access control section', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'access_control', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
parentID: 'access_control', visible: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -1,45 +1,13 @@
|
|||||||
import { Component, HostListener, Injector, OnInit } from '@angular/core';
|
import { Component, HostListener, Injector, OnInit } from '@angular/core';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest, combineLatest, Observable } from 'rxjs';
|
import { debounceTime, distinctUntilChanged, first, map, withLatestFrom } from 'rxjs/operators';
|
||||||
import { debounceTime, distinctUntilChanged, filter, first, map, take, withLatestFrom } from 'rxjs/operators';
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import {
|
|
||||||
METADATA_EXPORT_SCRIPT_NAME,
|
|
||||||
METADATA_IMPORT_SCRIPT_NAME,
|
|
||||||
ScriptDataService
|
|
||||||
} from '../../core/data/processes/script-data.service';
|
|
||||||
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
||||||
import {
|
|
||||||
CreateCollectionParentSelectorComponent
|
|
||||||
} from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
|
||||||
import {
|
|
||||||
CreateCommunityParentSelectorComponent
|
|
||||||
} from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
|
||||||
import {
|
|
||||||
CreateItemParentSelectorComponent
|
|
||||||
} from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
|
||||||
import {
|
|
||||||
EditCollectionSelectorComponent
|
|
||||||
} from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
|
||||||
import {
|
|
||||||
EditCommunitySelectorComponent
|
|
||||||
} from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
|
||||||
import {
|
|
||||||
EditItemSelectorComponent
|
|
||||||
} from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
|
||||||
import {
|
|
||||||
ExportMetadataSelectorComponent
|
|
||||||
} from '../../shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
|
||||||
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
|
||||||
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
|
|
||||||
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
|
|
||||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
|
||||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||||
import { MenuItemType } from '../../shared/menu/menu-item-type.model';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,11 +53,9 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
protected menuService: MenuService,
|
protected menuService: MenuService,
|
||||||
protected injector: Injector,
|
protected injector: Injector,
|
||||||
protected variableService: CSSVariableService,
|
private variableService: CSSVariableService,
|
||||||
protected authService: AuthService,
|
private authService: AuthService,
|
||||||
protected modalService: NgbModal,
|
|
||||||
public authorizationService: AuthorizationDataService,
|
public authorizationService: AuthorizationDataService,
|
||||||
protected scriptDataService: ScriptDataService,
|
|
||||||
public route: ActivatedRoute
|
public route: ActivatedRoute
|
||||||
) {
|
) {
|
||||||
super(menuService, injector, authorizationService, route);
|
super(menuService, injector, authorizationService, route);
|
||||||
@@ -105,7 +71,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
this.authService.isAuthenticated()
|
this.authService.isAuthenticated()
|
||||||
.subscribe((loggedIn: boolean) => {
|
.subscribe((loggedIn: boolean) => {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
this.createMenu();
|
|
||||||
this.menuService.showMenu(this.menuID);
|
this.menuService.showMenu(this.menuID);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -135,515 +100,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize all menu sections and items for this menu
|
|
||||||
*/
|
|
||||||
createMenu() {
|
|
||||||
this.createMainMenuSections();
|
|
||||||
this.createSiteAdministratorMenuSections();
|
|
||||||
this.createExportMenuSections();
|
|
||||||
this.createImportMenuSections();
|
|
||||||
this.createAccessControlMenuSections();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the main menu sections.
|
|
||||||
* edit_community / edit_collection is only included if the current user is a Community or Collection admin
|
|
||||||
*/
|
|
||||||
createMainMenuSections() {
|
|
||||||
combineLatest([
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf)
|
|
||||||
]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => {
|
|
||||||
const menuList = [
|
|
||||||
/* News */
|
|
||||||
{
|
|
||||||
id: 'new',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.new'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'plus',
|
|
||||||
index: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new_community',
|
|
||||||
parentID: 'new',
|
|
||||||
active: false,
|
|
||||||
visible: isCommunityAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.new_community',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(CreateCommunityParentSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new_collection',
|
|
||||||
parentID: 'new',
|
|
||||||
active: false,
|
|
||||||
visible: isCommunityAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.new_collection',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(CreateCollectionParentSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new_item',
|
|
||||||
parentID: 'new',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.new_item',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(CreateItemParentSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new_process',
|
|
||||||
parentID: 'new',
|
|
||||||
active: false,
|
|
||||||
visible: isCollectionAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.new_process',
|
|
||||||
link: '/processes/new'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'new_item_version',
|
|
||||||
// parentID: 'new',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.new_item_version',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Edit */
|
|
||||||
{
|
|
||||||
id: 'edit',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.edit'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'pencil-alt',
|
|
||||||
index: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edit_community',
|
|
||||||
parentID: 'edit',
|
|
||||||
active: false,
|
|
||||||
visible: isCommunityAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.edit_community',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(EditCommunitySelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edit_collection',
|
|
||||||
parentID: 'edit',
|
|
||||||
active: false,
|
|
||||||
visible: isCollectionAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.edit_collection',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(EditCollectionSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edit_item',
|
|
||||||
parentID: 'edit',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.edit_item',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(EditItemSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Statistics */
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'statistics_task',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.statistics_task',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// icon: 'chart-bar',
|
|
||||||
// index: 8
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Control Panel */
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'control_panel',
|
|
||||||
// active: false,
|
|
||||||
// visible: isSiteAdmin,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.control_panel',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// icon: 'cogs',
|
|
||||||
// index: 9
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Processes */
|
|
||||||
{
|
|
||||||
id: 'processes',
|
|
||||||
active: false,
|
|
||||||
visible: isSiteAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.processes',
|
|
||||||
link: '/processes'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'terminal',
|
|
||||||
index: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'health',
|
|
||||||
active: false,
|
|
||||||
visible: isSiteAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.health',
|
|
||||||
link: '/health'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'heartbeat',
|
|
||||||
index: 11
|
|
||||||
},
|
|
||||||
];
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
|
||||||
* the export scripts exist and the current user is allowed to execute them
|
|
||||||
*/
|
|
||||||
createExportMenuSections() {
|
|
||||||
const menuList = [
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'export_community',
|
|
||||||
// parentID: 'export',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.export_community',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// shouldPersistOnRouteChange: true
|
|
||||||
// },
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'export_collection',
|
|
||||||
// parentID: 'export',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.export_collection',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// shouldPersistOnRouteChange: true
|
|
||||||
// },
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'export_item',
|
|
||||||
// parentID: 'export',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.export_item',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// shouldPersistOnRouteChange: true
|
|
||||||
// },
|
|
||||||
];
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
|
||||||
|
|
||||||
observableCombineLatest([
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
|
||||||
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME)
|
|
||||||
]).pipe(
|
|
||||||
filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
|
|
||||||
take(1)
|
|
||||||
).subscribe(() => {
|
|
||||||
// Hides the export menu for unauthorised people
|
|
||||||
// If in the future more sub-menus are added,
|
|
||||||
// it should be reviewed if they need to be in this subscribe
|
|
||||||
this.menuService.addSection(this.menuID, {
|
|
||||||
id: 'export',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.export'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'file-export',
|
|
||||||
index: 3,
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
});
|
|
||||||
this.menuService.addSection(this.menuID, {
|
|
||||||
id: 'export_metadata',
|
|
||||||
parentID: 'export',
|
|
||||||
active: true,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.export_metadata',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(ExportMetadataSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
|
||||||
* the import scripts exist and the current user is allowed to execute them
|
|
||||||
*/
|
|
||||||
createImportMenuSections() {
|
|
||||||
const menuList = [
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'import_batch',
|
|
||||||
// parentID: 'import',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.import_batch',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// }
|
|
||||||
];
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
})));
|
|
||||||
|
|
||||||
observableCombineLatest([
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
|
||||||
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME)
|
|
||||||
]).pipe(
|
|
||||||
filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
|
|
||||||
take(1)
|
|
||||||
).subscribe(() => {
|
|
||||||
// Hides the import menu for unauthorised people
|
|
||||||
// If in the future more sub-menus are added,
|
|
||||||
// it should be reviewed if they need to be in this subscribe
|
|
||||||
this.menuService.addSection(this.menuID, {
|
|
||||||
id: 'import',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.import'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'file-import',
|
|
||||||
index: 2
|
|
||||||
});
|
|
||||||
this.menuService.addSection(this.menuID, {
|
|
||||||
id: 'import_metadata',
|
|
||||||
parentID: 'import',
|
|
||||||
active: true,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.import_metadata',
|
|
||||||
link: '/admin/metadata-import'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create menu sections dependent on whether or not the current user is a site administrator
|
|
||||||
*/
|
|
||||||
createSiteAdministratorMenuSections() {
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => {
|
|
||||||
const menuList = [
|
|
||||||
/* Admin Search */
|
|
||||||
{
|
|
||||||
id: 'admin_search',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.admin_search',
|
|
||||||
link: '/admin/search'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'search',
|
|
||||||
index: 5
|
|
||||||
},
|
|
||||||
/* Registries */
|
|
||||||
{
|
|
||||||
id: 'registries',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.registries'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'list',
|
|
||||||
index: 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'registries_metadata',
|
|
||||||
parentID: 'registries',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.registries_metadata',
|
|
||||||
link: 'admin/registries/metadata'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'registries_format',
|
|
||||||
parentID: 'registries',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.registries_format',
|
|
||||||
link: 'admin/registries/bitstream-formats'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Curation tasks */
|
|
||||||
{
|
|
||||||
id: 'curation_tasks',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.curation_task',
|
|
||||||
link: 'admin/curation-tasks'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'filter',
|
|
||||||
index: 7
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Workflow */
|
|
||||||
{
|
|
||||||
id: 'workflow',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.workflow',
|
|
||||||
link: '/admin/workflow'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'user-check',
|
|
||||||
index: 11
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create menu sections dependent on whether or not the current user can manage access control groups
|
|
||||||
*/
|
|
||||||
createAccessControlMenuSections() {
|
|
||||||
observableCombineLatest([
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanManageGroups)
|
|
||||||
]).subscribe(([isSiteAdmin, canManageGroups]) => {
|
|
||||||
const menuList = [
|
|
||||||
/* Access Control */
|
|
||||||
{
|
|
||||||
id: 'access_control_people',
|
|
||||||
parentID: 'access_control',
|
|
||||||
active: false,
|
|
||||||
visible: isSiteAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.access_control_people',
|
|
||||||
link: '/access-control/epeople'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'access_control_groups',
|
|
||||||
parentID: 'access_control',
|
|
||||||
active: false,
|
|
||||||
visible: canManageGroups,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.access_control_groups',
|
|
||||||
link: '/access-control/groups'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'access_control_authorizations',
|
|
||||||
// parentID: 'access_control',
|
|
||||||
// active: false,
|
|
||||||
// visible: authorized,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.access_control_authorizations',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
id: 'access_control',
|
|
||||||
active: false,
|
|
||||||
visible: canManageGroups || isSiteAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.access_control'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'key',
|
|
||||||
index: 4
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
|
||||||
shouldPersistOnRouteChange: true,
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('focusin')
|
@HostListener('focusin')
|
||||||
public handleFocusIn() {
|
public handleFocusIn() {
|
||||||
this.inFocus$.next(true);
|
this.inFocus$.next(true);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule, NoPreloading } from '@angular/router';
|
||||||
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
|
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
|
||||||
|
|
||||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
ThemedPageInternalServerErrorComponent
|
ThemedPageInternalServerErrorComponent
|
||||||
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||||
|
import { MenuResolver } from './menu.resolver';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -46,6 +47,7 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
|||||||
path: '',
|
path: '',
|
||||||
canActivate: [AuthBlockingGuard],
|
canActivate: [AuthBlockingGuard],
|
||||||
canActivateChild: [ServerCheckGuard],
|
canActivateChild: [ServerCheckGuard],
|
||||||
|
resolve: [MenuResolver],
|
||||||
children: [
|
children: [
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
{
|
{
|
||||||
@@ -229,6 +231,12 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
], {
|
], {
|
||||||
|
// enableTracing: true,
|
||||||
|
useHash: false,
|
||||||
|
scrollPositionRestoration: 'enabled',
|
||||||
|
anchorScrolling: 'enabled',
|
||||||
|
initialNavigation: 'enabledBlocking',
|
||||||
|
preloadingStrategy: NoPreloading,
|
||||||
onSameUrlNavigation: 'reload',
|
onSameUrlNavigation: 'reload',
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
@@ -72,7 +72,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
/**
|
/**
|
||||||
* Whether or not the app is in the process of rerouting
|
* Whether or not the app is in the process of rerouting
|
||||||
*/
|
*/
|
||||||
isRouteLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
isRouteLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the theme is in the process of being swapped
|
* Whether or not the theme is in the process of being swapped
|
||||||
@@ -121,7 +121,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
this.themeService.getThemeName$().subscribe((themeName: string) => {
|
this.themeService.getThemeName$().subscribe((themeName: string) => {
|
||||||
if (isPlatformBrowser(this.platformId)) {
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
// the theme css will never download server side, so this should only happen on the browser
|
// the theme css will never download server side, so this should only happen on the browser
|
||||||
this.isThemeCSSLoading$.next(true);
|
this.distinctNext(this.isThemeCSSLoading$, true);
|
||||||
}
|
}
|
||||||
if (hasValue(themeName)) {
|
if (hasValue(themeName)) {
|
||||||
this.loadGlobalThemeConfig(themeName);
|
this.loadGlobalThemeConfig(themeName);
|
||||||
@@ -200,7 +200,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
this.router.events.subscribe((event) => {
|
this.router.events.subscribe((event) => {
|
||||||
if (event instanceof NavigationStart) {
|
if (event instanceof NavigationStart) {
|
||||||
resolveEndFound = false;
|
resolveEndFound = false;
|
||||||
this.isRouteLoading$.next(true);
|
this.distinctNext(this.isRouteLoading$, true);
|
||||||
} else if (event instanceof ResolveEnd) {
|
} else if (event instanceof ResolveEnd) {
|
||||||
resolveEndFound = true;
|
resolveEndFound = true;
|
||||||
const activatedRouteSnapShot: ActivatedRouteSnapshot = event.state.root;
|
const activatedRouteSnapShot: ActivatedRouteSnapshot = event.state.root;
|
||||||
@@ -213,16 +213,16 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
).subscribe((changed) => {
|
).subscribe((changed) => {
|
||||||
this.isThemeLoading$.next(changed);
|
this.distinctNext(this.isThemeLoading$, changed);
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
event instanceof NavigationEnd ||
|
event instanceof NavigationEnd ||
|
||||||
event instanceof NavigationCancel
|
event instanceof NavigationCancel
|
||||||
) {
|
) {
|
||||||
if (!resolveEndFound) {
|
if (!resolveEndFound) {
|
||||||
this.isThemeLoading$.next(false);
|
this.distinctNext(this.isThemeLoading$, false);
|
||||||
}
|
}
|
||||||
this.isRouteLoading$.next(false);
|
this.distinctNext(this.isRouteLoading$, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -280,7 +280,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// the fact that this callback is used, proves we're on the browser.
|
// the fact that this callback is used, proves we're on the browser.
|
||||||
this.isThemeCSSLoading$.next(false);
|
this.distinctNext(this.isThemeCSSLoading$, false);
|
||||||
};
|
};
|
||||||
head.appendChild(link);
|
head.appendChild(link);
|
||||||
}
|
}
|
||||||
@@ -375,4 +375,17 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use nextValue to update a given BehaviorSubject, only if it differs from its current value
|
||||||
|
*
|
||||||
|
* @param bs a BehaviorSubject
|
||||||
|
* @param nextValue the next value for that BehaviorSubject
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected distinctNext<T>(bs: BehaviorSubject<T>, nextValue: T): void {
|
||||||
|
if (bs.getValue() !== nextValue) {
|
||||||
|
bs.next(nextValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,10 +15,6 @@ import {
|
|||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
|
||||||
import { AdminSidebarSectionComponent } from './admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
|
||||||
import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component';
|
|
||||||
import { ExpandableAdminSidebarSectionComponent } from './admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { appEffects } from './app.effects';
|
import { appEffects } from './app.effects';
|
||||||
@@ -27,40 +23,20 @@ import { appReducers, AppState, storeModuleConfig } from './app.reducer';
|
|||||||
import { CheckAuthenticationTokenAction } from './core/auth/auth.actions';
|
import { CheckAuthenticationTokenAction } from './core/auth/auth.actions';
|
||||||
import { CoreModule } from './core/core.module';
|
import { CoreModule } from './core/core.module';
|
||||||
import { ClientCookieService } from './core/services/client-cookie.service';
|
import { ClientCookieService } from './core/services/client-cookie.service';
|
||||||
import { FooterComponent } from './footer/footer.component';
|
|
||||||
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
|
|
||||||
import { HeaderComponent } from './header/header.component';
|
|
||||||
import { NavbarModule } from './navbar/navbar.module';
|
import { NavbarModule } from './navbar/navbar.module';
|
||||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
|
||||||
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
|
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
|
||||||
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
|
||||||
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
|
||||||
import { SharedModule } from './shared/shared.module';
|
import { SharedModule } from './shared/shared.module';
|
||||||
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
|
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { ForbiddenComponent } from './forbidden/forbidden.component';
|
|
||||||
import { AuthInterceptor } from './core/auth/auth.interceptor';
|
import { AuthInterceptor } from './core/auth/auth.interceptor';
|
||||||
import { LocaleInterceptor } from './core/locale/locale.interceptor';
|
import { LocaleInterceptor } from './core/locale/locale.interceptor';
|
||||||
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
|
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
|
||||||
import { LogInterceptor } from './core/log/log.interceptor';
|
import { LogInterceptor } from './core/log/log.interceptor';
|
||||||
import { RootComponent } from './root/root.component';
|
import { EagerThemesModule } from '../themes/eager-themes.module';
|
||||||
import { ThemedRootComponent } from './root/themed-root.component';
|
|
||||||
import { ThemedEntryComponentModule } from '../themes/themed-entry-component.module';
|
|
||||||
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
|
|
||||||
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
|
||||||
import { ThemedHeaderComponent } from './header/themed-header.component';
|
|
||||||
import { ThemedFooterComponent } from './footer/themed-footer.component';
|
|
||||||
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
|
||||||
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
|
||||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
|
||||||
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
|
|
||||||
import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component';
|
|
||||||
import { ThemedAdminSidebarComponent } from './admin/admin-sidebar/themed-admin-sidebar.component';
|
|
||||||
|
|
||||||
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
||||||
import { NgxMaskModule } from 'ngx-mask';
|
import { NgxMaskModule } from 'ngx-mask';
|
||||||
|
|
||||||
import { StoreDevModules } from '../config/store/devtools';
|
import { StoreDevModules } from '../config/store/devtools';
|
||||||
|
import { RootModule } from './root.module';
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return environment;
|
return environment;
|
||||||
@@ -96,8 +72,9 @@ const IMPORTS = [
|
|||||||
EffectsModule.forRoot(appEffects),
|
EffectsModule.forRoot(appEffects),
|
||||||
StoreModule.forRoot(appReducers, storeModuleConfig),
|
StoreModule.forRoot(appReducers, storeModuleConfig),
|
||||||
StoreRouterConnectingModule.forRoot(),
|
StoreRouterConnectingModule.forRoot(),
|
||||||
ThemedEntryComponentModule.withEntryComponents(),
|
|
||||||
StoreDevModules,
|
StoreDevModules,
|
||||||
|
EagerThemesModule,
|
||||||
|
RootModule,
|
||||||
];
|
];
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
@@ -162,29 +139,6 @@ const PROVIDERS = [
|
|||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
RootComponent,
|
|
||||||
ThemedRootComponent,
|
|
||||||
HeaderComponent,
|
|
||||||
ThemedHeaderComponent,
|
|
||||||
HeaderNavbarWrapperComponent,
|
|
||||||
ThemedHeaderNavbarWrapperComponent,
|
|
||||||
AdminSidebarComponent,
|
|
||||||
ThemedAdminSidebarComponent,
|
|
||||||
AdminSidebarSectionComponent,
|
|
||||||
ExpandableAdminSidebarSectionComponent,
|
|
||||||
FooterComponent,
|
|
||||||
ThemedFooterComponent,
|
|
||||||
PageNotFoundComponent,
|
|
||||||
ThemedPageNotFoundComponent,
|
|
||||||
NotificationComponent,
|
|
||||||
NotificationsBoardComponent,
|
|
||||||
BreadcrumbsComponent,
|
|
||||||
ThemedBreadcrumbsComponent,
|
|
||||||
ForbiddenComponent,
|
|
||||||
ThemedForbiddenComponent,
|
|
||||||
IdleModalComponent,
|
|
||||||
ThemedPageInternalServerErrorComponent,
|
|
||||||
PageInternalServerErrorComponent
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const EXPORTS = [
|
const EXPORTS = [
|
||||||
|
@@ -192,7 +192,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
idle: false
|
idle: false
|
||||||
};
|
};
|
||||||
@@ -212,7 +212,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
idle: false
|
idle: false
|
||||||
};
|
};
|
||||||
@@ -558,7 +558,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: [],
|
authMethods: [],
|
||||||
idle: false
|
idle: false
|
||||||
|
@@ -92,11 +92,15 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.AUTHENTICATED:
|
case AuthActionTypes.AUTHENTICATED:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
loading: true,
|
||||||
|
blocking: true
|
||||||
|
});
|
||||||
|
|
||||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
|
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
|
||||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE:
|
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: true,
|
loading: true,
|
||||||
blocking: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.AUTHENTICATED_ERROR:
|
case AuthActionTypes.AUTHENTICATED_ERROR:
|
||||||
@@ -210,7 +214,6 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS:
|
case AuthActionTypes.RETRIEVE_AUTH_METHODS:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: true,
|
loading: true,
|
||||||
blocking: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS:
|
case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS:
|
||||||
|
@@ -75,7 +75,6 @@ import { RegistryService } from './registry/registry.service';
|
|||||||
import { RoleService } from './roles/role.service';
|
import { RoleService } from './roles/role.service';
|
||||||
import { FeedbackDataService } from './feedback/feedback-data.service';
|
import { FeedbackDataService } from './feedback/feedback-data.service';
|
||||||
|
|
||||||
import { ApiService } from './services/api.service';
|
|
||||||
import { ServerResponseService } from './services/server-response.service';
|
import { ServerResponseService } from './services/server-response.service';
|
||||||
import { NativeWindowFactory, NativeWindowService } from './services/window.service';
|
import { NativeWindowFactory, NativeWindowService } from './services/window.service';
|
||||||
import { BitstreamFormat } from './shared/bitstream-format.model';
|
import { BitstreamFormat } from './shared/bitstream-format.model';
|
||||||
@@ -191,7 +190,6 @@ const DECLARATIONS = [];
|
|||||||
const EXPORTS = [];
|
const EXPORTS = [];
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
ApiService,
|
|
||||||
AuthenticatedGuard,
|
AuthenticatedGuard,
|
||||||
CommunityDataService,
|
CommunityDataService,
|
||||||
CollectionDataService,
|
CollectionDataService,
|
||||||
|
@@ -1,24 +0,0 @@
|
|||||||
import { throwError as observableThrowError } from 'rxjs';
|
|
||||||
import { catchError } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ApiService {
|
|
||||||
constructor(public _http: HttpClient) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* whatever domain/feature method name
|
|
||||||
*/
|
|
||||||
get(url: string, options?: any) {
|
|
||||||
return this._http.get(url, options).pipe(
|
|
||||||
catchError((err) => {
|
|
||||||
console.log('Error: ', err);
|
|
||||||
return observableThrowError(err);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -27,7 +27,7 @@
|
|||||||
<a *ngIf="bitstreamDownloadUrl != null" [href]="bitstreamDownloadUrl"
|
<a *ngIf="bitstreamDownloadUrl != null" [href]="bitstreamDownloadUrl"
|
||||||
class="btn btn-outline-primary btn-sm"
|
class="btn btn-outline-primary btn-sm"
|
||||||
title="{{'item.edit.bitstreams.edit.buttons.download' | translate}}"
|
title="{{'item.edit.bitstreams.edit.buttons.download' | translate}}"
|
||||||
data-test="download-button">
|
[attr.data-test]="'download-button' | dsBrowserOnly">
|
||||||
<i class="fas fa-download fa-fw"></i>
|
<i class="fas fa-download fa-fw"></i>
|
||||||
</a>
|
</a>
|
||||||
<button [routerLink]="['/bitstreams/', bitstream.id, 'edit']" class="btn btn-outline-primary btn-sm"
|
<button [routerLink]="['/bitstreams/', bitstream.id, 'edit']" class="btn btn-outline-primary btn-sm"
|
||||||
|
@@ -12,6 +12,7 @@ import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
import { getBitstreamDownloadRoute } from '../../../../app-routing-paths';
|
import { getBitstreamDownloadRoute } from '../../../../app-routing-paths';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { BrowserOnlyMockPipe } from '../../../../shared/testing/browser-only-mock.pipe';
|
||||||
|
|
||||||
let comp: ItemEditBitstreamComponent;
|
let comp: ItemEditBitstreamComponent;
|
||||||
let fixture: ComponentFixture<ItemEditBitstreamComponent>;
|
let fixture: ComponentFixture<ItemEditBitstreamComponent>;
|
||||||
@@ -72,7 +73,11 @@ describe('ItemEditBitstreamComponent', () => {
|
|||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot()],
|
||||||
declarations: [ItemEditBitstreamComponent, VarDirective],
|
declarations: [
|
||||||
|
ItemEditBitstreamComponent,
|
||||||
|
VarDirective,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService }
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService }
|
||||||
], schemas: [
|
], schemas: [
|
||||||
|
331
src/app/menu.resolver.spec.ts
Normal file
331
src/app/menu.resolver.spec.ts
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MenuResolver } from './menu.resolver';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from './core/data/feature-authorization/feature-id';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
|
import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ScriptDataService } from './core/data/processes/script-data.service';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { MenuServiceStub } from './shared/testing/menu-service.stub';
|
||||||
|
import { MenuID } from './shared/menu/menu-id.model';
|
||||||
|
import { BrowseService } from './core/browse/browse.service';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import createSpy = jasmine.createSpy;
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from './shared/remote-data.utils';
|
||||||
|
import { createPaginatedList } from './shared/testing/utils.test';
|
||||||
|
|
||||||
|
const BOOLEAN = { t: true, f: false };
|
||||||
|
const MENU_STATE = {
|
||||||
|
id: 'some menu'
|
||||||
|
};
|
||||||
|
const BROWSE_DEFINITIONS = [
|
||||||
|
{ id: 'definition1' },
|
||||||
|
{ id: 'definition2' },
|
||||||
|
{ id: 'definition3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('MenuResolver', () => {
|
||||||
|
let resolver: MenuResolver;
|
||||||
|
|
||||||
|
let menuService;
|
||||||
|
let browseService;
|
||||||
|
let authorizationService;
|
||||||
|
let scriptService;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
menuService = new MenuServiceStub();
|
||||||
|
spyOn(menuService, 'getMenu').and.returnValue(observableOf(MENU_STATE));
|
||||||
|
|
||||||
|
browseService = jasmine.createSpyObj('browseService', {
|
||||||
|
getBrowseDefinitions: createSuccessfulRemoteDataObject$(createPaginatedList(BROWSE_DEFINITIONS))
|
||||||
|
});
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
scriptService = jasmine.createSpyObj('scriptService', {
|
||||||
|
scriptWithNameExistsAndCanExecute: observableOf(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
|
||||||
|
declarations: [AdminSidebarComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: MenuService, useValue: menuService },
|
||||||
|
{ provide: BrowseService, useValue: browseService },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{
|
||||||
|
provide: NgbModal, useValue: {
|
||||||
|
open: () => {/*comment*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
});
|
||||||
|
resolver = TestBed.inject(MenuResolver);
|
||||||
|
|
||||||
|
spyOn(menuService, 'addSection');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(resolver).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolve', () => {
|
||||||
|
it('should create all menus', (done) => {
|
||||||
|
spyOn(resolver, 'createPublicMenu$').and.returnValue(observableOf(true));
|
||||||
|
spyOn(resolver, 'createAdminMenu$').and.returnValue(observableOf(true));
|
||||||
|
|
||||||
|
resolver.resolve(null, null).subscribe(resolved => {
|
||||||
|
expect(resolved).toBeTrue();
|
||||||
|
expect(resolver.createPublicMenu$).toHaveBeenCalled();
|
||||||
|
expect(resolver.createAdminMenu$).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an Observable that emits true as soon as all menus are created', () => {
|
||||||
|
spyOn(resolver, 'createPublicMenu$').and.returnValue(cold('--(t|)', BOOLEAN));
|
||||||
|
spyOn(resolver, 'createAdminMenu$').and.returnValue(cold('----(t|)', BOOLEAN));
|
||||||
|
|
||||||
|
expect(resolver.resolve(null, null)).toBeObservable(cold('----(t|)', BOOLEAN));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createPublicMenu$', () => {
|
||||||
|
it('should retrieve the menu by ID return an Observable that emits true as soon as it is created', () => {
|
||||||
|
(menuService as any).getMenu.and.returnValue(cold('--u--m--', {
|
||||||
|
u: undefined,
|
||||||
|
m: MENU_STATE,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(resolver.createPublicMenu$()).toBeObservable(cold('-----(t|)', BOOLEAN));
|
||||||
|
expect(menuService.getMenu).toHaveBeenCalledOnceWith(MenuID.PUBLIC);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('contents', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
resolver.createPublicMenu$().subscribe((_) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include community list link', () => {
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
|
||||||
|
id: 'browse_global_communities_and_collections', visible: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include browse dropdown', () => {
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
|
||||||
|
id: 'browse_global_by_definition1', parentID: 'browse_global', visible: true,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
|
||||||
|
id: 'browse_global_by_definition2', parentID: 'browse_global', visible: true,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
|
||||||
|
id: 'browse_global_by_definition3', parentID: 'browse_global', visible: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
|
||||||
|
id: 'browse_global', visible: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAdminMenu$', () => {
|
||||||
|
it('should retrieve the menu by ID return an Observable that emits true as soon as it is created', () => {
|
||||||
|
(menuService as any).getMenu.and.returnValue(cold('--u--m', {
|
||||||
|
u: undefined,
|
||||||
|
m: MENU_STATE,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(resolver.createAdminMenu$()).toBeObservable(cold('-----(t|)', BOOLEAN));
|
||||||
|
expect(menuService.getMenu).toHaveBeenCalledOnceWith(MenuID.ADMIN);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for regular user', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake(() => {
|
||||||
|
return observableOf(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
resolver.createAdminMenu$().subscribe((_) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show site admin section', () => {
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'admin_search', visible: false,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'registries', visible: false,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
parentID: 'registries', visible: false,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'curation_tasks', visible: false,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'workflow', visible: false,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show edit_community', () => {
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'edit_community', visible: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show edit_collection', () => {
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'edit_collection', visible: false,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show access control section', () => {
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'access_control', visible: false,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
parentID: 'access_control', visible: false,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// We check that the menu section has not been called with visible set to true
|
||||||
|
// The reason why we don't check if it has been called with visible set to false
|
||||||
|
// Is because the function does not get called unless a user is authorised
|
||||||
|
it('should not show the import section', () => {
|
||||||
|
expect(menuService.addSection).not.toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'import', visible: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// We check that the menu section has not been called with visible set to true
|
||||||
|
// The reason why we don't check if it has been called with visible set to false
|
||||||
|
// Is because the function does not get called unless a user is authorised
|
||||||
|
it('should not show the export section', () => {
|
||||||
|
expect(menuService.addSection).not.toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'export', visible: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for site admin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||||
|
return observableOf(featureID === FeatureID.AdministratorOf);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
resolver.createAdminMenu$().subscribe((_) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain site admin section', () => {
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'admin_search', visible: true,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'registries', visible: true,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
parentID: 'registries', visible: true,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'curation_tasks', visible: true,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'workflow', visible: true,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'workflow', visible: true,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'import', visible: true,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'export', visible: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for community admin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||||
|
return observableOf(featureID === FeatureID.IsCommunityAdmin);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
resolver.createAdminMenu$().subscribe((_) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show edit_community', () => {
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'edit_community', visible: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for collection admin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||||
|
return observableOf(featureID === FeatureID.IsCollectionAdmin);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
resolver.createAdminMenu$().subscribe((_) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show edit_collection', () => {
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'edit_collection', visible: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for group admin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||||
|
return observableOf(featureID === FeatureID.CanManageGroups);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
resolver.createAdminMenu$().subscribe((_) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show access control section', () => {
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'access_control', visible: true,
|
||||||
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
parentID: 'access_control', visible: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
627
src/app/menu.resolver.ts
Normal file
627
src/app/menu.resolver.ts
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { combineLatest as observableCombineLatest, combineLatest, Observable } from 'rxjs';
|
||||||
|
import { MenuID } from './shared/menu/menu-id.model';
|
||||||
|
import { MenuState } from './shared/menu/menu-state.model';
|
||||||
|
import { MenuItemType } from './shared/menu/menu-item-type.model';
|
||||||
|
import { LinkMenuItemModel } from './shared/menu/menu-item/models/link.model';
|
||||||
|
import { getFirstCompletedRemoteData } from './core/shared/operators';
|
||||||
|
import { PaginatedList } from './core/data/paginated-list.model';
|
||||||
|
import { BrowseDefinition } from './core/shared/browse-definition.model';
|
||||||
|
import { RemoteData } from './core/data/remote-data';
|
||||||
|
import { TextMenuItemModel } from './shared/menu/menu-item/models/text.model';
|
||||||
|
import { BrowseService } from './core/browse/browse.service';
|
||||||
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
|
import { filter, find, map, take } from 'rxjs/operators';
|
||||||
|
import { hasValue } from './shared/empty.util';
|
||||||
|
import { FeatureID } from './core/data/feature-authorization/feature-id';
|
||||||
|
import { CreateCommunityParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||||
|
import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model';
|
||||||
|
import { CreateCollectionParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||||
|
import { CreateItemParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||||
|
import { EditCommunitySelectorComponent } from './shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||||
|
import { EditCollectionSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||||
|
import { EditItemSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||||
|
import { ExportMetadataSelectorComponent } from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
||||||
|
import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {
|
||||||
|
METADATA_EXPORT_SCRIPT_NAME, METADATA_IMPORT_SCRIPT_NAME, ScriptDataService
|
||||||
|
} from './core/data/processes/script-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates all of the app's menus
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MenuResolver implements Resolve<boolean> {
|
||||||
|
constructor(
|
||||||
|
protected menuService: MenuService,
|
||||||
|
protected browseService: BrowseService,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
protected scriptDataService: ScriptDataService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all menus
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
|
return combineLatest([
|
||||||
|
this.createPublicMenu$(),
|
||||||
|
this.createAdminMenu$(),
|
||||||
|
]).pipe(
|
||||||
|
map((menusDone: boolean[]) => menusDone.every(Boolean)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a specific menu to appear
|
||||||
|
* @param id the ID of the menu to wait for
|
||||||
|
* @return an Observable that emits true as soon as the menu is created
|
||||||
|
*/
|
||||||
|
protected waitForMenu$(id: MenuID): Observable<boolean> {
|
||||||
|
return this.menuService.getMenu(id).pipe(
|
||||||
|
find((menu: MenuState) => hasValue(menu)),
|
||||||
|
map(() => true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all menu sections and items for {@link MenuID.PUBLIC}
|
||||||
|
*/
|
||||||
|
createPublicMenu$(): Observable<boolean> {
|
||||||
|
const menuList: any[] = [
|
||||||
|
/* Communities & Collections tree */
|
||||||
|
{
|
||||||
|
id: `browse_global_communities_and_collections`,
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
index: 0,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: `menu.section.browse_global_communities_and_collections`,
|
||||||
|
link: `/community-list`
|
||||||
|
} as LinkMenuItemModel
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// Read the different Browse-By types from config and add them to the browse menu
|
||||||
|
this.browseService.getBrowseDefinitions()
|
||||||
|
.pipe(getFirstCompletedRemoteData<PaginatedList<BrowseDefinition>>())
|
||||||
|
.subscribe((browseDefListRD: RemoteData<PaginatedList<BrowseDefinition>>) => {
|
||||||
|
if (browseDefListRD.hasSucceeded) {
|
||||||
|
browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => {
|
||||||
|
menuList.push({
|
||||||
|
id: `browse_global_by_${browseDef.id}`,
|
||||||
|
parentID: 'browse_global',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: `menu.section.browse_global_by_${browseDef.id}`,
|
||||||
|
link: `/browse/${browseDef.id}`
|
||||||
|
} as LinkMenuItemModel
|
||||||
|
});
|
||||||
|
});
|
||||||
|
menuList.push(
|
||||||
|
/* Browse */
|
||||||
|
{
|
||||||
|
id: 'browse_global',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
index: 1,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.browse_global'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.PUBLIC, Object.assign(menuSection, {
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.waitForMenu$(MenuID.PUBLIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all menu sections and items for {@link MenuID.ADMIN}
|
||||||
|
*/
|
||||||
|
createAdminMenu$() {
|
||||||
|
this.createMainMenuSections();
|
||||||
|
this.createSiteAdministratorMenuSections();
|
||||||
|
this.createExportMenuSections();
|
||||||
|
this.createImportMenuSections();
|
||||||
|
this.createAccessControlMenuSections();
|
||||||
|
|
||||||
|
return this.waitForMenu$(MenuID.ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the main menu sections.
|
||||||
|
* edit_community / edit_collection is only included if the current user is a Community or Collection admin
|
||||||
|
*/
|
||||||
|
createMainMenuSections() {
|
||||||
|
combineLatest([
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.AdministratorOf)
|
||||||
|
]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => {
|
||||||
|
const menuList = [
|
||||||
|
/* News */
|
||||||
|
{
|
||||||
|
id: 'new',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.new'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'plus',
|
||||||
|
index: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new_community',
|
||||||
|
parentID: 'new',
|
||||||
|
active: false,
|
||||||
|
visible: isCommunityAdmin,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.ONCLICK,
|
||||||
|
text: 'menu.section.new_community',
|
||||||
|
function: () => {
|
||||||
|
this.modalService.open(CreateCommunityParentSelectorComponent);
|
||||||
|
}
|
||||||
|
} as OnClickMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new_collection',
|
||||||
|
parentID: 'new',
|
||||||
|
active: false,
|
||||||
|
visible: isCommunityAdmin,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.ONCLICK,
|
||||||
|
text: 'menu.section.new_collection',
|
||||||
|
function: () => {
|
||||||
|
this.modalService.open(CreateCollectionParentSelectorComponent);
|
||||||
|
}
|
||||||
|
} as OnClickMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new_item',
|
||||||
|
parentID: 'new',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.ONCLICK,
|
||||||
|
text: 'menu.section.new_item',
|
||||||
|
function: () => {
|
||||||
|
this.modalService.open(CreateItemParentSelectorComponent);
|
||||||
|
}
|
||||||
|
} as OnClickMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new_process',
|
||||||
|
parentID: 'new',
|
||||||
|
active: false,
|
||||||
|
visible: isCollectionAdmin,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.new_process',
|
||||||
|
link: '/processes/new'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
|
// {
|
||||||
|
// id: 'new_item_version',
|
||||||
|
// parentID: 'new',
|
||||||
|
// active: false,
|
||||||
|
// visible: true,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.LINK,
|
||||||
|
// text: 'menu.section.new_item_version',
|
||||||
|
// link: ''
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Edit */
|
||||||
|
{
|
||||||
|
id: 'edit',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.edit'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'pencil-alt',
|
||||||
|
index: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit_community',
|
||||||
|
parentID: 'edit',
|
||||||
|
active: false,
|
||||||
|
visible: isCommunityAdmin,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.ONCLICK,
|
||||||
|
text: 'menu.section.edit_community',
|
||||||
|
function: () => {
|
||||||
|
this.modalService.open(EditCommunitySelectorComponent);
|
||||||
|
}
|
||||||
|
} as OnClickMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit_collection',
|
||||||
|
parentID: 'edit',
|
||||||
|
active: false,
|
||||||
|
visible: isCollectionAdmin,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.ONCLICK,
|
||||||
|
text: 'menu.section.edit_collection',
|
||||||
|
function: () => {
|
||||||
|
this.modalService.open(EditCollectionSelectorComponent);
|
||||||
|
}
|
||||||
|
} as OnClickMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit_item',
|
||||||
|
parentID: 'edit',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.ONCLICK,
|
||||||
|
text: 'menu.section.edit_item',
|
||||||
|
function: () => {
|
||||||
|
this.modalService.open(EditItemSelectorComponent);
|
||||||
|
}
|
||||||
|
} as OnClickMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Statistics */
|
||||||
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
|
// {
|
||||||
|
// id: 'statistics_task',
|
||||||
|
// active: false,
|
||||||
|
// visible: true,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.LINK,
|
||||||
|
// text: 'menu.section.statistics_task',
|
||||||
|
// link: ''
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// icon: 'chart-bar',
|
||||||
|
// index: 8
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Control Panel */
|
||||||
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
|
// {
|
||||||
|
// id: 'control_panel',
|
||||||
|
// active: false,
|
||||||
|
// visible: isSiteAdmin,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.LINK,
|
||||||
|
// text: 'menu.section.control_panel',
|
||||||
|
// link: ''
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// icon: 'cogs',
|
||||||
|
// index: 9
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Processes */
|
||||||
|
{
|
||||||
|
id: 'processes',
|
||||||
|
active: false,
|
||||||
|
visible: isSiteAdmin,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.processes',
|
||||||
|
link: '/processes'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'terminal',
|
||||||
|
index: 10
|
||||||
|
},
|
||||||
|
];
|
||||||
|
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, {
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
||||||
|
* the export scripts exist and the current user is allowed to execute them
|
||||||
|
*/
|
||||||
|
createExportMenuSections() {
|
||||||
|
const menuList = [
|
||||||
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
|
// {
|
||||||
|
// id: 'export_community',
|
||||||
|
// parentID: 'export',
|
||||||
|
// active: false,
|
||||||
|
// visible: true,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.LINK,
|
||||||
|
// text: 'menu.section.export_community',
|
||||||
|
// link: ''
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// shouldPersistOnRouteChange: true
|
||||||
|
// },
|
||||||
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
|
// {
|
||||||
|
// id: 'export_collection',
|
||||||
|
// parentID: 'export',
|
||||||
|
// active: false,
|
||||||
|
// visible: true,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.LINK,
|
||||||
|
// text: 'menu.section.export_collection',
|
||||||
|
// link: ''
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// shouldPersistOnRouteChange: true
|
||||||
|
// },
|
||||||
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
|
// {
|
||||||
|
// id: 'export_item',
|
||||||
|
// parentID: 'export',
|
||||||
|
// active: false,
|
||||||
|
// visible: true,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.LINK,
|
||||||
|
// text: 'menu.section.export_item',
|
||||||
|
// link: ''
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// shouldPersistOnRouteChange: true
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection));
|
||||||
|
|
||||||
|
observableCombineLatest([
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||||
|
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME)
|
||||||
|
]).pipe(
|
||||||
|
filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
|
||||||
|
take(1)
|
||||||
|
).subscribe(() => {
|
||||||
|
// Hides the export menu for unauthorised people
|
||||||
|
// If in the future more sub-menus are added,
|
||||||
|
// it should be reviewed if they need to be in this subscribe
|
||||||
|
this.menuService.addSection(MenuID.ADMIN, {
|
||||||
|
id: 'export',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.export'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'file-export',
|
||||||
|
index: 3,
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
});
|
||||||
|
this.menuService.addSection(MenuID.ADMIN, {
|
||||||
|
id: 'export_metadata',
|
||||||
|
parentID: 'export',
|
||||||
|
active: true,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.ONCLICK,
|
||||||
|
text: 'menu.section.export_metadata',
|
||||||
|
function: () => {
|
||||||
|
this.modalService.open(ExportMetadataSelectorComponent);
|
||||||
|
}
|
||||||
|
} as OnClickMenuItemModel,
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
||||||
|
* the import scripts exist and the current user is allowed to execute them
|
||||||
|
*/
|
||||||
|
createImportMenuSections() {
|
||||||
|
const menuList = [
|
||||||
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
|
// {
|
||||||
|
// id: 'import_batch',
|
||||||
|
// parentID: 'import',
|
||||||
|
// active: false,
|
||||||
|
// visible: true,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.LINK,
|
||||||
|
// text: 'menu.section.import_batch',
|
||||||
|
// link: ''
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// }
|
||||||
|
];
|
||||||
|
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection));
|
||||||
|
|
||||||
|
observableCombineLatest([
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||||
|
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME)
|
||||||
|
]).pipe(
|
||||||
|
filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
|
||||||
|
take(1)
|
||||||
|
).subscribe(() => {
|
||||||
|
// Hides the import menu for unauthorised people
|
||||||
|
// If in the future more sub-menus are added,
|
||||||
|
// it should be reviewed if they need to be in this subscribe
|
||||||
|
this.menuService.addSection(MenuID.ADMIN, {
|
||||||
|
id: 'import',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.import'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'file-import',
|
||||||
|
index: 2,
|
||||||
|
shouldPersistOnRouteChange: true,
|
||||||
|
});
|
||||||
|
this.menuService.addSection(MenuID.ADMIN, {
|
||||||
|
id: 'import_metadata',
|
||||||
|
parentID: 'import',
|
||||||
|
active: true,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.import_metadata',
|
||||||
|
link: '/admin/metadata-import'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create menu sections dependent on whether or not the current user is a site administrator
|
||||||
|
*/
|
||||||
|
createSiteAdministratorMenuSections() {
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => {
|
||||||
|
const menuList = [
|
||||||
|
/* Admin Search */
|
||||||
|
{
|
||||||
|
id: 'admin_search',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.admin_search',
|
||||||
|
link: '/admin/search'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'search',
|
||||||
|
index: 5
|
||||||
|
},
|
||||||
|
/* Registries */
|
||||||
|
{
|
||||||
|
id: 'registries',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.registries'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'list',
|
||||||
|
index: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'registries_metadata',
|
||||||
|
parentID: 'registries',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.registries_metadata',
|
||||||
|
link: 'admin/registries/metadata'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'registries_format',
|
||||||
|
parentID: 'registries',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.registries_format',
|
||||||
|
link: 'admin/registries/bitstream-formats'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Curation tasks */
|
||||||
|
{
|
||||||
|
id: 'curation_tasks',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.curation_task',
|
||||||
|
link: 'admin/curation-tasks'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'filter',
|
||||||
|
index: 7
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Workflow */
|
||||||
|
{
|
||||||
|
id: 'workflow',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.workflow',
|
||||||
|
link: '/admin/workflow'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'user-check',
|
||||||
|
index: 11
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, {
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create menu sections dependent on whether or not the current user can manage access control groups
|
||||||
|
*/
|
||||||
|
createAccessControlMenuSections() {
|
||||||
|
observableCombineLatest([
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.CanManageGroups)
|
||||||
|
]).subscribe(([isSiteAdmin, canManageGroups]) => {
|
||||||
|
const menuList = [
|
||||||
|
/* Access Control */
|
||||||
|
{
|
||||||
|
id: 'access_control_people',
|
||||||
|
parentID: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: isSiteAdmin,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.access_control_people',
|
||||||
|
link: '/access-control/epeople'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'access_control_groups',
|
||||||
|
parentID: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: canManageGroups,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.access_control_groups',
|
||||||
|
link: '/access-control/groups'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
|
// {
|
||||||
|
// id: 'access_control_authorizations',
|
||||||
|
// parentID: 'access_control',
|
||||||
|
// active: false,
|
||||||
|
// visible: authorized,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.LINK,
|
||||||
|
// text: 'menu.section.access_control_authorizations',
|
||||||
|
// link: ''
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
id: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: canManageGroups || isSiteAdmin,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.access_control'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'key',
|
||||||
|
index: 4
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, {
|
||||||
|
shouldPersistOnRouteChange: true,
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,7 @@
|
|||||||
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownImport" ngbDropdownToggle
|
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownImport" ngbDropdownToggle
|
||||||
type="button" [disabled]="!(initialized$|async)"
|
type="button" [disabled]="!(initialized$|async)"
|
||||||
attr.aria-label="{{'mydspace.new-submission-external' | translate}}"
|
attr.aria-label="{{'mydspace.new-submission-external' | translate}}"
|
||||||
|
[attr.data-test]="'import-dropdown' | dsBrowserOnly"
|
||||||
title="{{'mydspace.new-submission-external' | translate}}">
|
title="{{'mydspace.new-submission-external' | translate}}">
|
||||||
<i class="fa fa-file-import" aria-hidden="true"></i>
|
<i class="fa fa-file-import" aria-hidden="true"></i>
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
|
@@ -13,6 +13,7 @@ import { ResourceType } from '../../../core/shared/resource-type';
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||||
|
import { BrowserOnlyMockPipe } from '../../../shared/testing/browser-only-mock.pipe';
|
||||||
|
|
||||||
export function getMockEntityTypeService(): EntityTypeService {
|
export function getMockEntityTypeService(): EntityTypeService {
|
||||||
const pageInfo = { elementsPerPage: 20, totalElements: 4, totalPages: 1, currentPage: 0 } as PageInfo;
|
const pageInfo = { elementsPerPage: 20, totalElements: 4, totalPages: 1, currentPage: 0 } as PageInfo;
|
||||||
@@ -83,7 +84,8 @@ describe('MyDSpaceNewExternalDropdownComponent test', () => {
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
MyDSpaceNewExternalDropdownComponent,
|
MyDSpaceNewExternalDropdownComponent,
|
||||||
TestComponent
|
TestComponent,
|
||||||
|
BrowserOnlyMockPipe
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() },
|
{ provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() },
|
||||||
@@ -134,7 +136,8 @@ describe('MyDSpaceNewExternalDropdownComponent test', () => {
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
MyDSpaceNewExternalDropdownComponent,
|
MyDSpaceNewExternalDropdownComponent,
|
||||||
TestComponent
|
TestComponent,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: EntityTypeService, useValue: getMockEntityTypeService() },
|
{ provide: EntityTypeService, useValue: getMockEntityTypeService() },
|
||||||
|
@@ -9,6 +9,7 @@
|
|||||||
<button class="btn btn-lg btn-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle
|
<button class="btn btn-lg btn-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle
|
||||||
type="button" [disabled]="!(initialized$|async)"
|
type="button" [disabled]="!(initialized$|async)"
|
||||||
attr.aria-label="{{'mydspace.new-submission' | translate}}"
|
attr.aria-label="{{'mydspace.new-submission' | translate}}"
|
||||||
|
[attr.data-test]="'submission-dropdown' | dsBrowserOnly"
|
||||||
title="{{'mydspace.new-submission' | translate}}">
|
title="{{'mydspace.new-submission' | translate}}">
|
||||||
<i class="fa fa-plus-circle" aria-hidden="true"></i>
|
<i class="fa fa-plus-circle" aria-hidden="true"></i>
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
|
@@ -12,6 +12,7 @@ import { ItemType } from '../../../core/shared/item-relationships/item-type.mode
|
|||||||
import { ResourceType } from '../../../core/shared/resource-type';
|
import { ResourceType } from '../../../core/shared/resource-type';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
|
import { BrowserOnlyMockPipe } from '../../../shared/testing/browser-only-mock.pipe';
|
||||||
|
|
||||||
export function getMockEntityTypeService(): EntityTypeService {
|
export function getMockEntityTypeService(): EntityTypeService {
|
||||||
const type1: ItemType = {
|
const type1: ItemType = {
|
||||||
@@ -87,7 +88,8 @@ describe('MyDSpaceNewSubmissionDropdownComponent test', () => {
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
MyDSpaceNewSubmissionDropdownComponent,
|
MyDSpaceNewSubmissionDropdownComponent,
|
||||||
TestComponent
|
TestComponent,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() },
|
{ provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() },
|
||||||
@@ -138,7 +140,8 @@ describe('MyDSpaceNewSubmissionDropdownComponent test', () => {
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
MyDSpaceNewSubmissionDropdownComponent,
|
MyDSpaceNewSubmissionDropdownComponent,
|
||||||
TestComponent
|
TestComponent,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: EntityTypeService, useValue: getMockEntityTypeService() },
|
{ provide: EntityTypeService, useValue: getMockEntityTypeService() },
|
||||||
|
@@ -2,18 +2,11 @@ import { Component, Injector } from '@angular/core';
|
|||||||
import { slideMobileNav } from '../shared/animations/slide';
|
import { slideMobileNav } from '../shared/animations/slide';
|
||||||
import { MenuComponent } from '../shared/menu/menu.component';
|
import { MenuComponent } from '../shared/menu/menu.component';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model';
|
|
||||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { BrowseService } from '../core/browse/browse.service';
|
import { BrowseService } from '../core/browse/browse.service';
|
||||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
|
||||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
|
||||||
import { BrowseDefinition } from '../core/shared/browse-definition.model';
|
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
import { MenuID } from '../shared/menu/menu-id.model';
|
import { MenuID } from '../shared/menu/menu-id.model';
|
||||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing the public navbar
|
* Component representing the public navbar
|
||||||
@@ -42,64 +35,6 @@ export class NavbarComponent extends MenuComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.createMenu();
|
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize all menu sections and items for this menu
|
|
||||||
*/
|
|
||||||
createMenu() {
|
|
||||||
const menuList: any[] = [
|
|
||||||
/* Communities & Collections tree */
|
|
||||||
{
|
|
||||||
id: `browse_global_communities_and_collections`,
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
index: 0,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: `menu.section.browse_global_communities_and_collections`,
|
|
||||||
link: `/community-list`
|
|
||||||
} as LinkMenuItemModel
|
|
||||||
}
|
|
||||||
];
|
|
||||||
// Read the different Browse-By types from config and add them to the browse menu
|
|
||||||
this.browseService.getBrowseDefinitions()
|
|
||||||
.pipe(getFirstCompletedRemoteData<PaginatedList<BrowseDefinition>>())
|
|
||||||
.subscribe((browseDefListRD: RemoteData<PaginatedList<BrowseDefinition>>) => {
|
|
||||||
if (browseDefListRD.hasSucceeded) {
|
|
||||||
browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => {
|
|
||||||
menuList.push({
|
|
||||||
id: `browse_global_by_${browseDef.id}`,
|
|
||||||
parentID: 'browse_global',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: `menu.section.browse_global_by_${browseDef.id}`,
|
|
||||||
link: `/browse/${browseDef.id}`
|
|
||||||
} as LinkMenuItemModel
|
|
||||||
});
|
|
||||||
});
|
|
||||||
menuList.push(
|
|
||||||
/* Browse */
|
|
||||||
{
|
|
||||||
id: 'browse_global',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
index: 1,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.browse_global'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
100
src/app/root.module.ts
Normal file
100
src/app/root.module.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AdminSidebarSectionComponent
|
||||||
|
} from './admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||||
|
import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component';
|
||||||
|
import { ThemedAdminSidebarComponent } from './admin/admin-sidebar/themed-admin-sidebar.component';
|
||||||
|
import {
|
||||||
|
ExpandableAdminSidebarSectionComponent
|
||||||
|
} from './admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||||
|
import { FooterComponent } from './footer/footer.component';
|
||||||
|
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
|
||||||
|
import { HeaderComponent } from './header/header.component';
|
||||||
|
import { NavbarModule } from './navbar/navbar.module';
|
||||||
|
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||||
|
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
||||||
|
import {
|
||||||
|
NotificationsBoardComponent
|
||||||
|
} from './shared/notifications/notifications-board/notifications-board.component';
|
||||||
|
import { SharedModule } from './shared/shared.module';
|
||||||
|
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
|
||||||
|
import { ForbiddenComponent } from './forbidden/forbidden.component';
|
||||||
|
import { RootComponent } from './root/root.component';
|
||||||
|
import { ThemedRootComponent } from './root/themed-root.component';
|
||||||
|
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
|
||||||
|
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
||||||
|
import { ThemedHeaderComponent } from './header/themed-header.component';
|
||||||
|
import { ThemedFooterComponent } from './footer/themed-footer.component';
|
||||||
|
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
||||||
|
import {
|
||||||
|
ThemedHeaderNavbarWrapperComponent
|
||||||
|
} from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
||||||
|
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||||
|
import {
|
||||||
|
ThemedPageInternalServerErrorComponent
|
||||||
|
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||||
|
import {
|
||||||
|
PageInternalServerErrorComponent
|
||||||
|
} from './page-internal-server-error/page-internal-server-error.component';
|
||||||
|
|
||||||
|
const IMPORTS = [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule.withEntryComponents(),
|
||||||
|
NavbarModule,
|
||||||
|
NgbModule,
|
||||||
|
];
|
||||||
|
|
||||||
|
const PROVIDERS = [
|
||||||
|
];
|
||||||
|
|
||||||
|
const DECLARATIONS = [
|
||||||
|
RootComponent,
|
||||||
|
ThemedRootComponent,
|
||||||
|
HeaderComponent,
|
||||||
|
ThemedHeaderComponent,
|
||||||
|
HeaderNavbarWrapperComponent,
|
||||||
|
ThemedHeaderNavbarWrapperComponent,
|
||||||
|
AdminSidebarComponent,
|
||||||
|
ThemedAdminSidebarComponent,
|
||||||
|
AdminSidebarSectionComponent,
|
||||||
|
ExpandableAdminSidebarSectionComponent,
|
||||||
|
FooterComponent,
|
||||||
|
ThemedFooterComponent,
|
||||||
|
PageNotFoundComponent,
|
||||||
|
ThemedPageNotFoundComponent,
|
||||||
|
NotificationComponent,
|
||||||
|
NotificationsBoardComponent,
|
||||||
|
BreadcrumbsComponent,
|
||||||
|
ThemedBreadcrumbsComponent,
|
||||||
|
ForbiddenComponent,
|
||||||
|
ThemedForbiddenComponent,
|
||||||
|
IdleModalComponent,
|
||||||
|
ThemedPageInternalServerErrorComponent,
|
||||||
|
PageInternalServerErrorComponent
|
||||||
|
];
|
||||||
|
|
||||||
|
const EXPORTS = [
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
...IMPORTS
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
...PROVIDERS
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
...DECLARATIONS,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
...EXPORTS,
|
||||||
|
...DECLARATIONS,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class RootModule {
|
||||||
|
|
||||||
|
}
|
@@ -3,8 +3,8 @@
|
|||||||
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on">
|
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on">
|
||||||
<input #searchInput [@toggleAnimation]="isExpanded" [attr.aria-label]="('nav.search' | translate)" name="query"
|
<input #searchInput [@toggleAnimation]="isExpanded" [attr.aria-label]="('nav.search' | translate)" name="query"
|
||||||
formControlName="query" type="text" placeholder="{{searchExpanded ? ('nav.search' | translate) : ''}}"
|
formControlName="query" type="text" placeholder="{{searchExpanded ? ('nav.search' | translate) : ''}}"
|
||||||
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-right p-1" data-test="header-search-box">
|
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-right p-1" [attr.data-test]="'header-search-box' | dsBrowserOnly">
|
||||||
<a class="submit-icon" [routerLink]="" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" data-test="header-search-icon">
|
<a class="submit-icon" [routerLink]="" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly">
|
||||||
<em class="fas fa-search fa-lg fa-fw"></em>
|
<em class="fas fa-search fa-lg fa-fw"></em>
|
||||||
</a>
|
</a>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -10,6 +10,7 @@ import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
|
|||||||
import { SearchNavbarComponent } from './search-navbar.component';
|
import { SearchNavbarComponent } from './search-navbar.component';
|
||||||
import { PaginationServiceStub } from '../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../shared/testing/pagination-service.stub';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { BrowserOnlyMockPipe } from '../shared/testing/browser-only-mock.pipe';
|
||||||
|
|
||||||
describe('SearchNavbarComponent', () => {
|
describe('SearchNavbarComponent', () => {
|
||||||
let component: SearchNavbarComponent;
|
let component: SearchNavbarComponent;
|
||||||
@@ -44,7 +45,10 @@ describe('SearchNavbarComponent', () => {
|
|||||||
useClass: TranslateLoaderMock
|
useClass: TranslateLoaderMock
|
||||||
}
|
}
|
||||||
})],
|
})],
|
||||||
declarations: [SearchNavbarComponent],
|
declarations: [
|
||||||
|
SearchNavbarComponent,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: mockSearchService }
|
{ provide: SearchService, useValue: mockSearchService }
|
||||||
]
|
]
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
|
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
|
||||||
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
|
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
|
||||||
(click)="$event.stopPropagation();">
|
(click)="$event.stopPropagation();">
|
||||||
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" data-test="login-menu" @fadeInOut>
|
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||||
<a href="javascript:void(0);" class="dropdownLogin px-1 " [attr.aria-label]="'nav.login' |translate" (click)="$event.preventDefault()" ngbDropdownToggle>
|
<a href="javascript:void(0);" class="dropdownLogin px-1 " [attr.aria-label]="'nav.login' |translate" (click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly" ngbDropdownToggle>
|
||||||
{{ 'nav.login' | translate }}
|
{{ 'nav.login' | translate }}
|
||||||
</a>
|
</a>
|
||||||
<div class="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
|
<div class="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
|
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item" data-test="user-menu">
|
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
|
||||||
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||||
<a href="javascript:void(0);" role="button" [attr.aria-label]="'nav.logout' |translate" (click)="$event.preventDefault()" [title]="'nav.logout' | translate" class="px-1" ngbDropdownToggle>
|
<a href="javascript:void(0);" role="button" [attr.aria-label]="'nav.logout' |translate" (click)="$event.preventDefault()" [title]="'nav.logout' | translate" class="px-1" [attr.data-test]="'user-menu' | dsBrowserOnly" ngbDropdownToggle>
|
||||||
<i class="fas fa-user-circle fa-lg fa-fw"></i></a>
|
<i class="fas fa-user-circle fa-lg fa-fw"></i></a>
|
||||||
<div class="logoutDropdownMenu" ngbDropdownMenu [attr.aria-label]="'nav.logout' |translate">
|
<div class="logoutDropdownMenu" ngbDropdownMenu [attr.aria-label]="'nav.logout' |translate">
|
||||||
<ds-user-menu></ds-user-menu>
|
<ds-user-menu></ds-user-menu>
|
||||||
|
@@ -15,6 +15,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe';
|
||||||
|
|
||||||
describe('AuthNavMenuComponent', () => {
|
describe('AuthNavMenuComponent', () => {
|
||||||
|
|
||||||
@@ -77,7 +78,8 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AuthNavMenuComponent
|
AuthNavMenuComponent,
|
||||||
|
BrowserOnlyMockPipe
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: HostWindowService, useValue: window },
|
{ provide: HostWindowService, useValue: window },
|
||||||
|
@@ -8,6 +8,6 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" data-test="register">{{"login.form.new-user" | translate}}</a>
|
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" [attr.data-test]="'register' | dsBrowserOnly">{{"login.form.new-user" | translate}}</a>
|
||||||
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" data-test="forgot">{{"login.form.forgot-password" | translate}}</a>
|
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" [attr.data-test]="'forgot' | dsBrowserOnly">{{"login.form.forgot-password" | translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
placeholder="{{'login.form.email' | translate}}"
|
placeholder="{{'login.form.email' | translate}}"
|
||||||
required
|
required
|
||||||
type="email"
|
type="email"
|
||||||
data-test="email">
|
[attr.data-test]="'email' | dsBrowserOnly">
|
||||||
<label class="sr-only">{{"login.form.password" | translate}}</label>
|
<label class="sr-only">{{"login.form.password" | translate}}</label>
|
||||||
<input [attr.aria-label]="'login.form.password' |translate"
|
<input [attr.aria-label]="'login.form.password' |translate"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -19,12 +19,12 @@
|
|||||||
formControlName="password"
|
formControlName="password"
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
data-test="password">
|
[attr.data-test]="'password' | dsBrowserOnly">
|
||||||
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert"
|
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert"
|
||||||
@fadeOut>{{ (error | async) | translate }}</div>
|
@fadeOut>{{ (error | async) | translate }}</div>
|
||||||
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert"
|
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert"
|
||||||
@fadeOut>{{ (message | async) | translate }}</div>
|
@fadeOut>{{ (message | async) | translate }}</div>
|
||||||
|
|
||||||
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" data-test="login-button"
|
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" [attr.data-test]="'login-button' | dsBrowserOnly"
|
||||||
[disabled]="!form.valid"><i class="fas fa-sign-in-alt"></i> {{"login.form.submit" | translate}}</button>
|
[disabled]="!form.valid"><i class="fas fa-sign-in-alt"></i> {{"login.form.submit" | translate}}</button>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -17,6 +17,7 @@ import { storeModuleConfig } from '../../../../app.reducer';
|
|||||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||||
|
import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe';
|
||||||
|
|
||||||
describe('LogInPasswordComponent', () => {
|
describe('LogInPasswordComponent', () => {
|
||||||
|
|
||||||
@@ -57,7 +58,8 @@ describe('LogInPasswordComponent', () => {
|
|||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
LogInPasswordComponent
|
LogInPasswordComponent,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: AuthService, useClass: AuthServiceStub },
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
<div *ngIf="(error | async)" class="alert alert-danger" role="alert" @fadeOut>{{ error | async }}</div>
|
<div *ngIf="(error | async)" class="alert alert-danger" role="alert" @fadeOut>{{ error | async }}</div>
|
||||||
|
|
||||||
<button class="btn btn-lg btn-primary btn-block mt-3" (click)="logOut()" data-test="logout-button"><i class="fas fa-sign-out-alt"></i> {{"logout.form.submit" | translate}}</button>
|
<button class="btn btn-lg btn-primary btn-block mt-3" (click)="logOut()" [attr.data-test]="'logout-button' | dsBrowserOnly"><i class="fas fa-sign-out-alt"></i> {{"logout.form.submit" | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -12,6 +12,7 @@ import { Router } from '@angular/router';
|
|||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { LogOutComponent } from './log-out.component';
|
import { LogOutComponent } from './log-out.component';
|
||||||
import { RouterStub } from '../testing/router.stub';
|
import { RouterStub } from '../testing/router.stub';
|
||||||
|
import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe';
|
||||||
|
|
||||||
describe('LogOutComponent', () => {
|
describe('LogOutComponent', () => {
|
||||||
|
|
||||||
@@ -46,7 +47,8 @@ describe('LogOutComponent', () => {
|
|||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
LogOutComponent
|
LogOutComponent,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: Router, useValue: routerStub },
|
{ provide: Router, useValue: routerStub },
|
||||||
|
@@ -39,7 +39,7 @@ const menuByIDSelector = (menuID: MenuID): MemoizedSelector<AppState, MenuState>
|
|||||||
return keySelector<MenuState>(menuID, menusStateSelector);
|
return keySelector<MenuState>(menuID, menusStateSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuSectionStateSelector = (state: MenuState) => state.sections;
|
const menuSectionStateSelector = (state: MenuState) => hasValue(state) ? state.sections : {};
|
||||||
|
|
||||||
const menuSectionByIDSelector = (id: string): MemoizedSelector<MenuState, MenuSection> => {
|
const menuSectionByIDSelector = (id: string): MemoizedSelector<MenuState, MenuSection> => {
|
||||||
return menuKeySelector<MenuSection>(id, menuSectionStateSelector);
|
return menuKeySelector<MenuSection>(id, menuSectionStateSelector);
|
||||||
@@ -166,7 +166,7 @@ export class MenuService {
|
|||||||
*/
|
*/
|
||||||
isMenuCollapsed(menuID: MenuID): Observable<boolean> {
|
isMenuCollapsed(menuID: MenuID): Observable<boolean> {
|
||||||
return this.getMenu(menuID).pipe(
|
return this.getMenu(menuID).pipe(
|
||||||
map((state: MenuState) => state.collapsed)
|
map((state: MenuState) => hasValue(state) ? state.collapsed : undefined)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ export class MenuService {
|
|||||||
*/
|
*/
|
||||||
isMenuPreviewCollapsed(menuID: MenuID): Observable<boolean> {
|
isMenuPreviewCollapsed(menuID: MenuID): Observable<boolean> {
|
||||||
return this.getMenu(menuID).pipe(
|
return this.getMenu(menuID).pipe(
|
||||||
map((state: MenuState) => state.previewCollapsed)
|
map((state: MenuState) => hasValue(state) ? state.previewCollapsed : undefined)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ export class MenuService {
|
|||||||
*/
|
*/
|
||||||
isMenuVisible(menuID: MenuID): Observable<boolean> {
|
isMenuVisible(menuID: MenuID): Observable<boolean> {
|
||||||
return this.getMenu(menuID).pipe(
|
return this.getMenu(menuID).pipe(
|
||||||
map((state: MenuState) => state.visible)
|
map((state: MenuState) => hasValue(state) ? state.visible : undefined)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
>
|
>
|
||||||
<div class="card-columns row" *ngIf="objects?.hasSucceeded">
|
<div class="card-columns row" *ngIf="objects?.hasSucceeded">
|
||||||
<div class="card-column col col-sm-6 col-lg-4" *ngFor="let column of (columns$ | async)" @fadeIn>
|
<div class="card-column col col-sm-6 col-lg-4" *ngFor="let column of (columns$ | async)" @fadeIn>
|
||||||
<div class="card-element" *ngFor="let object of column" data-test="grid-object">
|
<div class="card-element" *ngFor="let object of column" [attr.data-test]="'grid-object' | dsBrowserOnly">
|
||||||
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [context]="context" [linkType]="linkType"></ds-listable-object-component-loader>
|
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [context]="context" [linkType]="linkType"></ds-listable-object-component-loader>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
(next)="goNext()"
|
(next)="goNext()"
|
||||||
>
|
>
|
||||||
<ul *ngIf="objects?.hasSucceeded" class="list-unstyled" [ngClass]="{'ml-4': selectable}">
|
<ul *ngIf="objects?.hasSucceeded" class="list-unstyled" [ngClass]="{'ml-4': selectable}">
|
||||||
<li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4 d-flex" [class.border-bottom]="hasBorder && !last" data-test="list-object">
|
<li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4 d-flex" [class.border-bottom]="hasBorder && !last" [attr.data-test]="'list-object' | dsBrowserOnly">
|
||||||
<ds-selectable-list-item-control *ngIf="selectable" [index]="i"
|
<ds-selectable-list-item-control *ngIf="selectable" [index]="i"
|
||||||
[object]="object"
|
[object]="object"
|
||||||
[selectionConfig]="selectionConfig"
|
[selectionConfig]="selectionConfig"
|
||||||
|
@@ -4,10 +4,10 @@
|
|||||||
<div *ngIf="showScopeSelector" class="input-group-prepend">
|
<div *ngIf="showScopeSelector" class="input-group-prepend">
|
||||||
<button class="scope-button btn btn-outline-secondary text-truncate" [ngbTooltip]="(selectedScope | async)?.name" type="button" (click)="openScopeModal()">{{(selectedScope | async)?.name || ('search.form.scope.all' | translate)}}</button>
|
<button class="scope-button btn btn-outline-secondary text-truncate" [ngbTooltip]="(selectedScope | async)?.name" type="button" (click)="openScopeModal()">{{(selectedScope | async)?.name || ('search.form.scope.all' | translate)}}</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" [(ngModel)]="query" name="query" class="form-control" attr.aria-label="{{ searchPlaceholder }}" data-test="search-box"
|
<input type="text" [(ngModel)]="query" name="query" class="form-control" attr.aria-label="{{ searchPlaceholder }}" [attr.data-test]="'search-box' | dsBrowserOnly"
|
||||||
[placeholder]="searchPlaceholder">
|
[placeholder]="searchPlaceholder">
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button type="submit" class="search-button btn btn-{{brandColor}}" data-test="search-button"><i class="fas fa-search"></i> {{ ('search.form.search' | translate) }}</button>
|
<button type="submit" class="search-button btn btn-{{brandColor}}" [attr.data-test]="'search-button' | dsBrowserOnly"><i class="fas fa-search"></i> {{ ('search.form.search' | translate) }}</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -13,6 +13,7 @@ import { SearchConfigurationService } from '../../core/shared/search/search-conf
|
|||||||
import { PaginationServiceStub } from '../testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../testing/pagination-service.stub';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||||
|
import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe';
|
||||||
import { SearchServiceStub } from '../testing/search-service.stub';
|
import { SearchServiceStub } from '../testing/search-service.stub';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { RouterStub } from '../testing/router.stub';
|
import { RouterStub } from '../testing/router.stub';
|
||||||
@@ -41,7 +42,10 @@ describe('SearchFormComponent', () => {
|
|||||||
{ provide: SearchConfigurationService, useValue: searchConfigService },
|
{ provide: SearchConfigurationService, useValue: searchConfigService },
|
||||||
{ provide: DSpaceObjectDataService, useValue: dspaceObjectService },
|
{ provide: DSpaceObjectDataService, useValue: dspaceObjectService },
|
||||||
],
|
],
|
||||||
declarations: [SearchFormComponent]
|
declarations: [
|
||||||
|
SearchFormComponent,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
class="filter-name d-flex" [attr.aria-controls]="regionId" [id]="toggleId"
|
class="filter-name d-flex" [attr.aria-controls]="regionId" [id]="toggleId"
|
||||||
[attr.aria-expanded]="false"
|
[attr.aria-expanded]="false"
|
||||||
[attr.aria-label]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate"
|
[attr.aria-label]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate"
|
||||||
|
[attr.data-test]="'filter-toggle' | dsBrowserOnly"
|
||||||
>
|
>
|
||||||
<h5 class="d-inline-block mb-0">
|
<h5 class="d-inline-block mb-0">
|
||||||
{{'search.filters.filter.' + filter.name + '.head'| translate}}
|
{{'search.filters.filter.' + filter.name + '.head'| translate}}
|
||||||
|
@@ -13,6 +13,7 @@ import { FilterType } from '../../models/filter-type.model';
|
|||||||
import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||||
import { SequenceService } from '../../../../core/shared/sequence.service';
|
import { SequenceService } from '../../../../core/shared/sequence.service';
|
||||||
|
import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe';
|
||||||
|
|
||||||
describe('SearchFilterComponent', () => {
|
describe('SearchFilterComponent', () => {
|
||||||
let comp: SearchFilterComponent;
|
let comp: SearchFilterComponent;
|
||||||
@@ -62,7 +63,10 @@ describe('SearchFilterComponent', () => {
|
|||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
||||||
declarations: [SearchFilterComponent],
|
declarations: [
|
||||||
|
SearchFilterComponent,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: searchServiceStub },
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
{
|
{
|
||||||
|
@@ -175,7 +175,7 @@ import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'
|
|||||||
import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component';
|
import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component';
|
||||||
import { RSSComponent } from './rss-feed/rss.component';
|
import { RSSComponent } from './rss-feed/rss.component';
|
||||||
import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component';
|
import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component';
|
||||||
|
import { BrowserOnlyPipe } from './utils/browser-only.pipe';
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SortablejsModule,
|
SortablejsModule,
|
||||||
@@ -216,7 +216,8 @@ const PIPES = [
|
|||||||
ObjectKeysPipe,
|
ObjectKeysPipe,
|
||||||
ObjectValuesPipe,
|
ObjectValuesPipe,
|
||||||
ConsolePipe,
|
ConsolePipe,
|
||||||
ObjNgFor
|
ObjNgFor,
|
||||||
|
BrowserOnlyPipe,
|
||||||
];
|
];
|
||||||
|
|
||||||
const COMPONENTS = [
|
const COMPONENTS = [
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div class="facet-filter d-block mb-3 p-3">
|
<div class="facet-filter d-block mb-3 p-3">
|
||||||
<div (click)="toggle()" class="filter-name">
|
<div (click)="toggle()" class="filter-name" [attr.data-test]="'filter-toggle' | dsBrowserOnly">
|
||||||
<h5 class="d-inline-block mb-0">
|
<h5 class="d-inline-block mb-0">
|
||||||
{{ label | translate }}
|
{{ label | translate }}
|
||||||
</h5>
|
</h5>
|
||||||
|
13
src/app/shared/testing/browser-only-mock.pipe.ts
Normal file
13
src/app/shared/testing/browser-only-mock.pipe.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support dsBrowserOnly in unit tests.
|
||||||
|
*/
|
||||||
|
@Pipe({
|
||||||
|
name: 'dsBrowserOnly'
|
||||||
|
})
|
||||||
|
export class BrowserOnlyMockPipe implements PipeTransform {
|
||||||
|
transform(value: string): string | undefined {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { MenuSection } from '../menu/menu-section.model';
|
import { MenuSection } from '../menu/menu-section.model';
|
||||||
|
import { MenuState } from '../menu/menu-state.model';
|
||||||
import { MenuID } from '../menu/menu-id.model';
|
import { MenuID } from '../menu/menu-id.model';
|
||||||
|
|
||||||
export class MenuServiceStub {
|
export class MenuServiceStub {
|
||||||
@@ -77,6 +78,10 @@ export class MenuServiceStub {
|
|||||||
return observableOf(true);
|
return observableOf(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMenu(id: MenuID): Observable<MenuState> { // todo: resolve import
|
||||||
|
return observableOf({} as MenuState);
|
||||||
|
}
|
||||||
|
|
||||||
getMenuTopSections(id: MenuID): Observable<MenuSection[]> {
|
getMenuTopSections(id: MenuID): Observable<MenuSection[]> {
|
||||||
return observableOf([this.visibleSection1, this.visibleSection2]);
|
return observableOf([this.visibleSection1, this.visibleSection2]);
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import { SharedModule } from '../shared.module';
|
|||||||
import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive.stub';
|
import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive.stub';
|
||||||
import { QueryParamsDirectiveStub } from './query-params-directive.stub';
|
import { QueryParamsDirectiveStub } from './query-params-directive.stub';
|
||||||
import { RouterLinkDirectiveStub } from './router-link-directive.stub';
|
import { RouterLinkDirectiveStub } from './router-link-directive.stub';
|
||||||
|
import { BrowserOnlyMockPipe } from './browser-only-mock.pipe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module isn't used. It serves to prevent the AoT compiler
|
* This module isn't used. It serves to prevent the AoT compiler
|
||||||
@@ -21,7 +22,8 @@ import { RouterLinkDirectiveStub } from './router-link-directive.stub';
|
|||||||
QueryParamsDirectiveStub,
|
QueryParamsDirectiveStub,
|
||||||
MySimpleItemActionComponent,
|
MySimpleItemActionComponent,
|
||||||
RouterLinkDirectiveStub,
|
RouterLinkDirectiveStub,
|
||||||
NgComponentOutletDirectiveStub
|
NgComponentOutletDirectiveStub,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
QueryParamsDirectiveStub
|
QueryParamsDirectiveStub
|
||||||
|
35
src/app/shared/utils/browser-only.pipe.ts
Normal file
35
src/app/shared/utils/browser-only.pipe.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Inject, Pipe, PipeTransform, PLATFORM_ID } from '@angular/core';
|
||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pipe that only returns its input when run in the browser.
|
||||||
|
* Used to distinguish client-side rendered content from server-side rendered content.
|
||||||
|
*
|
||||||
|
* When used with attributes as in
|
||||||
|
* ```
|
||||||
|
* [attr.data-test]="'something' | dsBrowserOnly"
|
||||||
|
* ```
|
||||||
|
* the server-side rendered HTML will not contain the `data-test` attribute.
|
||||||
|
* When rendered client-side, the HTML will contain `data-test="something"`
|
||||||
|
*
|
||||||
|
* This can be useful for end-to-end testing elements that need JS (that isn't included in SSR HTML) to function:
|
||||||
|
* By depending on `dsBrowserOnly` attributes in tests we can make sure we wait
|
||||||
|
* until such components are fully interactive before trying to interact with them.
|
||||||
|
*/
|
||||||
|
@Pipe({
|
||||||
|
name: 'dsBrowserOnly'
|
||||||
|
})
|
||||||
|
export class BrowserOnlyPipe implements PipeTransform {
|
||||||
|
constructor(
|
||||||
|
@Inject(PLATFORM_ID) private platformID: any,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
transform(value: string): string | undefined {
|
||||||
|
if (isPlatformBrowser((this.platformID))) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@
|
|||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
[class.active]="currentMode === viewModeEnum.ListElement"
|
[class.active]="currentMode === viewModeEnum.ListElement"
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
data-test="list-view">
|
[attr.data-test]="'list-view' | dsBrowserOnly">
|
||||||
<i class="fas fa-list" title="{{'search.view-switch.show-list' | translate}}"></i>
|
<i class="fas fa-list" title="{{'search.view-switch.show-list' | translate}}"></i>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="isToShow(viewModeEnum.GridElement)"
|
<a *ngIf="isToShow(viewModeEnum.GridElement)"
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
[class.active]="currentMode === viewModeEnum.GridElement"
|
[class.active]="currentMode === viewModeEnum.GridElement"
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
data-test="grid-view">
|
[attr.data-test]="'grid-view' | dsBrowserOnly">
|
||||||
<i class="fas fa-th-large" title="{{'search.view-switch.show-grid' | translate}}"></i>
|
<i class="fas fa-th-large" title="{{'search.view-switch.show-grid' | translate}}"></i>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="isToShow(viewModeEnum.DetailedListElement)"
|
<a *ngIf="isToShow(viewModeEnum.DetailedListElement)"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
[class.active]="currentMode === viewModeEnum.DetailedListElement"
|
[class.active]="currentMode === viewModeEnum.DetailedListElement"
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
data-test="detail-view">
|
[attr.data-test]="'detail-view' | dsBrowserOnly">
|
||||||
<i class="far fa-square" title="{{'search.view-switch.show-detail' | translate}}"></i>
|
<i class="far fa-square" title="{{'search.view-switch.show-detail' | translate}}"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -9,6 +9,7 @@ import { SearchService } from '../../core/shared/search/search.service';
|
|||||||
import { ViewModeSwitchComponent } from './view-mode-switch.component';
|
import { ViewModeSwitchComponent } from './view-mode-switch.component';
|
||||||
import { SearchServiceStub } from '../testing/search-service.stub';
|
import { SearchServiceStub } from '../testing/search-service.stub';
|
||||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
|
import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe';
|
||||||
|
|
||||||
@Component({ template: '' })
|
@Component({ template: '' })
|
||||||
class DummyComponent {
|
class DummyComponent {
|
||||||
@@ -36,7 +37,8 @@ describe('ViewModeSwitchComponent', () => {
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ViewModeSwitchComponent,
|
ViewModeSwitchComponent,
|
||||||
DummyComponent
|
DummyComponent,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: searchService },
|
{ provide: SearchService, useValue: searchService },
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
<button *ngIf="(showDepositAndDiscard | async)"
|
<button *ngIf="(showDepositAndDiscard | async)"
|
||||||
type="button"
|
type="button"
|
||||||
id="discard"
|
id="discard"
|
||||||
|
[attr.data-test]="'discard' | dsBrowserOnly"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
|
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
|
||||||
(click)="$event.preventDefault();confirmDiscard(content)">
|
(click)="$event.preventDefault();confirmDiscard(content)">
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
id="save"
|
id="save"
|
||||||
|
[attr.data-test]="'save' | dsBrowserOnly"
|
||||||
[disabled]="(processingSaveStatus | async) || !(hasUnsavedModification | async)"
|
[disabled]="(processingSaveStatus | async) || !(hasUnsavedModification | async)"
|
||||||
(click)="save($event)">
|
(click)="save($event)">
|
||||||
<span><i class="fas fa-save"></i> {{'submission.general.save' | translate}}</span>
|
<span><i class="fas fa-save"></i> {{'submission.general.save' | translate}}</span>
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
[class.btn-secondary]="(showDepositAndDiscard | async)"
|
[class.btn-secondary]="(showDepositAndDiscard | async)"
|
||||||
class="btn"
|
class="btn"
|
||||||
id="saveForLater"
|
id="saveForLater"
|
||||||
|
[attr.data-test]="'save-for-later' | dsBrowserOnly"
|
||||||
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
|
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
|
||||||
(click)="saveLater($event)">
|
(click)="saveLater($event)">
|
||||||
<span><i class="fas fa-save"></i> {{'submission.general.save-later' | translate}}</span>
|
<span><i class="fas fa-save"></i> {{'submission.general.save-later' | translate}}</span>
|
||||||
@@ -42,6 +45,7 @@
|
|||||||
<button *ngIf="(showDepositAndDiscard | async)"
|
<button *ngIf="(showDepositAndDiscard | async)"
|
||||||
type="button"
|
type="button"
|
||||||
id="deposit"
|
id="deposit"
|
||||||
|
[attr.data-test]="'deposit' | dsBrowserOnly"
|
||||||
class="btn btn-success"
|
class="btn btn-success"
|
||||||
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
|
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
|
||||||
(click)="deposit($event)">
|
(click)="deposit($event)">
|
||||||
|
@@ -15,12 +15,13 @@ import { SubmissionRestServiceStub } from '../../../shared/testing/submission-re
|
|||||||
import { SubmissionFormFooterComponent } from './submission-form-footer.component';
|
import { SubmissionFormFooterComponent } from './submission-form-footer.component';
|
||||||
import { SubmissionRestService } from '../../../core/submission/submission-rest.service';
|
import { SubmissionRestService } from '../../../core/submission/submission-rest.service';
|
||||||
import { createTestComponent } from '../../../shared/testing/utils.test';
|
import { createTestComponent } from '../../../shared/testing/utils.test';
|
||||||
|
import { BrowserOnlyMockPipe } from '../../../shared/testing/browser-only-mock.pipe';
|
||||||
|
|
||||||
const submissionServiceStub: SubmissionServiceStub = new SubmissionServiceStub();
|
const submissionServiceStub: SubmissionServiceStub = new SubmissionServiceStub();
|
||||||
|
|
||||||
const submissionId = mockSubmissionId;
|
const submissionId = mockSubmissionId;
|
||||||
|
|
||||||
describe('SubmissionFormFooterComponent Component', () => {
|
describe('SubmissionFormFooterComponent', () => {
|
||||||
|
|
||||||
let comp: SubmissionFormFooterComponent;
|
let comp: SubmissionFormFooterComponent;
|
||||||
let compAsAny: any;
|
let compAsAny: any;
|
||||||
@@ -36,7 +37,8 @@ describe('SubmissionFormFooterComponent Component', () => {
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
SubmissionFormFooterComponent,
|
SubmissionFormFooterComponent,
|
||||||
TestComponent
|
TestComponent,
|
||||||
|
BrowserOnlyMockPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SubmissionService, useValue: submissionServiceStub },
|
{ provide: SubmissionService, useValue: submissionServiceStub },
|
||||||
|
@@ -2,11 +2,10 @@ import { HttpClient, HttpClientModule } from '@angular/common/http';
|
|||||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser';
|
import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { RouterModule, NoPreloading } from '@angular/router';
|
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { TranslateJson5HttpLoader } from '../../ngx-translate-loaders/translate-json5-http.loader';
|
import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader';
|
||||||
|
|
||||||
import { IdlePreloadModule } from 'angular-idle-preload';
|
import { IdlePreloadModule } from 'angular-idle-preload';
|
||||||
|
|
||||||
@@ -42,8 +41,8 @@ import { environment } from '../../environments/environment';
|
|||||||
|
|
||||||
export const REQ_KEY = makeStateKey<string>('req');
|
export const REQ_KEY = makeStateKey<string>('req');
|
||||||
|
|
||||||
export function createTranslateLoader(http: HttpClient) {
|
export function createTranslateLoader(transferState: TransferState, http: HttpClient) {
|
||||||
return new TranslateJson5HttpLoader(http, 'assets/i18n/', '.json5');
|
return new TranslateBrowserLoader(transferState, http, 'assets/i18n/', '.json5');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRequest(transferState: TransferState): any {
|
export function getRequest(transferState: TransferState): any {
|
||||||
@@ -59,13 +58,6 @@ export function getRequest(transferState: TransferState): any {
|
|||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
// forRoot ensures the providers are only created once
|
// forRoot ensures the providers are only created once
|
||||||
IdlePreloadModule.forRoot(),
|
IdlePreloadModule.forRoot(),
|
||||||
RouterModule.forRoot([], {
|
|
||||||
// enableTracing: true,
|
|
||||||
useHash: false,
|
|
||||||
scrollPositionRestoration: 'enabled',
|
|
||||||
anchorScrolling: 'enabled',
|
|
||||||
preloadingStrategy: NoPreloading
|
|
||||||
}),
|
|
||||||
StatisticsModule.forRoot(),
|
StatisticsModule.forRoot(),
|
||||||
Angulartics2RouterlessModule.forRoot(),
|
Angulartics2RouterlessModule.forRoot(),
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
@@ -74,7 +66,7 @@ export function getRequest(transferState: TransferState): any {
|
|||||||
loader: {
|
loader: {
|
||||||
provide: TranslateLoader,
|
provide: TranslateLoader,
|
||||||
useFactory: (createTranslateLoader),
|
useFactory: (createTranslateLoader),
|
||||||
deps: [HttpClient]
|
deps: [TransferState, HttpClient]
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
AppModule
|
AppModule
|
||||||
@@ -92,9 +84,11 @@ export function getRequest(transferState: TransferState): any {
|
|||||||
// extend environment with app config for browser
|
// extend environment with app config for browser
|
||||||
extendEnvironmentWithAppConfig(environment, appConfig);
|
extendEnvironmentWithAppConfig(environment, appConfig);
|
||||||
}
|
}
|
||||||
dspaceTransferState.transfer();
|
return () =>
|
||||||
correlationIdService.initCorrelationId();
|
dspaceTransferState.transfer().then((b: boolean) => {
|
||||||
return () => true;
|
correlationIdService.initCorrelationId();
|
||||||
|
return b;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
deps: [TransferState, DSpaceTransferState, CorrelationIdService],
|
deps: [TransferState, DSpaceTransferState, CorrelationIdService],
|
||||||
multi: true
|
multi: true
|
||||||
|
@@ -3,7 +3,6 @@ import { APP_INITIALIZER, NgModule } from '@angular/core';
|
|||||||
import { BrowserModule, TransferState } from '@angular/platform-browser';
|
import { BrowserModule, TransferState } from '@angular/platform-browser';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { ServerModule } from '@angular/platform-server';
|
import { ServerModule } from '@angular/platform-server';
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ import { AppComponent } from '../../app/app.component';
|
|||||||
import { AppModule } from '../../app/app.module';
|
import { AppModule } from '../../app/app.module';
|
||||||
import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server-transfer-state.module';
|
import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server-transfer-state.module';
|
||||||
import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service';
|
import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service';
|
||||||
import { TranslateJson5UniversalLoader } from '../../ngx-translate-loaders/translate-json5-universal.loader';
|
import { TranslateServerLoader } from '../../ngx-translate-loaders/translate-server.loader';
|
||||||
import { CookieService } from '../../app/core/services/cookie.service';
|
import { CookieService } from '../../app/core/services/cookie.service';
|
||||||
import { ServerCookieService } from '../../app/core/services/server-cookie.service';
|
import { ServerCookieService } from '../../app/core/services/server-cookie.service';
|
||||||
import { AuthService } from '../../app/core/auth/auth.service';
|
import { AuthService } from '../../app/core/auth/auth.service';
|
||||||
@@ -37,8 +36,8 @@ import { AppConfig, APP_CONFIG_STATE } from '../../config/app-config.interface';
|
|||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
export function createTranslateLoader() {
|
export function createTranslateLoader(transferState: TransferState) {
|
||||||
return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5');
|
return new TranslateServerLoader(transferState, 'dist/server/assets/i18n/', '.json5');
|
||||||
}
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -47,16 +46,13 @@ export function createTranslateLoader() {
|
|||||||
BrowserModule.withServerTransition({
|
BrowserModule.withServerTransition({
|
||||||
appId: 'dspace-angular'
|
appId: 'dspace-angular'
|
||||||
}),
|
}),
|
||||||
RouterModule.forRoot([], {
|
|
||||||
useHash: false
|
|
||||||
}),
|
|
||||||
NoopAnimationsModule,
|
NoopAnimationsModule,
|
||||||
DSpaceServerTransferStateModule,
|
DSpaceServerTransferStateModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
loader: {
|
loader: {
|
||||||
provide: TranslateLoader,
|
provide: TranslateLoader,
|
||||||
useFactory: (createTranslateLoader),
|
useFactory: (createTranslateLoader),
|
||||||
deps: []
|
deps: [TransferState]
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
AppModule,
|
AppModule,
|
||||||
|
@@ -1,12 +1,19 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { coreSelector } from 'src/app/core/core.selectors';
|
||||||
import { StoreAction, StoreActionTypes } from '../../app/store.actions';
|
import { StoreAction, StoreActionTypes } from '../../app/store.actions';
|
||||||
import { DSpaceTransferState } from './dspace-transfer-state.service';
|
import { DSpaceTransferState } from './dspace-transfer-state.service';
|
||||||
|
import { find, map } from 'rxjs/operators';
|
||||||
|
import { isNotEmpty } from '../../app/shared/empty.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DSpaceBrowserTransferState extends DSpaceTransferState {
|
export class DSpaceBrowserTransferState extends DSpaceTransferState {
|
||||||
transfer() {
|
transfer(): Promise<boolean> {
|
||||||
const state = this.transferState.get<any>(DSpaceTransferState.NGRX_STATE, null);
|
const state = this.transferState.get<any>(DSpaceTransferState.NGRX_STATE, null);
|
||||||
this.transferState.remove(DSpaceTransferState.NGRX_STATE);
|
this.transferState.remove(DSpaceTransferState.NGRX_STATE);
|
||||||
this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state));
|
this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state));
|
||||||
|
return this.store.select(coreSelector).pipe(
|
||||||
|
find((core: any) => isNotEmpty(core)),
|
||||||
|
map(() => true)
|
||||||
|
).toPromise();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ import { DSpaceTransferState } from './dspace-transfer-state.service';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DSpaceServerTransferState extends DSpaceTransferState {
|
export class DSpaceServerTransferState extends DSpaceTransferState {
|
||||||
transfer() {
|
transfer(): Promise<boolean> {
|
||||||
this.transferState.onSerialize(DSpaceTransferState.NGRX_STATE, () => {
|
this.transferState.onSerialize(DSpaceTransferState.NGRX_STATE, () => {
|
||||||
let state;
|
let state;
|
||||||
this.store.pipe(take(1)).subscribe((saveState: any) => {
|
this.store.pipe(take(1)).subscribe((saveState: any) => {
|
||||||
@@ -14,5 +14,7 @@ export class DSpaceServerTransferState extends DSpaceTransferState {
|
|||||||
|
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return new Promise<boolean>(() => true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,5 +14,5 @@ export abstract class DSpaceTransferState {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract transfer(): void;
|
abstract transfer(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
15
src/ngx-translate-loaders/ngx-translate-state.ts
Normal file
15
src/ngx-translate-loaders/ngx-translate-state.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { makeStateKey } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents ngx-translate messages in different languages in the TransferState
|
||||||
|
*/
|
||||||
|
export class NgxTranslateState {
|
||||||
|
[lang: string]: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key to store the NgxTranslateState as part of the TransferState
|
||||||
|
*/
|
||||||
|
export const NGX_TRANSLATE_STATE = makeStateKey<NgxTranslateState>('NGX_TRANSLATE_STATE');
|
44
src/ngx-translate-loaders/translate-browser.loader.ts
Normal file
44
src/ngx-translate-loaders/translate-browser.loader.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { TranslateLoader } from '@ngx-translate/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { TransferState } from '@angular/platform-browser';
|
||||||
|
import { NGX_TRANSLATE_STATE, NgxTranslateState } from './ngx-translate-state';
|
||||||
|
import { hasValue } from '../app/shared/empty.util';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { of as observableOf, Observable } from 'rxjs';
|
||||||
|
import * as JSON5 from 'json5';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A TranslateLoader for ngx-translate to retrieve i18n messages from the TransferState, or download
|
||||||
|
* them if they're not available there
|
||||||
|
*/
|
||||||
|
export class TranslateBrowserLoader implements TranslateLoader {
|
||||||
|
constructor(
|
||||||
|
protected transferState: TransferState,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected prefix?: string,
|
||||||
|
protected suffix?: string
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the i18n messages for a given language, first try to find them in the TransferState
|
||||||
|
* retrieve them using HttpClient if they're not available there
|
||||||
|
*
|
||||||
|
* @param lang the language code
|
||||||
|
*/
|
||||||
|
getTranslation(lang: string): Observable<any> {
|
||||||
|
// Get the ngx-translate messages from the transfer state, to speed up the initial page load
|
||||||
|
// client side
|
||||||
|
const state = this.transferState.get<NgxTranslateState>(NGX_TRANSLATE_STATE, {});
|
||||||
|
const messages = state[lang];
|
||||||
|
if (hasValue(messages)) {
|
||||||
|
return observableOf(messages);
|
||||||
|
} else {
|
||||||
|
// If they're not available on the transfer state (e.g. when running in dev mode), retrieve
|
||||||
|
// them using HttpClient
|
||||||
|
return this.http.get('' + this.prefix + lang + this.suffix, { responseType: 'text' }).pipe(
|
||||||
|
map((json: any) => JSON5.parse(json))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,15 +0,0 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { TranslateLoader } from '@ngx-translate/core';
|
|
||||||
import { map } from 'rxjs/operators';
|
|
||||||
import * as JSON5 from 'json5';
|
|
||||||
|
|
||||||
export class TranslateJson5HttpLoader implements TranslateLoader {
|
|
||||||
constructor(private http: HttpClient, public prefix?: string, public suffix?: string) {
|
|
||||||
}
|
|
||||||
|
|
||||||
getTranslation(lang: string): any {
|
|
||||||
return this.http.get('' + this.prefix + lang + this.suffix, {responseType: 'text'}).pipe(
|
|
||||||
map((json: any) => JSON5.parse(json))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
import { TranslateLoader } from '@ngx-translate/core';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
const JSON5 = require('json5').default;
|
|
||||||
|
|
||||||
export class TranslateJson5UniversalLoader implements TranslateLoader {
|
|
||||||
|
|
||||||
constructor(private prefix: string = 'dist/assets/i18n/', private suffix: string = '.json') { }
|
|
||||||
|
|
||||||
public getTranslation(lang: string): Observable<any> {
|
|
||||||
return Observable.create((observer: any) => {
|
|
||||||
observer.next(JSON5.parse(fs.readFileSync(`${this.prefix}${lang}${this.suffix}`, 'utf8')));
|
|
||||||
observer.complete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
52
src/ngx-translate-loaders/translate-server.loader.ts
Normal file
52
src/ngx-translate-loaders/translate-server.loader.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { TranslateLoader } from '@ngx-translate/core';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { TransferState } from '@angular/platform-browser';
|
||||||
|
import { NGX_TRANSLATE_STATE, NgxTranslateState } from './ngx-translate-state';
|
||||||
|
|
||||||
|
const JSON5 = require('json5').default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A TranslateLoader for ngx-translate to parse json5 files server-side, and store them in the
|
||||||
|
* TransferState
|
||||||
|
*/
|
||||||
|
export class TranslateServerLoader implements TranslateLoader {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected transferState: TransferState,
|
||||||
|
protected prefix: string = 'dist/assets/i18n/',
|
||||||
|
protected suffix: string = '.json'
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the i18n messages for a given language, and store them in the TransferState
|
||||||
|
*
|
||||||
|
* @param lang the language code
|
||||||
|
*/
|
||||||
|
public getTranslation(lang: string): Observable<any> {
|
||||||
|
// Retrieve the file for the given language, and parse it
|
||||||
|
const messages = JSON5.parse(fs.readFileSync(`${this.prefix}${lang}${this.suffix}`, 'utf8'));
|
||||||
|
// Store the parsed messages in the transfer state so they'll be available immediately when the
|
||||||
|
// app loads on the client
|
||||||
|
this.storeInTransferState(lang, messages);
|
||||||
|
// Return the parsed messages to translate things server side
|
||||||
|
return observableOf(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the i18n messages for the given language code in the transfer state, so they can be
|
||||||
|
* retrieved client side
|
||||||
|
*
|
||||||
|
* @param lang the language code
|
||||||
|
* @param messages the i18n messages
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected storeInTransferState(lang: string, messages) {
|
||||||
|
const prevState = this.transferState.get<NgxTranslateState>(NGX_TRANSLATE_STATE, {});
|
||||||
|
const nextState = Object.assign({}, prevState, {
|
||||||
|
[lang]: messages
|
||||||
|
});
|
||||||
|
this.transferState.set(NGX_TRANSLATE_STATE, nextState);
|
||||||
|
}
|
||||||
|
}
|
58
src/themes/custom/eager-theme.module.ts
Normal file
58
src/themes/custom/eager-theme.module.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { SharedModule } from '../../app/shared/shared.module';
|
||||||
|
import { HomeNewsComponent } from './app/home-page/home-news/home-news.component';
|
||||||
|
import { NavbarComponent } from './app/navbar/navbar.component';
|
||||||
|
import { HeaderComponent } from './app/header/header.component';
|
||||||
|
import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component';
|
||||||
|
import { SearchModule } from '../../app/shared/search/search.module';
|
||||||
|
import { RootModule } from '../../app/root.module';
|
||||||
|
import { NavbarModule } from '../../app/navbar/navbar.module';
|
||||||
|
import { PublicationComponent } from './app/item-page/simple/item-types/publication/publication.component';
|
||||||
|
import { ItemPageModule } from '../../app/item-page/item-page.module';
|
||||||
|
import { FooterComponent } from './app/footer/footer.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS.
|
||||||
|
* This will ensure that decorator gets picked up when the app loads
|
||||||
|
*/
|
||||||
|
const ENTRY_COMPONENTS = [
|
||||||
|
PublicationComponent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const DECLARATIONS = [
|
||||||
|
...ENTRY_COMPONENTS,
|
||||||
|
HomeNewsComponent,
|
||||||
|
HeaderComponent,
|
||||||
|
HeaderNavbarWrapperComponent,
|
||||||
|
NavbarComponent,
|
||||||
|
FooterComponent,
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
SearchModule,
|
||||||
|
FormsModule,
|
||||||
|
RootModule,
|
||||||
|
NavbarModule,
|
||||||
|
ItemPageModule,
|
||||||
|
],
|
||||||
|
declarations: DECLARATIONS,
|
||||||
|
providers: [
|
||||||
|
...ENTRY_COMPONENTS.map((component) => ({ provide: component }))
|
||||||
|
],
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This module is included in the main bundle that gets downloaded at first page load. So it should
|
||||||
|
* contain only the themed components that have to be available immediately for the first page load,
|
||||||
|
* and the minimal set of imports required to make them work. Anything you can cut from it will make
|
||||||
|
* the initial page load faster, but may cause the page to flicker as components that were already
|
||||||
|
* rendered server side need to be lazy-loaded again client side
|
||||||
|
*
|
||||||
|
* Themed EntryComponents should also be added here
|
||||||
|
*/
|
||||||
|
export class EagerThemeModule {
|
||||||
|
}
|
@@ -1,5 +0,0 @@
|
|||||||
import { PublicationComponent } from './app/item-page/simple/item-types/publication/publication.component';
|
|
||||||
|
|
||||||
export const ENTRY_COMPONENTS = [
|
|
||||||
PublicationComponent
|
|
||||||
];
|
|
@@ -28,20 +28,28 @@ import { StatisticsModule } from '../../app/statistics/statistics.module';
|
|||||||
import { StoreModule } from '@ngrx/store';
|
import { StoreModule } from '@ngrx/store';
|
||||||
import { StoreRouterConnectingModule } from '@ngrx/router-store';
|
import { StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
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 { HomePageModule } from '../../app/home-page/home-page.module';
|
||||||
import { RootComponent } from './app/root/root.component';
|
|
||||||
import { AppModule } from '../../app/app.module';
|
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 { ItemPageModule } from '../../app/item-page/item-page.module';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { AccessControlModule } from '../../app/access-control/access-control.module';
|
import { CommunityListPageModule } from '../../app/community-list-page/community-list-page.module';
|
||||||
|
import { InfoModule } from '../../app/info/info.module';
|
||||||
|
import { StatisticsPageModule } from '../../app/statistics-page/statistics-page.module';
|
||||||
|
import { CommunityPageModule } from '../../app/community-page/community-page.module';
|
||||||
|
import { CollectionPageModule } from '../../app/collection-page/collection-page.module';
|
||||||
|
import { SubmissionModule } from '../../app/submission/submission.module';
|
||||||
|
import { MyDSpacePageModule } from '../../app/my-dspace-page/my-dspace-page.module';
|
||||||
|
import { SearchModule } from '../../app/shared/search/search.module';
|
||||||
|
import { ResourcePoliciesModule } from '../../app/shared/resource-policies/resource-policies.module';
|
||||||
|
import { ComcolModule } from '../../app/shared/comcol/comcol.module';
|
||||||
|
import { RootModule } from '../../app/root.module';
|
||||||
|
import { FileSectionComponent } from './app/item-page/simple/field-components/file-section/file-section.component';
|
||||||
|
import { HomePageComponent } from './app/home-page/home-page.component';
|
||||||
|
import { RootComponent } from './app/root/root.component';
|
||||||
import { BrowseBySwitcherComponent } from './app/browse-by/browse-by-switcher/browse-by-switcher.component';
|
import { BrowseBySwitcherComponent } from './app/browse-by/browse-by-switcher/browse-by-switcher.component';
|
||||||
import { CommunityListPageComponent } from './app/community-list-page/community-list-page.component';
|
import { CommunityListPageComponent } from './app/community-list-page/community-list-page.component';
|
||||||
import { CommunityListPageModule } from '../../app/community-list-page/community-list-page.module';
|
|
||||||
import { SearchPageComponent } from './app/search-page/search-page.component';
|
import { SearchPageComponent } from './app/search-page/search-page.component';
|
||||||
import { InfoModule } from '../../app/info/info.module';
|
import { ConfigurationSearchPageComponent } from './app/search-page/configuration-search-page.component';
|
||||||
import { EndUserAgreementComponent } from './app/info/end-user-agreement/end-user-agreement.component';
|
import { EndUserAgreementComponent } from './app/info/end-user-agreement/end-user-agreement.component';
|
||||||
import { PageNotFoundComponent } from './app/pagenotfound/pagenotfound.component';
|
import { PageNotFoundComponent } from './app/pagenotfound/pagenotfound.component';
|
||||||
import { ObjectNotFoundComponent } from './app/lookup-by-id/objectnotfound/objectnotfound.component';
|
import { ObjectNotFoundComponent } from './app/lookup-by-id/objectnotfound/objectnotfound.component';
|
||||||
@@ -49,14 +57,10 @@ import { ForbiddenComponent } from './app/forbidden/forbidden.component';
|
|||||||
import { PrivacyComponent } from './app/info/privacy/privacy.component';
|
import { PrivacyComponent } from './app/info/privacy/privacy.component';
|
||||||
import { CollectionStatisticsPageComponent } from './app/statistics-page/collection-statistics-page/collection-statistics-page.component';
|
import { CollectionStatisticsPageComponent } from './app/statistics-page/collection-statistics-page/collection-statistics-page.component';
|
||||||
import { CommunityStatisticsPageComponent } from './app/statistics-page/community-statistics-page/community-statistics-page.component';
|
import { CommunityStatisticsPageComponent } from './app/statistics-page/community-statistics-page/community-statistics-page.component';
|
||||||
import { StatisticsPageModule } from '../../app/statistics-page/statistics-page.module';
|
|
||||||
import { ItemStatisticsPageComponent } from './app/statistics-page/item-statistics-page/item-statistics-page.component';
|
import { ItemStatisticsPageComponent } from './app/statistics-page/item-statistics-page/item-statistics-page.component';
|
||||||
import { SiteStatisticsPageComponent } from './app/statistics-page/site-statistics-page/site-statistics-page.component';
|
import { SiteStatisticsPageComponent } from './app/statistics-page/site-statistics-page/site-statistics-page.component';
|
||||||
import { CommunityPageComponent } from './app/community-page/community-page.component';
|
import { CommunityPageComponent } from './app/community-page/community-page.component';
|
||||||
import { CollectionPageComponent } from './app/collection-page/collection-page.component';
|
import { CollectionPageComponent } from './app/collection-page/collection-page.component';
|
||||||
import { CommunityPageModule } from '../../app/community-page/community-page.module';
|
|
||||||
import { CollectionPageModule } from '../../app/collection-page/collection-page.module';
|
|
||||||
import { ConfigurationSearchPageComponent } from './app/search-page/configuration-search-page.component';
|
|
||||||
import { ItemPageComponent } from './app/item-page/simple/item-page.component';
|
import { ItemPageComponent } from './app/item-page/simple/item-page.component';
|
||||||
import { FullItemPageComponent } from './app/item-page/full/full-item-page.component';
|
import { FullItemPageComponent } from './app/item-page/full/full-item-page.component';
|
||||||
import { LoginPageComponent } from './app/login-page/login-page.component';
|
import { LoginPageComponent } from './app/login-page/login-page.component';
|
||||||
@@ -66,32 +70,21 @@ import { ForgotEmailComponent } from './app/forgot-password/forgot-password-emai
|
|||||||
import { ForgotPasswordFormComponent } from './app/forgot-password/forgot-password-form/forgot-password-form.component';
|
import { ForgotPasswordFormComponent } from './app/forgot-password/forgot-password-form/forgot-password-form.component';
|
||||||
import { ProfilePageComponent } from './app/profile-page/profile-page.component';
|
import { ProfilePageComponent } from './app/profile-page/profile-page.component';
|
||||||
import { RegisterEmailComponent } from './app/register-page/register-email/register-email.component';
|
import { RegisterEmailComponent } from './app/register-page/register-email/register-email.component';
|
||||||
|
import { MyDSpacePageComponent } from './app/my-dspace-page/my-dspace-page.component';
|
||||||
import { SubmissionEditComponent } from './app/submission/edit/submission-edit.component';
|
import { SubmissionEditComponent } from './app/submission/edit/submission-edit.component';
|
||||||
import { SubmissionImportExternalComponent } from './app/submission/import-external/submission-import-external.component';
|
import { SubmissionImportExternalComponent } from './app/submission/import-external/submission-import-external.component';
|
||||||
import { SubmissionSubmitComponent } from './app/submission/submit/submission-submit.component';
|
import { SubmissionSubmitComponent } from './app/submission/submit/submission-submit.component';
|
||||||
import { MyDSpacePageComponent } from './app/my-dspace-page/my-dspace-page.component';
|
|
||||||
import { WorkflowItemSendBackComponent } from './app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component';
|
|
||||||
import { WorkflowItemDeleteComponent } from './app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component';
|
import { WorkflowItemDeleteComponent } from './app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component';
|
||||||
import { SubmissionModule } from '../../app/submission/submission.module';
|
import { WorkflowItemSendBackComponent } from './app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component';
|
||||||
import { MyDSpacePageModule } from '../../app/my-dspace-page/my-dspace-page.module';
|
|
||||||
import { NavbarComponent } from './app/navbar/navbar.component';
|
|
||||||
import { HeaderComponent } from './app/header/header.component';
|
|
||||||
import { FooterComponent } from './app/footer/footer.component';
|
|
||||||
import { BreadcrumbsComponent } from './app/breadcrumbs/breadcrumbs.component';
|
import { BreadcrumbsComponent } from './app/breadcrumbs/breadcrumbs.component';
|
||||||
import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component';
|
|
||||||
import { FileSectionComponent } from './app/item-page/simple/field-components/file-section/file-section.component';
|
|
||||||
import { SearchModule } from '../../app/shared/search/search.module';
|
|
||||||
import { ResourcePoliciesModule } from '../../app/shared/resource-policies/resource-policies.module';
|
|
||||||
import { ComcolModule } from '../../app/shared/comcol/comcol.module';
|
|
||||||
import { FeedbackComponent } from './app/info/feedback/feedback.component';
|
import { FeedbackComponent } from './app/info/feedback/feedback.component';
|
||||||
import { CommunityListComponent } from './app/community-list-page/community-list/community-list.component';
|
import { CommunityListComponent } from './app/community-list-page/community-list/community-list.component';
|
||||||
|
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
FileSectionComponent,
|
FileSectionComponent,
|
||||||
HomePageComponent,
|
HomePageComponent,
|
||||||
HomeNewsComponent,
|
|
||||||
RootComponent,
|
RootComponent,
|
||||||
PublicationComponent,
|
|
||||||
BrowseBySwitcherComponent,
|
BrowseBySwitcherComponent,
|
||||||
CommunityListPageComponent,
|
CommunityListPageComponent,
|
||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
@@ -122,22 +115,18 @@ const DECLARATIONS = [
|
|||||||
SubmissionSubmitComponent,
|
SubmissionSubmitComponent,
|
||||||
WorkflowItemDeleteComponent,
|
WorkflowItemDeleteComponent,
|
||||||
WorkflowItemSendBackComponent,
|
WorkflowItemSendBackComponent,
|
||||||
FooterComponent,
|
|
||||||
HeaderComponent,
|
|
||||||
NavbarComponent,
|
|
||||||
HeaderNavbarWrapperComponent,
|
|
||||||
BreadcrumbsComponent,
|
BreadcrumbsComponent,
|
||||||
FeedbackComponent,
|
FeedbackComponent,
|
||||||
CommunityListComponent
|
CommunityListComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
AccessControlModule,
|
|
||||||
AdminRegistriesModule,
|
AdminRegistriesModule,
|
||||||
AdminSearchModule,
|
AdminSearchModule,
|
||||||
AdminWorkflowModuleModule,
|
AdminWorkflowModuleModule,
|
||||||
AppModule,
|
AppModule,
|
||||||
|
RootModule,
|
||||||
BitstreamFormatsModule,
|
BitstreamFormatsModule,
|
||||||
BrowseByModule,
|
BrowseByModule,
|
||||||
CollectionFormModule,
|
CollectionFormModule,
|
||||||
@@ -178,9 +167,9 @@ const DECLARATIONS = [
|
|||||||
SearchModule,
|
SearchModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ResourcePoliciesModule,
|
ResourcePoliciesModule,
|
||||||
ComcolModule
|
ComcolModule,
|
||||||
],
|
],
|
||||||
declarations: DECLARATIONS
|
declarations: DECLARATIONS,
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,5 +179,5 @@ const DECLARATIONS = [
|
|||||||
* It is purposefully not exported, it should never be imported anywhere else, its only purpose is
|
* 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
|
* to give lazily loaded components a context in which they can be compiled successfully
|
||||||
*/
|
*/
|
||||||
class ThemeModule {
|
class LazyThemeModule {
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="background-image">
|
<div class="background-image-container">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="jumbotron jumbotron-fluid">
|
<div class="jumbotron jumbotron-fluid">
|
||||||
<div class="d-flex flex-wrap">
|
<div class="d-flex flex-wrap">
|
||||||
@@ -30,5 +30,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<picture class="background-image">
|
||||||
|
<source type="image/webp" srcset="assets/dspace/images/banner.webp 2000w, assets/dspace/images/banner-half.webp 1200w, assets/dspace/images/banner-tall.webp 768w">
|
||||||
|
<source type="image/jpg" srcset="assets/dspace/images/banner.jpg 2000w, assets/dspace/images/banner-half.jpg 1200w, assets/dspace/images/banner-tall.jpg 768w">
|
||||||
|
<img alt="" [src]="'assets/dspace/images/banner.jpg'"/><!-- without the []="''" Firefox downloads both the fallback and the resolved image -->
|
||||||
|
</picture>
|
||||||
<small class="credits">Photo by <a href="https://www.pexels.com/@inspiredimages">@inspiredimages</a></small>
|
<small class="credits">Photo by <a href="https://www.pexels.com/@inspiredimages">@inspiredimages</a></small>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,12 +2,21 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-top: calc(var(--ds-content-spacing) * -1);
|
margin-top: calc(var(--ds-content-spacing) * -1);
|
||||||
|
|
||||||
div.background-image {
|
div.background-image-container {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: var(--bs-info);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
background-image: url('/assets/dspace/images/banner.jpg');
|
|
||||||
background-size: cover;
|
.background-image > img {
|
||||||
|
background-color: var(--bs-info);
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
BIN
src/themes/dspace/assets/images/banner-half.jpg
Normal file
BIN
src/themes/dspace/assets/images/banner-half.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 284 KiB |
BIN
src/themes/dspace/assets/images/banner-half.webp
Normal file
BIN
src/themes/dspace/assets/images/banner-half.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 152 KiB |
BIN
src/themes/dspace/assets/images/banner-tall.jpg
Normal file
BIN
src/themes/dspace/assets/images/banner-tall.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 275 KiB |
BIN
src/themes/dspace/assets/images/banner-tall.webp
Normal file
BIN
src/themes/dspace/assets/images/banner-tall.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 115 KiB |
BIN
src/themes/dspace/assets/images/banner.webp
Normal file
BIN
src/themes/dspace/assets/images/banner.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 361 KiB |
52
src/themes/dspace/eager-theme.module.ts
Normal file
52
src/themes/dspace/eager-theme.module.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { SharedModule } from '../../app/shared/shared.module';
|
||||||
|
import { HomeNewsComponent } from './app/home-page/home-news/home-news.component';
|
||||||
|
import { NavbarComponent } from './app/navbar/navbar.component';
|
||||||
|
import { HeaderComponent } from './app/header/header.component';
|
||||||
|
import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component';
|
||||||
|
import { SearchModule } from '../../app/shared/search/search.module';
|
||||||
|
import { RootModule } from '../../app/root.module';
|
||||||
|
import { NavbarModule } from '../../app/navbar/navbar.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS.
|
||||||
|
* This will ensure that decorator gets picked up when the app loads
|
||||||
|
*/
|
||||||
|
const ENTRY_COMPONENTS = [
|
||||||
|
];
|
||||||
|
|
||||||
|
const DECLARATIONS = [
|
||||||
|
...ENTRY_COMPONENTS,
|
||||||
|
HomeNewsComponent,
|
||||||
|
HeaderComponent,
|
||||||
|
HeaderNavbarWrapperComponent,
|
||||||
|
NavbarComponent,
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
SearchModule,
|
||||||
|
FormsModule,
|
||||||
|
RootModule,
|
||||||
|
NavbarModule,
|
||||||
|
],
|
||||||
|
declarations: DECLARATIONS,
|
||||||
|
providers: [
|
||||||
|
...ENTRY_COMPONENTS.map((component) => ({ provide: component }))
|
||||||
|
],
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This module is included in the main bundle that gets downloaded at first page load. So it should
|
||||||
|
* contain only the themed components that have to be available immediately for the first page load,
|
||||||
|
* and the minimal set of imports required to make them work. Anything you can cut from it will make
|
||||||
|
* the initial page load faster, but may cause the page to flicker as components that were already
|
||||||
|
* rendered server side need to be lazy-loaded again client side
|
||||||
|
*
|
||||||
|
* Themed EntryComponents should also be added here
|
||||||
|
*/
|
||||||
|
export class EagerThemeModule {
|
||||||
|
}
|
@@ -1,2 +0,0 @@
|
|||||||
export const ENTRY_COMPONENTS = [
|
|
||||||
];
|
|
@@ -2,10 +2,16 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { AdminRegistriesModule } from '../../app/admin/admin-registries/admin-registries.module';
|
import { AdminRegistriesModule } from '../../app/admin/admin-registries/admin-registries.module';
|
||||||
import { AdminSearchModule } from '../../app/admin/admin-search-page/admin-search.module';
|
import { AdminSearchModule } from '../../app/admin/admin-search-page/admin-search.module';
|
||||||
import { AdminWorkflowModuleModule } from '../../app/admin/admin-workflow-page/admin-workflow.module';
|
import {
|
||||||
import { BitstreamFormatsModule } from '../../app/admin/admin-registries/bitstream-formats/bitstream-formats.module';
|
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 { BrowseByModule } from '../../app/browse-by/browse-by.module';
|
||||||
import { CollectionFormModule } from '../../app/collection-page/collection-form/collection-form.module';
|
import {
|
||||||
|
CollectionFormModule
|
||||||
|
} from '../../app/collection-page/collection-form/collection-form.module';
|
||||||
import { CommunityFormModule } from '../../app/community-page/community-form/community-form.module';
|
import { CommunityFormModule } from '../../app/community-page/community-form/community-form.module';
|
||||||
import { CoreModule } from '../../app/core/core.module';
|
import { CoreModule } from '../../app/core/core.module';
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
@@ -13,14 +19,18 @@ import { EditItemPageModule } from '../../app/item-page/edit-item-page/edit-item
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { IdlePreloadModule } from 'angular-idle-preload';
|
import { IdlePreloadModule } from 'angular-idle-preload';
|
||||||
import { JournalEntitiesModule } from '../../app/entity-groups/journal-entities/journal-entities.module';
|
import {
|
||||||
|
JournalEntitiesModule
|
||||||
|
} from '../../app/entity-groups/journal-entities/journal-entities.module';
|
||||||
import { MyDspaceSearchModule } from '../../app/my-dspace-page/my-dspace-search.module';
|
import { MyDspaceSearchModule } from '../../app/my-dspace-page/my-dspace-search.module';
|
||||||
import { MenuModule } from '../../app/shared/menu/menu.module';
|
import { MenuModule } from '../../app/shared/menu/menu.module';
|
||||||
import { NavbarModule } from '../../app/navbar/navbar.module';
|
import { NavbarModule } from '../../app/navbar/navbar.module';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ProfilePageModule } from '../../app/profile-page/profile-page.module';
|
import { ProfilePageModule } from '../../app/profile-page/profile-page.module';
|
||||||
import { RegisterEmailFormModule } from '../../app/register-email-form/register-email-form.module';
|
import { RegisterEmailFormModule } from '../../app/register-email-form/register-email-form.module';
|
||||||
import { ResearchEntitiesModule } from '../../app/entity-groups/research-entities/research-entities.module';
|
import {
|
||||||
|
ResearchEntitiesModule
|
||||||
|
} from '../../app/entity-groups/research-entities/research-entities.module';
|
||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
import { SearchPageModule } from '../../app/search-page/search-page.module';
|
import { SearchPageModule } from '../../app/search-page/search-page.module';
|
||||||
import { SharedModule } from '../../app/shared/shared.module';
|
import { SharedModule } from '../../app/shared/shared.module';
|
||||||
@@ -28,7 +38,6 @@ import { StatisticsModule } from '../../app/statistics/statistics.module';
|
|||||||
import { StoreModule } from '@ngrx/store';
|
import { StoreModule } from '@ngrx/store';
|
||||||
import { StoreRouterConnectingModule } from '@ngrx/router-store';
|
import { StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { HomeNewsComponent } from './app/home-page/home-news/home-news.component';
|
|
||||||
import { HomePageModule } from '../../app/home-page/home-page.module';
|
import { HomePageModule } from '../../app/home-page/home-page.module';
|
||||||
import { AppModule } from '../../app/app.module';
|
import { AppModule } from '../../app/app.module';
|
||||||
import { ItemPageModule } from '../../app/item-page/item-page.module';
|
import { ItemPageModule } from '../../app/item-page/item-page.module';
|
||||||
@@ -40,18 +49,14 @@ import { CommunityPageModule } from '../../app/community-page/community-page.mod
|
|||||||
import { CollectionPageModule } from '../../app/collection-page/collection-page.module';
|
import { CollectionPageModule } from '../../app/collection-page/collection-page.module';
|
||||||
import { SubmissionModule } from '../../app/submission/submission.module';
|
import { SubmissionModule } from '../../app/submission/submission.module';
|
||||||
import { MyDSpacePageModule } from '../../app/my-dspace-page/my-dspace-page.module';
|
import { MyDSpacePageModule } from '../../app/my-dspace-page/my-dspace-page.module';
|
||||||
import { NavbarComponent } from './app/navbar/navbar.component';
|
|
||||||
import { HeaderComponent } from './app/header/header.component';
|
|
||||||
import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component';
|
|
||||||
import { SearchModule } from '../../app/shared/search/search.module';
|
import { SearchModule } from '../../app/shared/search/search.module';
|
||||||
import { ResourcePoliciesModule } from '../../app/shared/resource-policies/resource-policies.module';
|
import {
|
||||||
|
ResourcePoliciesModule
|
||||||
|
} from '../../app/shared/resource-policies/resource-policies.module';
|
||||||
import { ComcolModule } from '../../app/shared/comcol/comcol.module';
|
import { ComcolModule } from '../../app/shared/comcol/comcol.module';
|
||||||
|
import { RootModule } from '../../app/root.module';
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
HomeNewsComponent,
|
|
||||||
HeaderComponent,
|
|
||||||
HeaderNavbarWrapperComponent,
|
|
||||||
NavbarComponent
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -60,6 +65,7 @@ const DECLARATIONS = [
|
|||||||
AdminSearchModule,
|
AdminSearchModule,
|
||||||
AdminWorkflowModuleModule,
|
AdminWorkflowModuleModule,
|
||||||
AppModule,
|
AppModule,
|
||||||
|
RootModule,
|
||||||
BitstreamFormatsModule,
|
BitstreamFormatsModule,
|
||||||
BrowseByModule,
|
BrowseByModule,
|
||||||
CollectionFormModule,
|
CollectionFormModule,
|
||||||
@@ -100,17 +106,17 @@ const DECLARATIONS = [
|
|||||||
SearchModule,
|
SearchModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ResourcePoliciesModule,
|
ResourcePoliciesModule,
|
||||||
ComcolModule
|
ComcolModule,
|
||||||
],
|
],
|
||||||
declarations: DECLARATIONS
|
declarations: DECLARATIONS,
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module serves as an index for all the components in this theme.
|
* 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
|
* It should import all other modules, so the compiler knows where to find any components referenced
|
||||||
* from a component in this theme
|
* from a component in this theme
|
||||||
* It is purposefully not exported, it should never be imported anywhere else, its only purpose is
|
* 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
|
* to give lazily loaded components a context in which they can be compiled successfully
|
||||||
*/
|
*/
|
||||||
class ThemeModule {
|
class LazyThemeModule {
|
||||||
}
|
}
|
19
src/themes/eager-themes.module.ts
Normal file
19
src/themes/eager-themes.module.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { EagerThemeModule as DSpaceEagerThemeModule } from './dspace/eager-theme.module';
|
||||||
|
// import { EagerThemeModule as CustomEagerThemeModule } from './custom/eager-theme.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module bundles the eager theme modules for all available themes.
|
||||||
|
* Eager modules contain components that are present on every page (to speed up initial loading)
|
||||||
|
* and entry components (to ensure their decorators get picked up).
|
||||||
|
*
|
||||||
|
* Themes that aren't in use should not be imported here so they don't take up unnecessary space in the main bundle.
|
||||||
|
*/
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
DSpaceEagerThemeModule,
|
||||||
|
// CustomEagerThemeModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class EagerThemesModule {
|
||||||
|
}
|
@@ -1,23 +0,0 @@
|
|||||||
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}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -3,12 +3,37 @@ import { join } from 'path';
|
|||||||
import { buildAppConfig } from '../src/config/config.server';
|
import { buildAppConfig } from '../src/config/config.server';
|
||||||
import { commonExports } from './webpack.common';
|
import { commonExports } from './webpack.common';
|
||||||
|
|
||||||
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
|
||||||
module.exports = Object.assign({}, commonExports, {
|
module.exports = Object.assign({}, commonExports, {
|
||||||
target: 'web',
|
target: 'web',
|
||||||
|
plugins: [
|
||||||
|
...commonExports.plugins,
|
||||||
|
new CompressionPlugin({
|
||||||
|
filename: '[path][base].gz',
|
||||||
|
algorithm: 'gzip',
|
||||||
|
test: /\.(js|css|html|svg|json5)$/,
|
||||||
|
threshold: 10240,
|
||||||
|
minRatio: 0.8,
|
||||||
|
}),
|
||||||
|
new CompressionPlugin({
|
||||||
|
filename: '[path][base].br',
|
||||||
|
algorithm: 'brotliCompress',
|
||||||
|
test: /\.(js|css|html|svg|json5)$/,
|
||||||
|
compressionOptions: {
|
||||||
|
params: {
|
||||||
|
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
threshold: 10240,
|
||||||
|
minRatio: 0.8,
|
||||||
|
}),
|
||||||
|
],
|
||||||
devServer: {
|
devServer: {
|
||||||
setupMiddlewares(middlewares, server) {
|
setupMiddlewares(middlewares, server) {
|
||||||
buildAppConfig(join(process.cwd(), 'src/assets/config.json'));
|
buildAppConfig(join(process.cwd(), 'src/assets/config.json'));
|
||||||
return middlewares;
|
return middlewares;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
75
yarn.lock
75
yarn.lock
@@ -4119,17 +4119,13 @@ compressible@~2.0.16:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime-db ">= 1.43.0 < 2"
|
mime-db ">= 1.43.0 < 2"
|
||||||
|
|
||||||
compression-webpack-plugin@^3.0.1:
|
compression-webpack-plugin@^9.2.0:
|
||||||
version "3.1.0"
|
version "9.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-3.1.0.tgz#9f510172a7b5fae5aad3b670652e8bd7997aeeca"
|
resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-9.2.0.tgz#57fd539d17c5907eebdeb4e83dcfe2d7eceb9ef6"
|
||||||
integrity sha512-iqTHj3rADN4yHwXMBrQa/xrncex/uEQy8QHlaTKxGchT/hC0SdlJlmL/5eRqffmWq2ep0/Romw6Ld39JjTR/ug==
|
integrity sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw==
|
||||||
dependencies:
|
dependencies:
|
||||||
cacache "^13.0.1"
|
schema-utils "^4.0.0"
|
||||||
find-cache-dir "^3.0.0"
|
serialize-javascript "^6.0.0"
|
||||||
neo-async "^2.5.0"
|
|
||||||
schema-utils "^2.6.1"
|
|
||||||
serialize-javascript "^2.1.2"
|
|
||||||
webpack-sources "^1.0.1"
|
|
||||||
|
|
||||||
compression@^1.7.4:
|
compression@^1.7.4:
|
||||||
version "1.7.4"
|
version "1.7.4"
|
||||||
@@ -4924,6 +4920,11 @@ dependency-graph@^0.11.0:
|
|||||||
resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27"
|
resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27"
|
||||||
integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==
|
integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==
|
||||||
|
|
||||||
|
destroy@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
||||||
|
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
|
||||||
|
|
||||||
destroy@~1.0.4:
|
destroy@~1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
||||||
@@ -5827,6 +5828,13 @@ express-rate-limit@^5.1.3:
|
|||||||
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.5.1.tgz#110c23f6a65dfa96ab468eda95e71697bc6987a2"
|
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.5.1.tgz#110c23f6a65dfa96ab468eda95e71697bc6987a2"
|
||||||
integrity sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==
|
integrity sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==
|
||||||
|
|
||||||
|
express-static-gzip@^2.1.5:
|
||||||
|
version "2.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/express-static-gzip/-/express-static-gzip-2.1.5.tgz#e45b512ef5596a068c45f729d7e0cc0b429b08b4"
|
||||||
|
integrity sha512-bgiQ1fY7ltuUrSzg0WoN7ycoAd7r2VEw7durn/3k0jCMUC5wydF0K36ouIuJPE+MNDwK5uoSaVgIBVNemwxWgw==
|
||||||
|
dependencies:
|
||||||
|
serve-static "^1.14.1"
|
||||||
|
|
||||||
express@^4.17.1:
|
express@^4.17.1:
|
||||||
version "4.17.3"
|
version "4.17.3"
|
||||||
resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1"
|
resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1"
|
||||||
@@ -6079,7 +6087,7 @@ finalhandler@1.1.2, finalhandler@~1.1.2:
|
|||||||
statuses "~1.5.0"
|
statuses "~1.5.0"
|
||||||
unpipe "~1.0.0"
|
unpipe "~1.0.0"
|
||||||
|
|
||||||
find-cache-dir@^3.0.0, find-cache-dir@^3.3.1:
|
find-cache-dir@^3.3.1:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
|
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
|
||||||
integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==
|
integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==
|
||||||
@@ -8831,7 +8839,7 @@ negotiator@0.6.3, negotiator@^0.6.2, negotiator@^0.6.3:
|
|||||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||||
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||||
|
|
||||||
neo-async@^2.5.0, neo-async@^2.6.2:
|
neo-async@^2.6.2:
|
||||||
version "2.6.2"
|
version "2.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||||
@@ -9201,6 +9209,13 @@ obuf@^1.0.0, obuf@^1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
|
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
|
||||||
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
|
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
|
||||||
|
|
||||||
|
on-finished@2.4.1:
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||||
|
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
||||||
|
dependencies:
|
||||||
|
ee-first "1.1.1"
|
||||||
|
|
||||||
on-finished@~2.3.0:
|
on-finished@~2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||||
@@ -11273,7 +11288,7 @@ schema-utils@2.7.0:
|
|||||||
ajv "^6.12.2"
|
ajv "^6.12.2"
|
||||||
ajv-keywords "^3.4.1"
|
ajv-keywords "^3.4.1"
|
||||||
|
|
||||||
schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6:
|
schema-utils@^2.6.5, schema-utils@^2.6.6:
|
||||||
version "2.7.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
|
||||||
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
|
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
|
||||||
@@ -11397,10 +11412,24 @@ send@0.17.2:
|
|||||||
range-parser "~1.2.1"
|
range-parser "~1.2.1"
|
||||||
statuses "~1.5.0"
|
statuses "~1.5.0"
|
||||||
|
|
||||||
serialize-javascript@^2.1.2:
|
send@0.18.0:
|
||||||
version "2.1.2"
|
version "0.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
|
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
|
||||||
integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
|
integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
|
||||||
|
dependencies:
|
||||||
|
debug "2.6.9"
|
||||||
|
depd "2.0.0"
|
||||||
|
destroy "1.2.0"
|
||||||
|
encodeurl "~1.0.2"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
etag "~1.8.1"
|
||||||
|
fresh "0.5.2"
|
||||||
|
http-errors "2.0.0"
|
||||||
|
mime "1.6.0"
|
||||||
|
ms "2.1.3"
|
||||||
|
on-finished "2.4.1"
|
||||||
|
range-parser "~1.2.1"
|
||||||
|
statuses "2.0.1"
|
||||||
|
|
||||||
serialize-javascript@^4.0.0:
|
serialize-javascript@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
@@ -11456,6 +11485,16 @@ serve-static@1.14.2:
|
|||||||
parseurl "~1.3.3"
|
parseurl "~1.3.3"
|
||||||
send "0.17.2"
|
send "0.17.2"
|
||||||
|
|
||||||
|
serve-static@^1.14.1:
|
||||||
|
version "1.15.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
|
||||||
|
integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
|
||||||
|
dependencies:
|
||||||
|
encodeurl "~1.0.2"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
parseurl "~1.3.3"
|
||||||
|
send "0.18.0"
|
||||||
|
|
||||||
server-destroy@1.0.1:
|
server-destroy@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/server-destroy/-/server-destroy-1.0.1.tgz#f13bf928e42b9c3e79383e61cc3998b5d14e6cdd"
|
resolved "https://registry.yarnpkg.com/server-destroy/-/server-destroy-1.0.1.tgz#f13bf928e42b9c3e79383e61cc3998b5d14e6cdd"
|
||||||
@@ -13026,7 +13065,7 @@ webpack-merge@5.8.0, webpack-merge@^5.7.3:
|
|||||||
clone-deep "^4.0.1"
|
clone-deep "^4.0.1"
|
||||||
wildcard "^2.0.0"
|
wildcard "^2.0.0"
|
||||||
|
|
||||||
webpack-sources@^1.0.1, webpack-sources@^1.4.3:
|
webpack-sources@^1.4.3:
|
||||||
version "1.4.3"
|
version "1.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
|
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
|
||||||
integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
|
integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
|
||||||
|
Reference in New Issue
Block a user