mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'dspace-7_x' into accessibility-settings-7_x
# Conflicts: # config/config.example.yml # src/config/app-config.interface.ts # src/config/default-app-config.ts # src/environments/environment.test.ts
This commit is contained in:
@@ -17,12 +17,34 @@ ui:
|
|||||||
# Trust X-FORWARDED-* headers from proxies (default = true)
|
# Trust X-FORWARDED-* headers from proxies (default = true)
|
||||||
useProxies: true
|
useProxies: true
|
||||||
|
|
||||||
# Angular Server Side Rendering (SSR) settings
|
# Angular Universal / Server Side Rendering (SSR) settings
|
||||||
ssr:
|
universal:
|
||||||
# Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
|
# Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
|
||||||
# Determining which styles are critical is a relatively expensive operation; this option is
|
# Determining which styles are critical is a relatively expensive operation; this option is
|
||||||
# disabled (false) by default to boost server performance at the expense of loading smoothness.
|
# disabled (false) by default to boost server performance at the expense of loading smoothness.
|
||||||
inlineCriticalCss: false
|
inlineCriticalCss: false
|
||||||
|
# Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects.
|
||||||
|
# NOTE: The "/handle/" path ensures Handle redirects work via SSR. The "/reload/" path ensures
|
||||||
|
# hard refreshes (e.g. after login) trigger SSR while fully reloading the page.
|
||||||
|
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ]
|
||||||
|
# Whether to enable rendering of Search component on SSR.
|
||||||
|
# If set to true the component will be included in the HTML returned from the server side rendering.
|
||||||
|
# If set to false the component will not be included in the HTML returned from the server side rendering.
|
||||||
|
enableSearchComponent: false
|
||||||
|
# Whether to enable rendering of Browse component on SSR.
|
||||||
|
# If set to true the component will be included in the HTML returned from the server side rendering.
|
||||||
|
# If set to false the component will not be included in the HTML returned from the server side rendering.
|
||||||
|
enableBrowseComponent: false
|
||||||
|
# Enable state transfer from the server-side application to the client-side application.
|
||||||
|
# Defaults to true.
|
||||||
|
# Note: When using an external application cache layer, it's recommended not to transfer the state to avoid caching it.
|
||||||
|
# Disabling it ensures that dynamic state information is not inadvertently cached, which can improve security and
|
||||||
|
# ensure that users always use the most up-to-date state.
|
||||||
|
transferState: true
|
||||||
|
# When a different REST base URL is used for the server-side application, the generated state contains references to
|
||||||
|
# REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs.
|
||||||
|
# Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues.
|
||||||
|
replaceRestUrl: true
|
||||||
|
|
||||||
# The REST API server settings
|
# The REST API server settings
|
||||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||||
@@ -33,6 +55,9 @@ rest:
|
|||||||
port: 443
|
port: 443
|
||||||
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
nameSpace: /server
|
nameSpace: /server
|
||||||
|
# Provide a different REST url to be used during SSR execution. It must contain the whole url including protocol, server port and
|
||||||
|
# server namespace (uncomment to use it).
|
||||||
|
#ssrBaseUrl: http://localhost:8080/server
|
||||||
|
|
||||||
# Caching settings
|
# Caching settings
|
||||||
cache:
|
cache:
|
||||||
@@ -411,6 +436,15 @@ liveRegion:
|
|||||||
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
|
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
|
||||||
isVisible: false
|
isVisible: false
|
||||||
|
|
||||||
|
|
||||||
|
# Search settings
|
||||||
|
search:
|
||||||
|
# Number used to render n UI elements called loading skeletons that act as placeholders.
|
||||||
|
# These elements indicate that some content will be loaded in their stead.
|
||||||
|
# Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
|
||||||
|
# e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
|
||||||
|
defaultFiltersCount: 5
|
||||||
|
|
||||||
# Configuration for storing accessibility settings, used by the AccessibilitySettingsService
|
# Configuration for storing accessibility settings, used by the AccessibilitySettingsService
|
||||||
accessibility:
|
accessibility:
|
||||||
# The duration in days after which the accessibility settings cookie expires
|
# The duration in days after which the accessibility settings cookie expires
|
||||||
|
@@ -9,6 +9,7 @@ describe('Admin Add New Modals', () => {
|
|||||||
|
|
||||||
it('Add new Community modal should pass accessibility tests', () => {
|
it('Add new Community modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
@@ -23,6 +24,7 @@ describe('Admin Add New Modals', () => {
|
|||||||
|
|
||||||
it('Add new Collection modal should pass accessibility tests', () => {
|
it('Add new Collection modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
@@ -37,6 +39,7 @@ describe('Admin Add New Modals', () => {
|
|||||||
|
|
||||||
it('Add new Item modal should pass accessibility tests', () => {
|
it('Add new Item modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
|
@@ -9,6 +9,7 @@ describe('Admin Edit Modals', () => {
|
|||||||
|
|
||||||
it('Edit Community modal should pass accessibility tests', () => {
|
it('Edit Community modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
@@ -23,6 +24,7 @@ describe('Admin Edit Modals', () => {
|
|||||||
|
|
||||||
it('Edit Collection modal should pass accessibility tests', () => {
|
it('Edit Collection modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
@@ -37,6 +39,7 @@ describe('Admin Edit Modals', () => {
|
|||||||
|
|
||||||
it('Edit Item modal should pass accessibility tests', () => {
|
it('Edit Item modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
|
@@ -9,6 +9,7 @@ describe('Admin Export Modals', () => {
|
|||||||
|
|
||||||
it('Export metadata modal should pass accessibility tests', () => {
|
it('Export metadata modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
@@ -23,6 +24,7 @@ describe('Admin Export Modals', () => {
|
|||||||
|
|
||||||
it('Export batch modal should pass accessibility tests', () => {
|
it('Export batch modal should pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
// Click on entry of menu
|
// Click on entry of menu
|
||||||
|
23
package.json
23
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dspace-angular",
|
"name": "dspace-angular",
|
||||||
"version": "7.6.3-next",
|
"version": "7.6.4-next",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"config:watch": "nodemon",
|
"config:watch": "nodemon",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"@angular/platform-browser-dynamic": "^15.2.10",
|
"@angular/platform-browser-dynamic": "^15.2.10",
|
||||||
"@angular/platform-server": "^15.2.10",
|
"@angular/platform-server": "^15.2.10",
|
||||||
"@angular/router": "^15.2.10",
|
"@angular/router": "^15.2.10",
|
||||||
"@babel/runtime": "7.26.0",
|
"@babel/runtime": "7.26.7",
|
||||||
"@kolkov/ngx-gallery": "^2.0.1",
|
"@kolkov/ngx-gallery": "^2.0.1",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||||
"@ng-dynamic-forms/core": "^15.0.0",
|
"@ng-dynamic-forms/core": "^15.0.0",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"compression": "^1.7.5",
|
"compression": "^1.7.5",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"core-js": "^3.39.0",
|
"core-js": "^3.40.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"date-fns-tz": "^1.3.7",
|
"date-fns-tz": "^1.3.7",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
@@ -91,18 +91,18 @@
|
|||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"http-proxy-middleware": "^2.0.7",
|
"http-proxy-middleware": "^2.0.7",
|
||||||
"http-terminator": "^3.2.0",
|
"http-terminator": "^3.2.0",
|
||||||
"isbot": "^5.1.21",
|
"isbot": "^5.1.22",
|
||||||
"js-cookie": "2.2.1",
|
"js-cookie": "2.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jsonschema": "1.4.1",
|
"jsonschema": "1.5.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"klaro": "^0.7.21",
|
"klaro": "^0.7.21",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lru-cache": "^7.14.1",
|
"lru-cache": "^7.14.1",
|
||||||
"markdown-it": "^13.0.2",
|
"markdown-it": "^13.0.2",
|
||||||
"markdown-it-mathjax3": "^4.3.2",
|
"markdown-it-mathjax3": "^4.3.2",
|
||||||
"mirador": "^3.4.2",
|
"mirador": "^3.4.3",
|
||||||
"mirador-dl-plugin": "^0.13.0",
|
"mirador-dl-plugin": "^0.13.0",
|
||||||
"mirador-share-plugin": "^0.16.0",
|
"mirador-share-plugin": "^0.16.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
@@ -110,6 +110,7 @@
|
|||||||
"ng2-nouislider": "^2.0.0",
|
"ng2-nouislider": "^2.0.0",
|
||||||
"ngx-infinite-scroll": "^15.0.0",
|
"ngx-infinite-scroll": "^15.0.0",
|
||||||
"ngx-pagination": "6.0.3",
|
"ngx-pagination": "6.0.3",
|
||||||
|
"ngx-skeleton-loader": "^7.0.0",
|
||||||
"ngx-sortablejs": "^11.1.0",
|
"ngx-sortablejs": "^11.1.0",
|
||||||
"ngx-ui-switch": "^14.1.0",
|
"ngx-ui-switch": "^14.1.0",
|
||||||
"nouislider": "^15.8.1",
|
"nouislider": "^15.8.1",
|
||||||
@@ -145,7 +146,7 @@
|
|||||||
"@types/grecaptcha": "^3.0.9",
|
"@types/grecaptcha": "^3.0.9",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/js-cookie": "2.2.6",
|
"@types/js-cookie": "2.2.6",
|
||||||
"@types/lodash": "^4.17.13",
|
"@types/lodash": "^4.17.15",
|
||||||
"@types/node": "^14.18.63",
|
"@types/node": "^14.18.63",
|
||||||
"@types/sanitize-html": "^2.13.0",
|
"@types/sanitize-html": "^2.13.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
@@ -156,13 +157,13 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"csstype": "^3.1.3",
|
"csstype": "^3.1.3",
|
||||||
"cypress": "^13.17.0",
|
"cypress": "^13.17.0",
|
||||||
"cypress-axe": "^1.5.0",
|
"cypress-axe": "^1.6.0",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.39.0",
|
||||||
"eslint-plugin-deprecation": "^1.5.0",
|
"eslint-plugin-deprecation": "^1.5.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-jsdoc": "^45.0.0",
|
"eslint-plugin-jsdoc": "^45.0.0",
|
||||||
"eslint-plugin-jsonc": "^2.18.2",
|
"eslint-plugin-jsonc": "^2.19.1",
|
||||||
"eslint-plugin-lodash": "^7.4.0",
|
"eslint-plugin-lodash": "^7.4.0",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"express-static-gzip": "^2.2.0",
|
"express-static-gzip": "^2.2.0",
|
||||||
@@ -177,7 +178,7 @@
|
|||||||
"ng-mocks": "^14.13.2",
|
"ng-mocks": "^14.13.2",
|
||||||
"ngx-mask": "^13.1.7",
|
"ngx-mask": "^13.1.7",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"postcss": "^8.4",
|
"postcss": "^8.5",
|
||||||
"postcss-import": "^14.0.0",
|
"postcss-import": "^14.0.0",
|
||||||
"postcss-loader": "^4.0.3",
|
"postcss-loader": "^4.0.3",
|
||||||
"postcss-preset-env": "^7.4.2",
|
"postcss-preset-env": "^7.4.2",
|
||||||
@@ -186,7 +187,7 @@
|
|||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "~1.83.1",
|
"sass": "~1.84.0",
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"sass-resources-loader": "^2.2.5",
|
"sass-resources-loader": "^2.2.5",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
|
16
server.ts
16
server.ts
@@ -79,6 +79,9 @@ let anonymousCache: LRU<string, any>;
|
|||||||
// extend environment with app config for server
|
// extend environment with app config for server
|
||||||
extendEnvironmentWithAppConfig(environment, appConfig);
|
extendEnvironmentWithAppConfig(environment, appConfig);
|
||||||
|
|
||||||
|
// The REST server base URL
|
||||||
|
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
|
||||||
|
|
||||||
// The Express app is exported so that it can be used by serverless Functions.
|
// The Express app is exported so that it can be used by serverless Functions.
|
||||||
export function app() {
|
export function app() {
|
||||||
|
|
||||||
@@ -176,7 +179,7 @@ export function app() {
|
|||||||
* Proxy the sitemaps
|
* Proxy the sitemaps
|
||||||
*/
|
*/
|
||||||
router.use('/sitemap**', createProxyMiddleware({
|
router.use('/sitemap**', createProxyMiddleware({
|
||||||
target: `${environment.rest.baseUrl}/sitemaps`,
|
target: `${REST_BASE_URL}/sitemaps`,
|
||||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}));
|
}));
|
||||||
@@ -185,7 +188,7 @@ export function app() {
|
|||||||
* Proxy the linksets
|
* Proxy the linksets
|
||||||
*/
|
*/
|
||||||
router.use('/signposting**', createProxyMiddleware({
|
router.use('/signposting**', createProxyMiddleware({
|
||||||
target: `${environment.rest.baseUrl}`,
|
target: `${REST_BASE_URL}`,
|
||||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}));
|
}));
|
||||||
@@ -238,7 +241,7 @@ export function app() {
|
|||||||
* The callback function to serve server side angular
|
* The callback function to serve server side angular
|
||||||
*/
|
*/
|
||||||
function ngApp(req, res) {
|
function ngApp(req, res) {
|
||||||
if (environment.universal.preboot) {
|
if (environment.universal.preboot && req.method === 'GET' && (req.path === '/' || environment.universal.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) {
|
||||||
// Render the page to user via SSR (server side rendering)
|
// Render the page to user via SSR (server side rendering)
|
||||||
serverSideRender(req, res);
|
serverSideRender(req, res);
|
||||||
} else {
|
} else {
|
||||||
@@ -269,6 +272,11 @@ function serverSideRender(req, res, sendToUser: boolean = true) {
|
|||||||
requestUrl: req.originalUrl,
|
requestUrl: req.originalUrl,
|
||||||
}, (err, data) => {
|
}, (err, data) => {
|
||||||
if (hasNoValue(err) && hasValue(data)) {
|
if (hasNoValue(err) && hasValue(data)) {
|
||||||
|
// Replace REST URL with UI URL
|
||||||
|
if (environment.universal.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
|
||||||
|
data = data.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// save server side rendered page to cache (if any are enabled)
|
// save server side rendered page to cache (if any are enabled)
|
||||||
saveToCache(req, data);
|
saveToCache(req, data);
|
||||||
if (sendToUser) {
|
if (sendToUser) {
|
||||||
@@ -621,7 +629,7 @@ function start() {
|
|||||||
* The callback function to serve health check requests
|
* The callback function to serve health check requests
|
||||||
*/
|
*/
|
||||||
function healthCheck(req, res) {
|
function healthCheck(req, res) {
|
||||||
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
|
const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
|
||||||
axios.get(baseUrl)
|
axios.get(baseUrl)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
res.status(response.status).send(response.data);
|
res.status(response.status).send(response.data);
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<button class="btn btn-outline-primary mr-3" (click)="reset()">
|
<button class="btn btn-outline-primary mr-3" (click)="reset()">
|
||||||
{{ 'access-control-cancel' | translate }}
|
{{ 'access-control-cancel' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="!canExport()" (click)="submit()">
|
<button class="btn btn-primary" [dsBtnDisabled]="!canExport()" (click)="submit()">
|
||||||
{{ 'access-control-execute' | translate }}
|
{{ 'access-control-execute' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -27,6 +27,7 @@ import { RequestService } from '../../core/data/request.service';
|
|||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||||
|
import {BtnDisabledDirective} from '../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
describe('EPeopleRegistryComponent', () => {
|
describe('EPeopleRegistryComponent', () => {
|
||||||
let component: EPeopleRegistryComponent;
|
let component: EPeopleRegistryComponent;
|
||||||
@@ -131,7 +132,7 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
declarations: [EPeopleRegistryComponent],
|
declarations: [EPeopleRegistryComponent, BtnDisabledDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
@@ -239,7 +240,8 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
it('should be disabled', () => {
|
it('should be disabled', () => {
|
||||||
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
||||||
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
|
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
expect(deleteButton.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -25,7 +25,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="displayResetPassword" between class="btn-group">
|
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
|
<button class="btn btn-primary" [dsBtnDisabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
|
||||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -35,6 +35,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import {BtnDisabledDirective} from '../../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
describe('EPersonFormComponent', () => {
|
describe('EPersonFormComponent', () => {
|
||||||
let component: EPersonFormComponent;
|
let component: EPersonFormComponent;
|
||||||
@@ -195,7 +196,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
RouterTestingModule,
|
RouterTestingModule,
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
],
|
],
|
||||||
declarations: [EPersonFormComponent],
|
declarations: [EPersonFormComponent, BtnDisabledDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
{ provide: GroupDataService, useValue: groupsDataService },
|
{ provide: GroupDataService, useValue: groupsDataService },
|
||||||
@@ -481,7 +482,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
// ePersonDataServiceStub.activeEPerson = eperson;
|
// ePersonDataServiceStub.activeEPerson = eperson;
|
||||||
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204));
|
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204));
|
||||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(false);
|
expect(deleteButton.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||||
|
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
deleteButton.triggerEventHandler('click', null);
|
deleteButton.triggerEventHandler('click', null);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
||||||
|
@@ -34,7 +34,7 @@
|
|||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button (click)="deleteMemberFromGroup(eperson)"
|
<button (click)="deleteMemberFromGroup(eperson)"
|
||||||
[disabled]="actionConfig.remove.disabled"
|
[dsBtnDisabled]="actionConfig.remove.disabled"
|
||||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(eperson) } }}">
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button (click)="addMemberToGroup(eperson)"
|
<button (click)="addMemberToGroup(eperson)"
|
||||||
[disabled]="actionConfig.add.disabled"
|
[dsBtnDisabled]="actionConfig.add.disabled"
|
||||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
|
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||||
<i [ngClass]="actionConfig.add.icon"></i>
|
<i [ngClass]="actionConfig.add.icon"></i>
|
||||||
|
@@ -69,7 +69,7 @@
|
|||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button *ngSwitchCase="false"
|
<button *ngSwitchCase="false"
|
||||||
[disabled]="true"
|
[dsBtnDisabled]="true"
|
||||||
class="btn btn-outline-primary btn-sm btn-edit"
|
class="btn btn-outline-primary btn-sm btn-edit"
|
||||||
placement="left"
|
placement="left"
|
||||||
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
|
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
|
||||||
|
@@ -34,6 +34,7 @@ import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
|||||||
import { NoContent } from '../../core/shared/NoContent.model';
|
import { NoContent } from '../../core/shared/NoContent.model';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock, UNDEFINED_NAME } from '../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock, UNDEFINED_NAME } from '../../shared/mocks/dso-name.service.mock';
|
||||||
|
import {BtnDisabledDirective} from '../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
describe('GroupsRegistryComponent', () => {
|
describe('GroupsRegistryComponent', () => {
|
||||||
let component: GroupsRegistryComponent;
|
let component: GroupsRegistryComponent;
|
||||||
@@ -171,7 +172,7 @@ describe('GroupsRegistryComponent', () => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
declarations: [GroupsRegistryComponent],
|
declarations: [GroupsRegistryComponent, BtnDisabledDirective],
|
||||||
providers: [GroupsRegistryComponent,
|
providers: [GroupsRegistryComponent,
|
||||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
@@ -231,7 +232,8 @@ describe('GroupsRegistryComponent', () => {
|
|||||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||||
expect(editButtonsFound.length).toEqual(2);
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
editButtonsFound.forEach((editButtonFound) => {
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||||
|
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -265,7 +267,8 @@ describe('GroupsRegistryComponent', () => {
|
|||||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||||
expect(editButtonsFound.length).toEqual(2);
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
editButtonsFound.forEach((editButtonFound) => {
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||||
|
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -284,7 +287,8 @@ describe('GroupsRegistryComponent', () => {
|
|||||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||||
expect(editButtonsFound.length).toEqual(2);
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
editButtonsFound.forEach((editButtonFound) => {
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
expect(editButtonFound.nativeElement.disabled).toBeTrue();
|
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -67,8 +67,8 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
|||||||
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
||||||
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
|
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
|
||||||
this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||||
this.isExpanded$ = combineLatestObservable([this.active, this.isSidebarCollapsed$, this.isSidebarPreviewCollapsed$]).pipe(
|
this.isExpanded$ = combineLatestObservable([this.active$, this.isSidebarCollapsed$, this.isSidebarPreviewCollapsed$]).pipe(
|
||||||
map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed)))
|
map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -30,6 +30,7 @@ import { EagerThemesModule } from '../themes/eager-themes.module';
|
|||||||
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
||||||
import { StoreDevModules } from '../config/store/devtools';
|
import { StoreDevModules } from '../config/store/devtools';
|
||||||
import { RootModule } from './root.module';
|
import { RootModule } from './root.module';
|
||||||
|
import { DspaceRestInterceptor } from './core/dspace-rest/dspace-rest.interceptor';
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return environment;
|
return environment;
|
||||||
@@ -103,6 +104,12 @@ const PROVIDERS = [
|
|||||||
useClass: LogInterceptor,
|
useClass: LogInterceptor,
|
||||||
multi: true
|
multi: true
|
||||||
},
|
},
|
||||||
|
// register DspaceRestInterceptor as HttpInterceptor
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: DspaceRestInterceptor,
|
||||||
|
multi: true
|
||||||
|
},
|
||||||
// register the dynamic matcher used by form. MUST be provided by the app module
|
// register the dynamic matcher used by form. MUST be provided by the app module
|
||||||
...DYNAMIC_MATCHER_PROVIDERS,
|
...DYNAMIC_MATCHER_PROVIDERS,
|
||||||
];
|
];
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<ng-container *ngVar="(bitstreamRD$ | async) as bitstreamRD">
|
<ng-container *ngVar="(bitstreamRD$ | async) as bitstreamRD">
|
||||||
<div class="container" *ngVar="(bitstreamFormatsRD$ | async) as formatsRD">
|
<div class="container">
|
||||||
<div class="row" *ngIf="bitstreamRD?.hasSucceeded && formatsRD?.hasSucceeded">
|
<div class="row" *ngIf="bitstreamRD?.hasSucceeded">
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<ds-themed-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-themed-thumbnail>
|
<ds-themed-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-themed-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
||||||
<ds-themed-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading"
|
<ds-themed-loading *ngIf="!bitstreamRD || bitstreamRD?.isLoading"
|
||||||
message="{{'loading.bitstream' | translate}}"></ds-themed-loading>
|
message="{{'loading.bitstream' | translate}}"></ds-themed-loading>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -239,7 +239,7 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should select the correct format', () => {
|
it('should select the correct format', () => {
|
||||||
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id);
|
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.shortDescription);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should put the \"New Format\" input on invisible', () => {
|
it('should put the \"New Format\" input on invisible', () => {
|
||||||
@@ -270,7 +270,13 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
|
|
||||||
describe('when an unknown format is selected', () => {
|
describe('when an unknown format is selected', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.updateNewFormatLayout(allFormats[0].id);
|
comp.onChange({
|
||||||
|
model: {
|
||||||
|
id: 'selectedFormat',
|
||||||
|
value: allFormats[0],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
comp.updateNewFormatLayout();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove the invisible class from the \"New Format\" input', () => {
|
it('should remove the invisible class from the \"New Format\" input', () => {
|
||||||
@@ -372,10 +378,11 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
|
|
||||||
describe('when selected format has changed', () => {
|
describe('when selected format has changed', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.formGroup.patchValue({
|
comp.onChange({
|
||||||
formatContainer: {
|
model: {
|
||||||
selectedFormat: allFormats[2].id
|
id: 'selectedFormat',
|
||||||
}
|
value: allFormats[2],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
comp.onSubmit();
|
comp.onSubmit();
|
||||||
|
@@ -2,14 +2,20 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
|
|||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
import {
|
||||||
import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core';
|
combineLatest,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
Subscription, take
|
||||||
|
} from 'rxjs';
|
||||||
|
import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core';
|
||||||
import { UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||||
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload } from '../../core/shared/operators';
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload } from '../../core/shared/operators';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
||||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||||
@@ -18,7 +24,6 @@ import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/em
|
|||||||
import { Metadata } from '../../core/shared/metadata.utils';
|
import { Metadata } from '../../core/shared/metadata.utils';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
|
||||||
import { getEntityEditRoute } from '../../item-page/item-page-routing-paths';
|
import { getEntityEditRoute } from '../../item-page/item-page-routing-paths';
|
||||||
import { Bundle } from '../../core/shared/bundle.model';
|
import { Bundle } from '../../core/shared/bundle.model';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
@@ -26,6 +31,8 @@ import { Item } from '../../core/shared/item.model';
|
|||||||
import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
||||||
import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
|
import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
|
||||||
import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service';
|
import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service';
|
||||||
|
import { DynamicScrollableDropdownModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||||
|
import { FindAllDataImpl } from '../../core/data/base/find-all-data';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-bitstream-page',
|
selector: 'ds-edit-bitstream-page',
|
||||||
@@ -44,12 +51,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
bitstreamRD$: Observable<RemoteData<Bitstream>>;
|
bitstreamRD$: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
/**
|
|
||||||
* The formats their remote data observable
|
|
||||||
* Tracks changes and updates the view
|
|
||||||
*/
|
|
||||||
bitstreamFormatsRD$: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The UUID of the primary bitstream for this bundle
|
* The UUID of the primary bitstream for this bundle
|
||||||
*/
|
*/
|
||||||
@@ -65,11 +66,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
originalFormat: BitstreamFormat;
|
originalFormat: BitstreamFormat;
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of all available bitstream formats
|
|
||||||
*/
|
|
||||||
formats: BitstreamFormat[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {string} Key prefix used to generate form messages
|
* @type {string} Key prefix used to generate form messages
|
||||||
*/
|
*/
|
||||||
@@ -113,7 +109,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Options for fetching all bitstream formats
|
* Options for fetching all bitstream formats
|
||||||
*/
|
*/
|
||||||
findAllOptions = { elementsPerPage: 9999 };
|
findAllOptions = {
|
||||||
|
elementsPerPage: 20,
|
||||||
|
currentPage: 1
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Dynamic Input Model for the file's name
|
* The Dynamic Input Model for the file's name
|
||||||
@@ -153,9 +152,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* The Dynamic Input Model for the selected format
|
* The Dynamic Input Model for the selected format
|
||||||
*/
|
*/
|
||||||
selectedFormatModel = new DynamicSelectModel({
|
selectedFormatModel = new DynamicScrollableDropdownModel({
|
||||||
id: 'selectedFormat',
|
id: 'selectedFormat',
|
||||||
name: 'selectedFormat'
|
name: 'selectedFormat',
|
||||||
|
displayKey: 'shortDescription',
|
||||||
|
repeatable: false,
|
||||||
|
metadataFields: [],
|
||||||
|
submissionId: '',
|
||||||
|
hasSelectableMetadata: false,
|
||||||
|
findAllFactory: this.findAllFormatsServiceFactory(),
|
||||||
|
formatFunction: (format: BitstreamFormat | string) => {
|
||||||
|
if (format instanceof BitstreamFormat) {
|
||||||
|
return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription;
|
||||||
|
} else {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -370,6 +382,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private bundle: Bundle;
|
private bundle: Bundle;
|
||||||
|
/**
|
||||||
|
* The currently selected format
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private selectedFormat: BitstreamFormat;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute,
|
constructor(private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -396,18 +413,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
this.itemId = this.route.snapshot.queryParams.itemId;
|
this.itemId = this.route.snapshot.queryParams.itemId;
|
||||||
this.entityType = this.route.snapshot.queryParams.entityType;
|
this.entityType = this.route.snapshot.queryParams.entityType;
|
||||||
this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream));
|
this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream));
|
||||||
this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions);
|
|
||||||
|
|
||||||
const bitstream$ = this.bitstreamRD$.pipe(
|
const bitstream$ = this.bitstreamRD$.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const allFormats$ = this.bitstreamFormatsRD$.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const bundle$ = bitstream$.pipe(
|
const bundle$ = bitstream$.pipe(
|
||||||
switchMap((bitstream: Bitstream) => bitstream.bundle),
|
switchMap((bitstream: Bitstream) => bitstream.bundle),
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
@@ -423,24 +434,31 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
switchMap((bundle: Bundle) => bundle.item),
|
switchMap((bundle: Bundle) => bundle.item),
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
);
|
);
|
||||||
|
const format$ = bitstream$.pipe(
|
||||||
|
switchMap(bitstream => bitstream.format),
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
bitstream$,
|
bitstream$,
|
||||||
allFormats$,
|
|
||||||
bundle$,
|
bundle$,
|
||||||
primaryBitstream$,
|
primaryBitstream$,
|
||||||
item$,
|
item$,
|
||||||
).pipe()
|
format$,
|
||||||
.subscribe(([bitstream, allFormats, bundle, primaryBitstream, item]) => {
|
).subscribe(([bitstream, bundle, primaryBitstream, item, format]) => {
|
||||||
this.bitstream = bitstream as Bitstream;
|
this.bitstream = bitstream as Bitstream;
|
||||||
this.formats = allFormats.page;
|
|
||||||
this.bundle = bundle;
|
this.bundle = bundle;
|
||||||
|
this.selectedFormat = format;
|
||||||
// hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
|
// hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
|
||||||
// be a success response, but empty
|
// be a success response, but empty
|
||||||
this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
|
this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
|
||||||
this.itemId = item.uuid;
|
this.itemId = item.uuid;
|
||||||
this.setIiifStatus(this.bitstream);
|
this.setIiifStatus(this.bitstream);
|
||||||
})
|
}),
|
||||||
|
format$.pipe(take(1)).subscribe(
|
||||||
|
(format) => this.originalFormat = format,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
@@ -456,7 +474,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
setForm() {
|
setForm() {
|
||||||
this.formGroup = this.formService.createFormGroup(this.formModel);
|
this.formGroup = this.formService.createFormGroup(this.formModel);
|
||||||
this.updateFormatModel();
|
|
||||||
this.updateForm(this.bitstream);
|
this.updateForm(this.bitstream);
|
||||||
this.updateFieldTranslations();
|
this.updateFieldTranslations();
|
||||||
}
|
}
|
||||||
@@ -475,8 +492,9 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
description: bitstream.firstMetadataValue('dc.description')
|
description: bitstream.firstMetadataValue('dc.description')
|
||||||
},
|
},
|
||||||
formatContainer: {
|
formatContainer: {
|
||||||
newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined
|
selectedFormat: this.selectedFormat.shortDescription,
|
||||||
}
|
newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (this.isIIIF) {
|
if (this.isIIIF) {
|
||||||
this.formGroup.patchValue({
|
this.formGroup.patchValue({
|
||||||
@@ -494,36 +512,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.bitstream.format.pipe(
|
this.updateNewFormatLayout();
|
||||||
getAllSucceededRemoteDataPayload()
|
|
||||||
).subscribe((format: BitstreamFormat) => {
|
|
||||||
this.originalFormat = format;
|
|
||||||
this.formGroup.patchValue({
|
|
||||||
formatContainer: {
|
|
||||||
selectedFormat: format.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.updateNewFormatLayout(format.id);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the list of unknown format IDs an add options to the selectedFormatModel
|
|
||||||
*/
|
|
||||||
updateFormatModel() {
|
|
||||||
this.selectedFormatModel.options = this.formats.map((format: BitstreamFormat) =>
|
|
||||||
Object.assign({
|
|
||||||
value: format.id,
|
|
||||||
label: this.isUnknownFormat(format.id) ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the layout of the "Other Format" input depending on the selected format
|
* Update the layout of the "Other Format" input depending on the selected format
|
||||||
* @param selectedId
|
* @param selectedId
|
||||||
*/
|
*/
|
||||||
updateNewFormatLayout(selectedId: string) {
|
updateNewFormatLayout() {
|
||||||
if (this.isUnknownFormat(selectedId)) {
|
if (this.isUnknownFormat()) {
|
||||||
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout;
|
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout;
|
||||||
} else {
|
} else {
|
||||||
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible';
|
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible';
|
||||||
@@ -534,9 +532,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
* Is the provided format (id) part of the list of unknown formats?
|
* Is the provided format (id) part of the list of unknown formats?
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
isUnknownFormat(id: string): boolean {
|
isUnknownFormat(): boolean {
|
||||||
const format = this.formats.find((f: BitstreamFormat) => f.id === id);
|
return hasValue(this.selectedFormat) && this.selectedFormat.supportLevel === BitstreamFormatSupportLevel.Unknown;
|
||||||
return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -568,7 +565,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
onChange(event) {
|
onChange(event) {
|
||||||
const model = event.model;
|
const model = event.model;
|
||||||
if (model.id === this.selectedFormatModel.id) {
|
if (model.id === this.selectedFormatModel.id) {
|
||||||
this.updateNewFormatLayout(model.value);
|
this.selectedFormat = model.value;
|
||||||
|
this.updateNewFormatLayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,8 +576,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
onSubmit() {
|
onSubmit() {
|
||||||
const updatedValues = this.formGroup.getRawValue();
|
const updatedValues = this.formGroup.getRawValue();
|
||||||
const updatedBitstream = this.formToBitstream(updatedValues);
|
const updatedBitstream = this.formToBitstream(updatedValues);
|
||||||
const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat);
|
const isNewFormat = this.selectedFormat.id !== this.originalFormat.id;
|
||||||
const isNewFormat = selectedFormat.id !== this.originalFormat.id;
|
|
||||||
const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream;
|
const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream;
|
||||||
const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid;
|
const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid;
|
||||||
|
|
||||||
@@ -631,7 +628,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
bundle$ = observableOf(this.bundle);
|
bundle$ = observableOf(this.bundle);
|
||||||
}
|
}
|
||||||
if (isNewFormat) {
|
if (isNewFormat) {
|
||||||
bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe(
|
bitstream$ = this.bitstreamService.updateFormat(this.bitstream, this.selectedFormat).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
map((formatResponse: RemoteData<Bitstream>) => {
|
map((formatResponse: RemoteData<Bitstream>) => {
|
||||||
if (hasValue(formatResponse) && formatResponse.hasFailed) {
|
if (hasValue(formatResponse) && formatResponse.hasFailed) {
|
||||||
@@ -789,4 +786,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
.forEach((subscription) => subscription.unsubscribe());
|
.forEach((subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findAllFormatsServiceFactory() {
|
||||||
|
return () => this.bitstreamFormatService as any as FindAllDataImpl<BitstreamFormat>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { BrowseByDatePageComponent } from './browse-by-date-page.component';
|
import { BrowseByDatePageComponent } from './browse-by-date-page.component';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -9,7 +9,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { RouterMock } from '../../shared/mocks/router.mock';
|
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||||
import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectorRef, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
@@ -24,6 +24,8 @@ import { APP_CONFIG } from '../../../config/app-config.interface';
|
|||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||||
import { cold } from 'jasmine-marbles';
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||||
|
|
||||||
describe('BrowseByDatePageComponent', () => {
|
describe('BrowseByDatePageComponent', () => {
|
||||||
let comp: BrowseByDatePageComponent;
|
let comp: BrowseByDatePageComponent;
|
||||||
@@ -95,7 +97,10 @@ describe('BrowseByDatePageComponent', () => {
|
|||||||
{ provide: Router, useValue: new RouterMock() },
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: ChangeDetectorRef, useValue: mockCdRef },
|
{ provide: ChangeDetectorRef, useValue: mockCdRef },
|
||||||
{ provide: APP_CONFIG, useValue: environment }
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
|
{ provide: Store, useValue: {} },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -131,4 +136,33 @@ describe('BrowseByDatePageComponent', () => {
|
|||||||
//expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
|
//expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
|
||||||
expect(comp.startsWithOptions[0]).toEqual(1960);
|
expect(comp.startsWithOptions[0]).toEqual(1960);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when rendered in SSR', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.platformId = 'server';
|
||||||
|
spyOn((comp as any).browseService, 'getBrowseItemsFor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call getBrowseItemsFor on init', (done) => {
|
||||||
|
comp.ngOnInit();
|
||||||
|
expect((comp as any).browseService.getBrowseItemsFor).not.toHaveBeenCalled();
|
||||||
|
comp.loading$.subscribe((res) => {
|
||||||
|
expect(res).toBeFalsy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when rendered in CSR', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.platformId = 'browser';
|
||||||
|
spyOn((comp as any).browseService, 'getBrowseItemsFor').and.returnValue(createSuccessfulRemoteDataObject$(new BrowseEntry()));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getBrowseItemsFor on init', fakeAsync(() => {
|
||||||
|
comp.ngOnInit();
|
||||||
|
tick(100);
|
||||||
|
expect((comp as any).browseService.getBrowseItemsFor).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
import { ChangeDetectorRef, Component, Inject, PLATFORM_ID } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
BrowseByMetadataPageComponent,
|
BrowseByMetadataPageComponent,
|
||||||
browseParamsToOptions
|
browseParamsToOptions
|
||||||
@@ -10,7 +10,8 @@ import { BrowseService } from '../../core/browse/browse.service';
|
|||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { map, distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { isValidDate } from '../../shared/date.util';
|
import { isValidDate } from '../../shared/date.util';
|
||||||
@@ -18,6 +19,8 @@ import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
|||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { isPlatformServer } from '@angular/common';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-date-page',
|
selector: 'ds-browse-by-date-page',
|
||||||
@@ -44,25 +47,33 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
protected cdRef: ChangeDetectorRef,
|
protected cdRef: ChangeDetectorRef,
|
||||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
|
@Inject(PLATFORM_ID) public platformId: any,
|
||||||
) {
|
) {
|
||||||
super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService);
|
super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService, platformId);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
if (!this.renderOnServerSide && !environment.universal.enableBrowseComponent && isPlatformServer(this.platformId)) {
|
||||||
|
this.loading$ = observableOf(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
||||||
this.startsWithType = StartsWithType.date;
|
this.startsWithType = StartsWithType.date;
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||||
this.subs.push(
|
const routeParams$: Observable<Params> = observableCombineLatest([
|
||||||
observableCombineLatest(
|
this.route.params,
|
||||||
[ this.route.params.pipe(take(1)),
|
|
||||||
this.route.queryParams,
|
this.route.queryParams,
|
||||||
|
]).pipe(
|
||||||
|
map(([params, queryParams]: [Params, Params]) => Object.assign({}, params, queryParams)),
|
||||||
|
distinctUntilChanged((prev: Params, curr: Params) => prev.id === curr.id && prev.startsWith === curr.startsWith),
|
||||||
|
);
|
||||||
|
this.subs.push(
|
||||||
|
observableCombineLatest([
|
||||||
|
routeParams$,
|
||||||
this.currentPagination$,
|
this.currentPagination$,
|
||||||
this.currentSort$]).pipe(
|
this.currentSort$,
|
||||||
map(([routeParams, queryParams, currentPage, currentSort]) => {
|
]).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||||
return [Object.assign({}, routeParams, queryParams), currentPage, currentSort];
|
|
||||||
})
|
|
||||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
|
||||||
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
|
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
|
||||||
this.browseId = params.id || this.defaultBrowseId;
|
this.browseId = params.id || this.defaultBrowseId;
|
||||||
this.startsWith = +params.startsWith || params.startsWith;
|
this.startsWith = +params.startsWith || params.startsWith;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="container">
|
<div class="container" *ngIf="(!ssrRenderingDisabled)">
|
||||||
<ng-container *ngVar="(parent$ | async) as parent">
|
<ng-container *ngVar="(parent$ | async) as parent">
|
||||||
<ng-container *ngIf="parent?.payload as parentContext">
|
<ng-container *ngIf="parent?.payload as parentContext">
|
||||||
<div class="d-flex flex-row border-bottom mb-4 pb-4">
|
<div class="d-flex flex-row border-bottom mb-4 pb-4">
|
||||||
|
@@ -3,7 +3,7 @@ import {
|
|||||||
browseParamsToOptions,
|
browseParamsToOptions,
|
||||||
getBrowseSearchOptions
|
getBrowseSearchOptions
|
||||||
} from './browse-by-metadata-page.component';
|
} from './browse-by-metadata-page.component';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
@@ -13,7 +13,7 @@ import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { PageInfo } from '../../core/shared/page-info.model';
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
@@ -111,7 +111,8 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: Router, useValue: new RouterMock() },
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
{ provide: APP_CONFIG, useValue: environmentMock }
|
{ provide: APP_CONFIG, useValue: environmentMock },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -224,6 +225,35 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
expect(result.fetchThumbnail).toBeTrue();
|
expect(result.fetchThumbnail).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when rendered in SSR', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.ssrRenderingDisabled = true;
|
||||||
|
spyOn((comp as any).browseService, 'getBrowseEntriesFor').and.returnValue(createSuccessfulRemoteDataObject$(null));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call getBrowseEntriesFor on init', (done) => {
|
||||||
|
comp.ngOnInit();
|
||||||
|
expect((comp as any).browseService.getBrowseEntriesFor).not.toHaveBeenCalled();
|
||||||
|
comp.loading$.subscribe((res) => {
|
||||||
|
expect(res).toBeFalsy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when rendered in CSR', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.ssrRenderingDisabled = false;
|
||||||
|
spyOn((comp as any).browseService, 'getBrowseEntriesFor').and.returnValue(createSuccessfulRemoteDataObject$(new BrowseEntry()));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getBrowseEntriesFor on init', fakeAsync(() => {
|
||||||
|
comp.ngOnInit();
|
||||||
|
tick(100);
|
||||||
|
expect((comp as any).browseService.getBrowseEntriesFor).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export function toRemoteData(objects: any[]): Observable<RemoteData<PaginatedList<any>>> {
|
export function toRemoteData(objects: any[]): Observable<RemoteData<PaginatedList<any>>> {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable, Subscription, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, Subscription, of as observableOf } from 'rxjs';
|
||||||
import { Component, Inject, OnInit, OnDestroy } from '@angular/core';
|
import { Component, Inject, OnInit, OnDestroy, Input, PLATFORM_ID } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
@@ -15,13 +15,15 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
|
|||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { filter, map, mergeMap, take } from 'rxjs/operators';
|
import { filter, map, mergeMap, distinctUntilChanged } from 'rxjs/operators';
|
||||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { Collection } from '../../core/shared/collection.model';
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { isPlatformServer } from '@angular/common';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
export const BBM_PAGINATION_ID = 'bbm';
|
export const BBM_PAGINATION_ID = 'bbm';
|
||||||
|
|
||||||
@@ -37,7 +39,10 @@ export const BBM_PAGINATION_ID = 'bbm';
|
|||||||
* 'dc.contributor.*'
|
* 'dc.contributor.*'
|
||||||
*/
|
*/
|
||||||
export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* Defines whether to fetch search results during SSR execution
|
||||||
|
*/
|
||||||
|
@Input() renderOnServerSide = false;
|
||||||
/**
|
/**
|
||||||
* The list of browse-entries to display
|
* The list of browse-entries to display
|
||||||
*/
|
*/
|
||||||
@@ -126,6 +131,10 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
|||||||
* Observable determining if the loading animation needs to be shown
|
* Observable determining if the loading animation needs to be shown
|
||||||
*/
|
*/
|
||||||
loading$ = observableOf(true);
|
loading$ = observableOf(true);
|
||||||
|
/**
|
||||||
|
* Whether this component should be rendered or not in SSR
|
||||||
|
*/
|
||||||
|
ssrRenderingDisabled = false;
|
||||||
|
|
||||||
public constructor(protected route: ActivatedRoute,
|
public constructor(protected route: ActivatedRoute,
|
||||||
protected browseService: BrowseService,
|
protected browseService: BrowseService,
|
||||||
@@ -134,6 +143,7 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
|||||||
protected router: Router,
|
protected router: Router,
|
||||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
|
@Inject(PLATFORM_ID) public platformId: any,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
|
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||||
@@ -142,25 +152,31 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
|||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: this.appConfig.browseBy.pageSize,
|
pageSize: this.appConfig.browseBy.pageSize,
|
||||||
});
|
});
|
||||||
|
this.ssrRenderingDisabled = !this.renderOnServerSide && !environment.universal.enableBrowseComponent && isPlatformServer(this.platformId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
if (this.ssrRenderingDisabled) {
|
||||||
|
this.loading$ = observableOf(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
||||||
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
|
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||||
this.subs.push(
|
const routeParams$: Observable<Params> = observableCombineLatest([
|
||||||
observableCombineLatest(
|
this.route.params,
|
||||||
[ this.route.params.pipe(take(1)),
|
|
||||||
this.route.queryParams,
|
this.route.queryParams,
|
||||||
|
]).pipe(
|
||||||
|
map(([params, queryParams]: [Params, Params]) => Object.assign({}, params, queryParams)),
|
||||||
|
distinctUntilChanged((prev: Params, curr: Params) => prev.id === curr.id && prev.authority === curr.authority && prev.value === curr.value && prev.startsWith === curr.startsWith),
|
||||||
|
);
|
||||||
|
this.subs.push(
|
||||||
|
observableCombineLatest([
|
||||||
|
routeParams$,
|
||||||
this.currentPagination$,
|
this.currentPagination$,
|
||||||
this.currentSort$]).pipe(
|
this.currentSort$,
|
||||||
map(([routeParams, queryParams, currentPage, currentSort]) => {
|
]).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||||
return [Object.assign({}, routeParams, queryParams),currentPage,currentSort];
|
|
||||||
})
|
|
||||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
|
||||||
this.browseId = params.id || this.defaultBrowseId;
|
this.browseId = params.id || this.defaultBrowseId;
|
||||||
this.authority = params.authority;
|
this.authority = params.authority;
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
@@ -22,6 +22,7 @@ import { PaginationService } from '../../core/pagination/pagination.service';
|
|||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
import { APP_CONFIG } from '../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../config/app-config.interface';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||||
|
|
||||||
|
|
||||||
describe('BrowseByTitlePageComponent', () => {
|
describe('BrowseByTitlePageComponent', () => {
|
||||||
@@ -63,7 +64,8 @@ describe('BrowseByTitlePageComponent', () => {
|
|||||||
|
|
||||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
params: observableOf({}),
|
params: observableOf({}),
|
||||||
data: observableOf({ metadata: 'title' })
|
queryParams: observableOf({}),
|
||||||
|
data: observableOf({ metadata: 'title' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const paginationService = new PaginationServiceStub();
|
const paginationService = new PaginationServiceStub();
|
||||||
@@ -97,4 +99,35 @@ describe('BrowseByTitlePageComponent', () => {
|
|||||||
expect(result.payload.page).toEqual(mockItems);
|
expect(result.payload.page).toEqual(mockItems);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when rendered in SSR', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.platformId = 'server';
|
||||||
|
spyOn((comp as any).browseService, 'getBrowseItemsFor');
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call getBrowseItemsFor on init', (done) => {
|
||||||
|
comp.ngOnInit();
|
||||||
|
expect((comp as any).browseService.getBrowseItemsFor).not.toHaveBeenCalled();
|
||||||
|
comp.loading$.subscribe((res) => {
|
||||||
|
expect(res).toBeFalsy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when rendered in CSR', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.platformId = 'browser';
|
||||||
|
fixture.detectChanges();
|
||||||
|
spyOn((comp as any).browseService, 'getBrowseItemsFor').and.returnValue(createSuccessfulRemoteDataObject$(new BrowseEntry()));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getBrowseItemsFor on init', fakeAsync(() => {
|
||||||
|
comp.ngOnInit();
|
||||||
|
tick(100);
|
||||||
|
expect((comp as any).browseService.getBrowseItemsFor).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject, PLATFORM_ID } from '@angular/core';
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import {
|
import {
|
||||||
@@ -10,10 +10,13 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
|
|||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { map, distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
|
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { isPlatformServer } from '@angular/common';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-title-page',
|
selector: 'ds-browse-by-title-page',
|
||||||
@@ -32,20 +35,32 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
protected router: Router,
|
protected router: Router,
|
||||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
|
@Inject(PLATFORM_ID) public platformId: any,
|
||||||
) {
|
) {
|
||||||
super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService);
|
super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService, platformId);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
if (!this.renderOnServerSide && !environment.universal.enableBrowseComponent && isPlatformServer(this.platformId)) {
|
||||||
|
this.loading$ = observableOf(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||||
|
const routeParams$: Observable<Params> = observableCombineLatest([
|
||||||
|
this.route.params,
|
||||||
|
this.route.queryParams,
|
||||||
|
]).pipe(
|
||||||
|
map(([params, queryParams]: [Params, Params]) => Object.assign({}, params, queryParams)),
|
||||||
|
distinctUntilChanged((prev: Params, curr: Params) => prev.id === curr.id && prev.startsWith === curr.startsWith),
|
||||||
|
);
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest([this.route.params.pipe(take(1)), this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe(
|
observableCombineLatest([
|
||||||
map(([routeParams, queryParams, currentPage, currentSort]) => {
|
routeParams$,
|
||||||
return [Object.assign({}, routeParams, queryParams),currentPage,currentSort];
|
this.currentPagination$,
|
||||||
})
|
this.currentSort$,
|
||||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
]).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||||
this.startsWith = +params.startsWith || params.startsWith;
|
this.startsWith = +params.startsWith || params.startsWith;
|
||||||
this.browseId = params.id || this.defaultBrowseId;
|
this.browseId = params.id || this.defaultBrowseId;
|
||||||
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
|
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
|
||||||
|
@@ -6,10 +6,10 @@
|
|||||||
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
|
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col text-right space-children-mr">
|
<div class="col text-right space-children-mr">
|
||||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
|
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [dsBtnDisabled]="(processing$ | async)">
|
||||||
<i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}}
|
<i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
|
<button class="btn btn-danger" (click)="onConfirm(dso)" [dsBtnDisabled]="(processing$ | async)">
|
||||||
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span>
|
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span>
|
||||||
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span>
|
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -19,32 +19,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button *ngIf="!(testConfigRunning$ |async)" class="btn btn-secondary"
|
<button *ngIf="!(testConfigRunning$ |async)" class="btn btn-secondary"
|
||||||
[disabled]="!(isEnabled)"
|
[dsBtnDisabled]="!(isEnabled)"
|
||||||
(click)="testConfiguration(contentSource)">
|
(click)="testConfiguration(contentSource)">
|
||||||
<span>{{'collection.source.controls.test.submit' | translate}}</span>
|
<span>{{'collection.source.controls.test.submit' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="(testConfigRunning$ |async)" class="btn btn-secondary"
|
<button *ngIf="(testConfigRunning$ |async)" class="btn btn-secondary"
|
||||||
[disabled]="true">
|
[dsBtnDisabled]="true">
|
||||||
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
||||||
<span>{{'collection.source.controls.test.running' | translate}}</span>
|
<span>{{'collection.source.controls.test.running' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="!(importRunning$ |async)" class="btn btn-primary"
|
<button *ngIf="!(importRunning$ |async)" class="btn btn-primary"
|
||||||
[disabled]="!(isEnabled)"
|
[dsBtnDisabled]="!(isEnabled)"
|
||||||
(click)="importNow()">
|
(click)="importNow()">
|
||||||
<span class="d-none d-sm-inline">{{'collection.source.controls.import.submit' | translate}}</span>
|
<span class="d-none d-sm-inline">{{'collection.source.controls.import.submit' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="(importRunning$ |async)" class="btn btn-primary"
|
<button *ngIf="(importRunning$ |async)" class="btn btn-primary"
|
||||||
[disabled]="true">
|
[dsBtnDisabled]="true">
|
||||||
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
||||||
<span class="d-none d-sm-inline">{{'collection.source.controls.import.running' | translate}}</span>
|
<span class="d-none d-sm-inline">{{'collection.source.controls.import.running' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="!(reImportRunning$ |async)" class="btn btn-primary"
|
<button *ngIf="!(reImportRunning$ |async)" class="btn btn-primary"
|
||||||
[disabled]="!(isEnabled)"
|
[dsBtnDisabled]="!(isEnabled)"
|
||||||
(click)="resetAndReimport()">
|
(click)="resetAndReimport()">
|
||||||
<span class="d-none d-sm-inline"> {{'collection.source.controls.reset.submit' | translate}}</span>
|
<span class="d-none d-sm-inline"> {{'collection.source.controls.reset.submit' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="(reImportRunning$ |async)" class="btn btn-primary"
|
<button *ngIf="(reImportRunning$ |async)" class="btn btn-primary"
|
||||||
[disabled]="true">
|
[dsBtnDisabled]="true">
|
||||||
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
||||||
<span class="d-none d-sm-inline"> {{'collection.source.controls.reset.running' | translate}}</span>
|
<span class="d-none d-sm-inline"> {{'collection.source.controls.reset.running' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -22,6 +22,7 @@ import { TestScheduler } from 'rxjs/testing';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../../shared/utils/var.directive';
|
||||||
import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer';
|
import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer';
|
||||||
|
import {BtnDisabledDirective} from '../../../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
describe('CollectionSourceControlsComponent', () => {
|
describe('CollectionSourceControlsComponent', () => {
|
||||||
let comp: CollectionSourceControlsComponent;
|
let comp: CollectionSourceControlsComponent;
|
||||||
@@ -100,7 +101,7 @@ describe('CollectionSourceControlsComponent', () => {
|
|||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule],
|
||||||
declarations: [CollectionSourceControlsComponent, VarDirective],
|
declarations: [CollectionSourceControlsComponent, VarDirective, BtnDisabledDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: ScriptDataService, useValue: scriptDataService},
|
{provide: ScriptDataService, useValue: scriptDataService},
|
||||||
{provide: ProcessDataService, useValue: processDataService},
|
{provide: ProcessDataService, useValue: processDataService},
|
||||||
@@ -189,9 +190,10 @@ describe('CollectionSourceControlsComponent', () => {
|
|||||||
|
|
||||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||||
|
|
||||||
expect(buttons[0].nativeElement.disabled).toBeTrue();
|
buttons.forEach(button => {
|
||||||
expect(buttons[1].nativeElement.disabled).toBeTrue();
|
expect(button.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
expect(buttons[2].nativeElement.disabled).toBeTrue();
|
expect(button.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('should be enabled when isEnabled is true', () => {
|
it('should be enabled when isEnabled is true', () => {
|
||||||
comp.shouldShow = true;
|
comp.shouldShow = true;
|
||||||
@@ -201,9 +203,10 @@ describe('CollectionSourceControlsComponent', () => {
|
|||||||
|
|
||||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||||
|
|
||||||
expect(buttons[0].nativeElement.disabled).toBeFalse();
|
buttons.forEach(button => {
|
||||||
expect(buttons[1].nativeElement.disabled).toBeFalse();
|
expect(button.nativeElement.getAttribute('aria-disabled')).toBe('false');
|
||||||
expect(buttons[2].nativeElement.disabled).toBeFalse();
|
expect(button.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('should call the corresponding button when clicked', () => {
|
it('should call the corresponding button when clicked', () => {
|
||||||
spyOn(comp, 'testConfiguration');
|
spyOn(comp, 'testConfiguration');
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="d-inline-block float-right space-children-mr">
|
<div class="d-inline-block float-right space-children-mr">
|
||||||
<button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
<button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||||
[disabled]="(hasChanges$ | async) !== true"
|
[dsBtnDisabled]="(hasChanges$ | async) !== true"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i>
|
class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary"
|
||||||
[disabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
[dsBtnDisabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
||||||
(click)="onSubmit()"><i
|
(click)="onSubmit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-inline-block float-right ml-1 space-children-mr">
|
<div class="d-inline-block float-right ml-1 space-children-mr">
|
||||||
<button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
<button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||||
[disabled]="(hasChanges$ | async) !== true"
|
[dsBtnDisabled]="(hasChanges$ | async) !== true"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i>
|
class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary"
|
||||||
[disabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
[dsBtnDisabled]="(hasChanges$ | async) !== true || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
||||||
(click)="onSubmit()"><i
|
(click)="onSubmit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
|
@@ -6,10 +6,10 @@
|
|||||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
|
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col text-right space-children-mr">
|
<div class="col text-right space-children-mr">
|
||||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
|
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [dsBtnDisabled]="(processing$ | async)">
|
||||||
<i class="fas fa-times" aria-hidden="true"></i> {{'community.delete.cancel' | translate}}
|
<i class="fas fa-times" aria-hidden="true"></i> {{'community.delete.cancel' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
|
<button class="btn btn-danger" (click)="onConfirm(dso)" [dsBtnDisabled]="(processing$ | async)">
|
||||||
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin' aria-hidden="true"></i> {{'community.delete.processing' | translate}}</span>
|
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin' aria-hidden="true"></i> {{'community.delete.processing' | translate}}</span>
|
||||||
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash" aria-hidden="true"></i> {{'community.delete.confirm' | translate}}</span>
|
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash" aria-hidden="true"></i> {{'community.delete.confirm' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -116,12 +116,24 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
*/
|
*/
|
||||||
private sortAuthMethods(authMethodModels: AuthMethod[]): AuthMethod[] {
|
private sortAuthMethods(authMethodModels: AuthMethod[]): AuthMethod[] {
|
||||||
const sortedAuthMethodModels: AuthMethod[] = [];
|
const sortedAuthMethodModels: AuthMethod[] = [];
|
||||||
|
let passwordAuthFound = false;
|
||||||
|
let ldapAuthFound = false;
|
||||||
|
|
||||||
authMethodModels.forEach((method) => {
|
authMethodModels.forEach((method) => {
|
||||||
if (method.authMethodType === AuthMethodType.Password) {
|
if (method.authMethodType === AuthMethodType.Password) {
|
||||||
sortedAuthMethodModels.push(method);
|
sortedAuthMethodModels.push(method);
|
||||||
|
passwordAuthFound = true;
|
||||||
|
}
|
||||||
|
if (method.authMethodType === AuthMethodType.Ldap) {
|
||||||
|
ldapAuthFound = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Using password authentication method to provide UI for LDAP authentication even if password auth is not present in server
|
||||||
|
if (ldapAuthFound && !(passwordAuthFound)) {
|
||||||
|
sortedAuthMethodModels.push(new AuthMethod(AuthMethodType.Password,0));
|
||||||
|
}
|
||||||
|
|
||||||
authMethodModels.forEach((method) => {
|
authMethodModels.forEach((method) => {
|
||||||
if (method.authMethodType !== AuthMethodType.Password) {
|
if (method.authMethodType !== AuthMethodType.Password) {
|
||||||
sortedAuthMethodModels.push(method);
|
sortedAuthMethodModels.push(method);
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
* http://www.dspace.org/license/
|
* http://www.dspace.org/license/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AsyncSubject, from as observableFrom, Observable, of as observableOf } from 'rxjs';
|
import { AsyncSubject, from as observableFrom, Observable, of as observableOf, shareReplay } from 'rxjs';
|
||||||
import { map, mergeMap, skipWhile, switchMap, take, tap, toArray } from 'rxjs/operators';
|
import { map, mergeMap, skipWhile, switchMap, take, tap, toArray } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
@@ -264,6 +264,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
isNotEmptyOperator(),
|
isNotEmptyOperator(),
|
||||||
take(1),
|
take(1),
|
||||||
map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)),
|
map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)),
|
||||||
|
shareReplay(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
const startTime: number = new Date().getTime();
|
const startTime: number = new Date().getTime();
|
||||||
@@ -299,6 +300,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
isNotEmptyOperator(),
|
isNotEmptyOperator(),
|
||||||
take(1),
|
take(1),
|
||||||
map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)),
|
map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)),
|
||||||
|
shareReplay(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
const startTime: number = new Date().getTime();
|
const startTime: number = new Date().getTime();
|
||||||
|
@@ -21,6 +21,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import objectContaining = jasmine.objectContaining;
|
import objectContaining = jasmine.objectContaining;
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
|
||||||
describe('BitstreamDataService', () => {
|
describe('BitstreamDataService', () => {
|
||||||
let service: BitstreamDataService;
|
let service: BitstreamDataService;
|
||||||
@@ -132,4 +133,30 @@ describe('BitstreamDataService', () => {
|
|||||||
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream2-self');
|
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream2-self');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('findByItemHandle', () => {
|
||||||
|
it('should encode the filename correctly in the search parameters', () => {
|
||||||
|
const handle = '123456789/1234';
|
||||||
|
const sequenceId = '5';
|
||||||
|
const filename = 'file with spaces.pdf';
|
||||||
|
const searchParams = [
|
||||||
|
new RequestParam('handle', handle),
|
||||||
|
new RequestParam('sequenceId', sequenceId),
|
||||||
|
new RequestParam('filename', filename)
|
||||||
|
];
|
||||||
|
const linksToFollow: FollowLinkConfig<Bitstream>[] = [];
|
||||||
|
|
||||||
|
spyOn(service as any, 'getSearchByHref').and.callThrough();
|
||||||
|
|
||||||
|
service.getSearchByHref('byItemHandle', { searchParams }, ...linksToFollow).subscribe((href) => {
|
||||||
|
expect(service.getSearchByHref).toHaveBeenCalledWith(
|
||||||
|
'byItemHandle',
|
||||||
|
{ searchParams },
|
||||||
|
...linksToFollow
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(href).toBe(`${url}/bitstreams/search/byItemHandle?handle=123456789%2F1234&sequenceId=5&filename=file%20with%20spaces.pdf`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, EMPTY } from 'rxjs';
|
||||||
import { find, map, switchMap, take } from 'rxjs/operators';
|
import { find, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig, followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { Bitstream } from '../shared/bitstream.model';
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
import { BITSTREAM } from '../shared/bitstream.resource-type';
|
import { BITSTREAM } from '../shared/bitstream.resource-type';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
import { Bundle } from '../shared/bundle.model';
|
import { Bundle } from '../shared/bundle.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
@@ -171,7 +172,7 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
|
|||||||
searchParams.push(new RequestParam('sequenceId', sequenceId));
|
searchParams.push(new RequestParam('sequenceId', sequenceId));
|
||||||
}
|
}
|
||||||
if (hasValue(filename)) {
|
if (hasValue(filename)) {
|
||||||
searchParams.push(new RequestParam('filename', encodeURIComponent(filename)));
|
searchParams.push(new RequestParam('filename', filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
const hrefObs = this.getSearchByHref(
|
const hrefObs = this.getSearchByHref(
|
||||||
@@ -201,6 +202,38 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
|
|||||||
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
|
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Make a request to get primary bitstream
|
||||||
|
* in all current use cases, and having it simplifies this method
|
||||||
|
*
|
||||||
|
* @param item the {@link Item} the {@link Bundle} is a part of
|
||||||
|
* @param bundleName the name of the {@link Bundle} we want to find
|
||||||
|
* {@link Bitstream}s for
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param options the {@link FindListOptions} for the request
|
||||||
|
* @return {Observable<Bitstream | null>}
|
||||||
|
* Return an observable that contains primary bitstream information or null
|
||||||
|
*/
|
||||||
|
public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions): Observable<Bitstream | null> {
|
||||||
|
return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, options, followLink('primaryBitstream')).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((rd: RemoteData<Bundle>) => {
|
||||||
|
if (!rd.hasSucceeded) {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
return rd.payload.primaryBitstream.pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rdb: RemoteData<Bitstream>) => rdb.hasSucceeded ? rdb.payload : null),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a new FindListRequest with given search method
|
* Make a new FindListRequest with given search method
|
||||||
*
|
*
|
||||||
|
@@ -75,10 +75,14 @@ export class BundleDataService extends IdentifiableDataService<Bundle> implement
|
|||||||
* requested after the response becomes stale
|
* requested after the response becomes stale
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
* {@link HALLink}s should be automatically resolved
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
* @param options the {@link FindListOptions} for the request
|
||||||
*/
|
*/
|
||||||
// TODO should be implemented rest side
|
// TODO should be implemented rest side
|
||||||
findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> {
|
findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> {
|
||||||
return this.findAllByItem(item, { elementsPerPage: 9999 }, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
|
//Since we filter by bundleName where the pagination options are not indicated we need to load all the possible bundles.
|
||||||
|
// This is a workaround, in substitution of the previously recursive call with expand
|
||||||
|
const paginationOptions = options ?? { elementsPerPage: 9999 };
|
||||||
|
return this.findAllByItem(item, paginationOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
|
||||||
map((rd: RemoteData<PaginatedList<Bundle>>) => {
|
map((rd: RemoteData<PaginatedList<Bundle>>) => {
|
||||||
if (hasValue(rd.payload) && hasValue(rd.payload.page)) {
|
if (hasValue(rd.payload) && hasValue(rd.payload.page)) {
|
||||||
const matchingBundle = rd.payload.page.find((bundle: Bundle) =>
|
const matchingBundle = rd.payload.page.find((bundle: Bundle) =>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, EMPTY, expand, from, Observable, reduce } from 'rxjs';
|
||||||
import { map, mergeMap, switchMap, toArray } from 'rxjs/operators';
|
import { map, mergeMap, toArray } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
@@ -62,11 +62,26 @@ export class RelationshipTypeDataService extends BaseDataService<RelationshipTyp
|
|||||||
*/
|
*/
|
||||||
getRelationshipTypeByLabelAndTypes(relationshipTypeLabel: string, firstItemType: string, secondItemType: string): Observable<RelationshipType> {
|
getRelationshipTypeByLabelAndTypes(relationshipTypeLabel: string, firstItemType: string, secondItemType: string): Observable<RelationshipType> {
|
||||||
// Retrieve all relationship types from the server in a single page
|
// Retrieve all relationship types from the server in a single page
|
||||||
return this.findAllData.findAll({ currentPage: 1, elementsPerPage: 9999 }, true, true, followLink('leftType'), followLink('rightType'))
|
const initialPageInfo = { currentPage: 1, elementsPerPage: 20 };
|
||||||
|
return this.findAllData.findAll(initialPageInfo, true, true, followLink('leftType'), followLink('rightType'))
|
||||||
.pipe(
|
.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
// Emit each type in the page array separately
|
// Emit each type in the page array separately
|
||||||
switchMap((typeListRD: RemoteData<PaginatedList<RelationshipType>>) => typeListRD.payload.page),
|
expand((typeListRD: RemoteData<PaginatedList<RelationshipType>>) => {
|
||||||
|
const currentPage = typeListRD.payload.pageInfo.currentPage;
|
||||||
|
const totalPages = typeListRD.payload.pageInfo.totalPages;
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
const nextPageInfo = { currentPage: currentPage + 1, elementsPerPage: 20 };
|
||||||
|
return this.findAllData.findAll(nextPageInfo, true, true, followLink('leftType'), followLink('rightType')).pipe(
|
||||||
|
getFirstSucceededRemoteData()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// Collect all pages into a single array
|
||||||
|
reduce((acc: RelationshipType[], typeListRD: RemoteData<PaginatedList<RelationshipType>>) => acc.concat(typeListRD.payload.page), []),
|
||||||
|
mergeMap((relationshipTypes: RelationshipType[]) => from(relationshipTypes)),
|
||||||
// Check each type individually, to see if it matches the provided types
|
// Check each type individually, to see if it matches the provided types
|
||||||
mergeMap((relationshipType: RelationshipType) => {
|
mergeMap((relationshipType: RelationshipType) => {
|
||||||
if (relationshipType.leftwardType === relationshipTypeLabel) {
|
if (relationshipType.leftwardType === relationshipTypeLabel) {
|
||||||
|
194
src/app/core/dspace-rest/dspace-rest.interceptor.spec.ts
Normal file
194
src/app/core/dspace-rest/dspace-rest.interceptor.spec.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import {
|
||||||
|
HTTP_INTERCEPTORS,
|
||||||
|
HttpClient,
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import {
|
||||||
|
HttpClientTestingModule,
|
||||||
|
HttpTestingController,
|
||||||
|
} from '@angular/common/http/testing';
|
||||||
|
import { PLATFORM_ID } from '@angular/core';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {
|
||||||
|
APP_CONFIG,
|
||||||
|
AppConfig,
|
||||||
|
} from '../../../config/app-config.interface';
|
||||||
|
import { DspaceRestInterceptor } from './dspace-rest.interceptor';
|
||||||
|
import { DspaceRestService } from './dspace-rest.service';
|
||||||
|
|
||||||
|
describe('DspaceRestInterceptor', () => {
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
const appConfig: Partial<AppConfig> = {
|
||||||
|
rest: {
|
||||||
|
ssl: false,
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8080,
|
||||||
|
nameSpace: '/server',
|
||||||
|
baseUrl: 'http://api.example.com/server',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const appConfigWithSSR: Partial<AppConfig> = {
|
||||||
|
rest: {
|
||||||
|
ssl: false,
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8080,
|
||||||
|
nameSpace: '/server',
|
||||||
|
baseUrl: 'http://api.example.com/server',
|
||||||
|
ssrBaseUrl: 'http://ssr.example.com/server',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('When SSR base URL is not set ', () => {
|
||||||
|
describe('and it\'s in the browser', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [
|
||||||
|
DspaceRestService,
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: DspaceRestInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{ provide: APP_CONFIG, useValue: appConfig },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not modify the request', () => {
|
||||||
|
const url = 'http://api.example.com/server/items';
|
||||||
|
httpClient.get(url).subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(url);
|
||||||
|
expect(req.request.url).toBe(url);
|
||||||
|
req.flush({});
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and it\'s in SSR mode', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [
|
||||||
|
DspaceRestService,
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: DspaceRestInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{ provide: APP_CONFIG, useValue: appConfig },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'server' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not replace the base URL', () => {
|
||||||
|
const url = 'http://api.example.com/server/items';
|
||||||
|
|
||||||
|
httpClient.get(url).subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(url);
|
||||||
|
expect(req.request.url).toBe(url);
|
||||||
|
req.flush({});
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When SSR base URL is set ', () => {
|
||||||
|
describe('and it\'s in the browser', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [
|
||||||
|
DspaceRestService,
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: DspaceRestInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{ provide: APP_CONFIG, useValue: appConfigWithSSR },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not modify the request', () => {
|
||||||
|
const url = 'http://api.example.com/server/items';
|
||||||
|
httpClient.get(url).subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(url);
|
||||||
|
expect(req.request.url).toBe(url);
|
||||||
|
req.flush({});
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and it\'s in SSR mode', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [
|
||||||
|
DspaceRestService,
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: DspaceRestInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{ provide: APP_CONFIG, useValue: appConfigWithSSR },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'server' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace the base URL', () => {
|
||||||
|
const url = 'http://api.example.com/server/items';
|
||||||
|
const ssrBaseUrl = appConfigWithSSR.rest.ssrBaseUrl;
|
||||||
|
|
||||||
|
httpClient.get(url).subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(ssrBaseUrl + '/items');
|
||||||
|
expect(req.request.url).toBe(ssrBaseUrl + '/items');
|
||||||
|
req.flush({});
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not replace any query param containing the base URL', () => {
|
||||||
|
const url = 'http://api.example.com/server/items?url=http://api.example.com/server/item/1';
|
||||||
|
const ssrBaseUrl = appConfigWithSSR.rest.ssrBaseUrl;
|
||||||
|
|
||||||
|
httpClient.get(url).subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(ssrBaseUrl + '/items?url=http://api.example.com/server/item/1');
|
||||||
|
expect(req.request.url).toBe(ssrBaseUrl + '/items?url=http://api.example.com/server/item/1');
|
||||||
|
req.flush({});
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
52
src/app/core/dspace-rest/dspace-rest.interceptor.ts
Normal file
52
src/app/core/dspace-rest/dspace-rest.interceptor.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
import {
|
||||||
|
HttpEvent,
|
||||||
|
HttpHandler,
|
||||||
|
HttpInterceptor,
|
||||||
|
HttpRequest,
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import {
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
PLATFORM_ID,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
APP_CONFIG,
|
||||||
|
AppConfig,
|
||||||
|
} from '../../../config/app-config.interface';
|
||||||
|
import { isEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* This Interceptor is used to use the configured base URL for the request made during SSR execution
|
||||||
|
*/
|
||||||
|
export class DspaceRestInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the configured application base URL
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected baseUrl: string;
|
||||||
|
protected ssrBaseUrl: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
|
@Inject(PLATFORM_ID) private platformId: string,
|
||||||
|
) {
|
||||||
|
this.baseUrl = this.appConfig.rest.baseUrl;
|
||||||
|
this.ssrBaseUrl = this.appConfig.rest.ssrBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||||
|
if (isPlatformBrowser(this.platformId) || isEmpty(this.ssrBaseUrl) || this.baseUrl === this.ssrBaseUrl) {
|
||||||
|
return next.handle(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different SSR Base URL specified so replace it in the current request url
|
||||||
|
const url = request.url.replace(this.baseUrl, this.ssrBaseUrl);
|
||||||
|
const newRequest: HttpRequest<any> = request.clone({ url });
|
||||||
|
return next.handle(newRequest);
|
||||||
|
}
|
||||||
|
}
|
@@ -45,6 +45,7 @@ import { CoreState } from '../core-state.model';
|
|||||||
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
||||||
import { getDownloadableBitstream } from '../shared/bitstream.operators';
|
import { getDownloadableBitstream } from '../shared/bitstream.operators';
|
||||||
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||||
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base selector function to select the metaTag section in the store
|
* The base selector function to select the metaTag section in the store
|
||||||
@@ -306,6 +307,7 @@ export class MetadataService {
|
|||||||
'ORIGINAL',
|
'ORIGINAL',
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
new FindListOptions(),
|
||||||
followLink('primaryBitstream'),
|
followLink('primaryBitstream'),
|
||||||
followLink('bitstreams', {
|
followLink('bitstreams', {
|
||||||
findListOptions: {
|
findListOptions: {
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { environment } from '../../../environments/environment.test';
|
||||||
import { ServerHardRedirectService } from './server-hard-redirect.service';
|
import { ServerHardRedirectService } from './server-hard-redirect.service';
|
||||||
|
|
||||||
describe('ServerHardRedirectService', () => {
|
describe('ServerHardRedirectService', () => {
|
||||||
@@ -6,7 +8,7 @@ describe('ServerHardRedirectService', () => {
|
|||||||
const mockRequest = jasmine.createSpyObj(['get']);
|
const mockRequest = jasmine.createSpyObj(['get']);
|
||||||
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
|
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
|
||||||
|
|
||||||
const service: ServerHardRedirectService = new ServerHardRedirectService(mockRequest, mockResponse);
|
let service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse);
|
||||||
const origin = 'https://test-host.com:4000';
|
const origin = 'https://test-host.com:4000';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -67,4 +69,23 @@ describe('ServerHardRedirectService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when SSR base url is set', () => {
|
||||||
|
const redirect = 'https://private-url:4000/server/api/bitstreams/uuid';
|
||||||
|
const replacedUrl = 'https://public-url/server/api/bitstreams/uuid';
|
||||||
|
const environmentWithSSRUrl: any = { ...environment, ...{ ...environment.rest, rest: {
|
||||||
|
ssrBaseUrl: 'https://private-url:4000/server',
|
||||||
|
baseUrl: 'https://public-url/server',
|
||||||
|
} } };
|
||||||
|
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.redirect(redirect);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform a 302 redirect', () => {
|
||||||
|
expect(mockResponse.redirect).toHaveBeenCalledWith(302, replacedUrl);
|
||||||
|
expect(mockResponse.end).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -2,6 +2,8 @@ import { Inject, Injectable } from '@angular/core';
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
import { HardRedirectService } from './hard-redirect.service';
|
import { HardRedirectService } from './hard-redirect.service';
|
||||||
|
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for performing hard redirects within the server app module
|
* Service for performing hard redirects within the server app module
|
||||||
@@ -10,6 +12,7 @@ import { HardRedirectService } from './hard-redirect.service';
|
|||||||
export class ServerHardRedirectService extends HardRedirectService {
|
export class ServerHardRedirectService extends HardRedirectService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
@Inject(REQUEST) protected req: Request,
|
@Inject(REQUEST) protected req: Request,
|
||||||
@Inject(RESPONSE) protected res: Response,
|
@Inject(RESPONSE) protected res: Response,
|
||||||
) {
|
) {
|
||||||
@@ -25,17 +28,22 @@ export class ServerHardRedirectService extends HardRedirectService {
|
|||||||
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
|
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
|
||||||
*/
|
*/
|
||||||
redirect(url: string, statusCode?: number) {
|
redirect(url: string, statusCode?: number) {
|
||||||
|
|
||||||
if (url === this.req.url) {
|
if (url === this.req.url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let redirectUrl = url;
|
||||||
|
// If redirect url contains SSR base url then replace with public base url
|
||||||
|
if (isNotEmpty(this.appConfig.rest.ssrBaseUrl) && this.appConfig.rest.baseUrl !== this.appConfig.rest.ssrBaseUrl) {
|
||||||
|
redirectUrl = url.replace(this.appConfig.rest.ssrBaseUrl, this.appConfig.rest.baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.res.finished) {
|
if (this.res.finished) {
|
||||||
const req: any = this.req;
|
const req: any = this.req;
|
||||||
req._r_count = (req._r_count || 0) + 1;
|
req._r_count = (req._r_count || 0) + 1;
|
||||||
|
|
||||||
console.warn('Attempted to redirect on a finished response. From',
|
console.warn('Attempted to redirect on a finished response. From',
|
||||||
this.req.url, 'to', url);
|
this.req.url, 'to', redirectUrl);
|
||||||
|
|
||||||
if (req._r_count > 10) {
|
if (req._r_count > 10) {
|
||||||
console.error('Detected a redirection loop. killing the nodejs process');
|
console.error('Detected a redirection loop. killing the nodejs process');
|
||||||
@@ -49,9 +57,9 @@ export class ServerHardRedirectService extends HardRedirectService {
|
|||||||
status = 302;
|
status = 302;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`);
|
console.info(`Redirecting from ${this.req.url} to ${redirectUrl} with ${status}`);
|
||||||
|
|
||||||
this.res.redirect(status, url);
|
this.res.redirect(status, redirectUrl);
|
||||||
this.res.end();
|
this.res.end();
|
||||||
// I haven't found a way to correctly stop Angular rendering.
|
// I haven't found a way to correctly stop Angular rendering.
|
||||||
// So we just let it end its work, though we have already closed
|
// So we just let it end its work, though we have already closed
|
||||||
|
@@ -24,31 +24,31 @@
|
|||||||
<button class="btn btn-outline-primary btn-sm ng-star-inserted" data-test="metadata-edit-btn" *ngIf="!mdValue.editing"
|
<button class="btn btn-outline-primary btn-sm ng-star-inserted" data-test="metadata-edit-btn" *ngIf="!mdValue.editing"
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.edit' | translate"
|
[title]="dsoType + '.edit.metadata.edit.buttons.edit' | translate"
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.edit' | translate }}"
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.edit' | translate }}"
|
||||||
[disabled]="isVirtual || mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE || (saving$ | async)" (click)="edit.emit()">
|
[dsBtnDisabled]="isVirtual || mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE || (saving$ | async)" (click)="edit.emit()">
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-success btn-sm ng-star-inserted" data-test="metadata-confirm-btn" *ngIf="mdValue.editing"
|
<button class="btn btn-outline-success btn-sm ng-star-inserted" data-test="metadata-confirm-btn" *ngIf="mdValue.editing"
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.confirm' | translate"
|
[title]="dsoType + '.edit.metadata.edit.buttons.confirm' | translate"
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.confirm' | translate }}"
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.confirm' | translate }}"
|
||||||
[disabled]="isVirtual || (saving$ | async)" (click)="confirm.emit(true)">
|
[dsBtnDisabled]="isVirtual || (saving$ | async)" (click)="confirm.emit(true)">
|
||||||
<i class="fas fa-check fa-fw"></i>
|
<i class="fas fa-check fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-danger btn-sm" data-test="metadata-remove-btn"
|
<button class="btn btn-outline-danger btn-sm" data-test="metadata-remove-btn"
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.remove' | translate"
|
[title]="dsoType + '.edit.metadata.edit.buttons.remove' | translate"
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.remove' | translate }}"
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.remove' | translate }}"
|
||||||
[disabled]="isVirtual || (mdValue.change && mdValue.change !== DsoEditMetadataChangeTypeEnum.ADD) || mdValue.editing || (saving$ | async)" (click)="remove.emit()">
|
[dsBtnDisabled]="isVirtual || (mdValue.change && mdValue.change !== DsoEditMetadataChangeTypeEnum.ADD) || mdValue.editing || (saving$ | async)" (click)="remove.emit()">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-warning btn-sm" data-test="metadata-undo-btn"
|
<button class="btn btn-outline-warning btn-sm" data-test="metadata-undo-btn"
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.undo' | translate"
|
[title]="dsoType + '.edit.metadata.edit.buttons.undo' | translate"
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.undo' | translate }}"
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.undo' | translate }}"
|
||||||
[disabled]="isVirtual || (!mdValue.change && mdValue.reordered) || (!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()">
|
[dsBtnDisabled]="isVirtual || (!mdValue.change && mdValue.reordered) || (!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()">
|
||||||
<i class="fas fa-undo-alt fa-fw"></i>
|
<i class="fas fa-undo-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-outline-secondary ds-drag-handle btn-sm" data-test="metadata-drag-btn" *ngVar="(isOnlyValue || (saving$ | async)) as disabled"
|
<button class="btn btn-outline-secondary ds-drag-handle btn-sm" data-test="metadata-drag-btn" *ngVar="(isOnlyValue || (saving$ | async)) as disabled"
|
||||||
cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [disabled]="disabled"
|
cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [dsBtnDisabled]="disabled"
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
|
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
|
||||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}">
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}">
|
||||||
<i class="fas fa-grip-vertical fa-fw"></i>
|
<i class="fas fa-grip-vertical fa-fw"></i>
|
||||||
|
@@ -11,6 +11,7 @@ import { ItemMetadataRepresentation } from '../../../core/shared/metadata-repres
|
|||||||
import { MetadataValue, VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models';
|
import { MetadataValue, VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models';
|
||||||
import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form';
|
import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
import {BtnDisabledDirective} from '../../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
const EDIT_BTN = 'edit';
|
const EDIT_BTN = 'edit';
|
||||||
const CONFIRM_BTN = 'confirm';
|
const CONFIRM_BTN = 'confirm';
|
||||||
@@ -49,7 +50,7 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
initServices();
|
initServices();
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [DsoEditMetadataValueComponent, VarDirective],
|
declarations: [DsoEditMetadataValueComponent, VarDirective, BtnDisabledDirective],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RelationshipDataService, useValue: relationshipService },
|
{ provide: RelationshipDataService, useValue: relationshipService },
|
||||||
@@ -158,7 +159,14 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should${disabled ? ' ' : ' not '}be disabled`, () => {
|
it(`should${disabled ? ' ' : ' not '}be disabled`, () => {
|
||||||
expect(btn.nativeElement.disabled).toBe(disabled);
|
if (disabled) {
|
||||||
|
expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(btn.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
|
} else {
|
||||||
|
// Can be null or false, depending on if button was ever disabled so just check not true
|
||||||
|
expect(btn.nativeElement.getAttribute('aria-disabled')).not.toBe('true');
|
||||||
|
expect(btn.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
it('should not exist', () => {
|
it('should not exist', () => {
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
<div class="item-metadata" *ngIf="form">
|
<div class="item-metadata" *ngIf="form">
|
||||||
<div class="button-row top d-flex my-2 space-children-mr ml-gap">
|
<div class="button-row top d-flex my-2 space-children-mr ml-gap">
|
||||||
<button class="mr-auto btn btn-success" id="dso-add-btn" [disabled]="form.newValue || (saving$ | async)"
|
<button class="mr-auto btn btn-success" id="dso-add-btn" [dsBtnDisabled]="form.newValue || (saving$ | async)"
|
||||||
[attr.aria-label]="dsoType + '.edit.metadata.add-button' | translate"
|
[attr.aria-label]="dsoType + '.edit.metadata.add-button' | translate"
|
||||||
[title]="dsoType + '.edit.metadata.add-button' | translate"
|
[title]="dsoType + '.edit.metadata.add-button' | translate"
|
||||||
(click)="add()"><i class="fas fa-plus" aria-hidden="true"></i>
|
(click)="add()"><i class="fas fa-plus" aria-hidden="true"></i>
|
||||||
<span class="d-none d-sm-inline"> {{ dsoType + '.edit.metadata.add-button' | translate }}</span>
|
<span class="d-none d-sm-inline"> {{ dsoType + '.edit.metadata.add-button' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning ml-1" id="dso-reinstate-btn" *ngIf="isReinstatable" [disabled]="(saving$ | async)"
|
<button class="btn btn-warning ml-1" id="dso-reinstate-btn" *ngIf="isReinstatable" [dsBtnDisabled]="(saving$ | async)"
|
||||||
[attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
[attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
||||||
[title]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
[title]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
||||||
(click)="reinstate()"><i class="fas fa-undo-alt" aria-hidden="true"></i>
|
(click)="reinstate()"><i class="fas fa-undo-alt" aria-hidden="true"></i>
|
||||||
<span class="d-none d-sm-inline"> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}</span>
|
<span class="d-none d-sm-inline"> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary ml-1" id="dso-save-btn" [disabled]="!hasChanges || (saving$ | async)"
|
<button class="btn btn-primary ml-1" id="dso-save-btn" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
|
||||||
[attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate"
|
[attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate"
|
||||||
[title]="dsoType + '.edit.metadata.save-button' | translate"
|
[title]="dsoType + '.edit.metadata.save-button' | translate"
|
||||||
(click)="submit()"><i class="fas fa-save" aria-hidden="true"></i>
|
(click)="submit()"><i class="fas fa-save" aria-hidden="true"></i>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<button class="btn btn-danger ml-1" id="dso-discard-btn" *ngIf="!isReinstatable"
|
<button class="btn btn-danger ml-1" id="dso-discard-btn" *ngIf="!isReinstatable"
|
||||||
[attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate"
|
[attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate"
|
||||||
[title]="dsoType + '.edit.metadata.discard-button' | translate"
|
[title]="dsoType + '.edit.metadata.discard-button' | translate"
|
||||||
[disabled]="!hasChanges || (saving$ | async)"
|
[dsBtnDisabled]="!hasChanges || (saving$ | async)"
|
||||||
(click)="discard()"><i class="fas fa-times" aria-hidden="true"></i>
|
(click)="discard()"><i class="fas fa-times" aria-hidden="true"></i>
|
||||||
<span class="d-none d-sm-inline"> {{ dsoType + '.edit.metadata.discard-button' | translate }}</span>
|
<span class="d-none d-sm-inline"> {{ dsoType + '.edit.metadata.discard-button' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -76,13 +76,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="button-row bottom d-inline-block w-100">
|
<div class="button-row bottom d-inline-block w-100">
|
||||||
<div class="mt-2 float-right space-children-mr ml-gap">
|
<div class="mt-2 float-right space-children-mr ml-gap">
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable" [disabled]="(saving$ | async)"
|
<button class="btn btn-warning" *ngIf="isReinstatable" [dsBtnDisabled]="(saving$ | async)"
|
||||||
[attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
[attr.aria-label]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
||||||
[title]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
[title]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
||||||
(click)="reinstate()">
|
(click)="reinstate()">
|
||||||
<i class="fas fa-undo-alt" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}
|
<i class="fas fa-undo-alt" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="!hasChanges || (saving$ | async)"
|
<button class="btn btn-primary" [dsBtnDisabled]="!hasChanges || (saving$ | async)"
|
||||||
[attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate"
|
[attr.aria-label]="dsoType + '.edit.metadata.save-button' | translate"
|
||||||
[title]="dsoType + '.edit.metadata.save-button' | translate"
|
[title]="dsoType + '.edit.metadata.save-button' | translate"
|
||||||
(click)="submit()">
|
(click)="submit()">
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
<button class="btn btn-danger" *ngIf="!isReinstatable"
|
<button class="btn btn-danger" *ngIf="!isReinstatable"
|
||||||
[attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate"
|
[attr.aria-label]="dsoType + '.edit.metadata.discard-button' | translate"
|
||||||
[title]="dsoType + '.edit.metadata.discard-button' | translate"
|
[title]="dsoType + '.edit.metadata.discard-button' | translate"
|
||||||
[disabled]="!hasChanges || (saving$ | async)"
|
[dsBtnDisabled]="!hasChanges || (saving$ | async)"
|
||||||
(click)="discard()">
|
(click)="discard()">
|
||||||
<i class="fas fa-times" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.discard-button' | translate }}
|
<i class="fas fa-times" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.discard-button' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -16,6 +16,7 @@ import { DATA_SERVICE_FACTORY } from '../../core/data/base/data-service.decorato
|
|||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import {BtnDisabledDirective} from '../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
const ADD_BTN = 'add';
|
const ADD_BTN = 'add';
|
||||||
const REINSTATE_BTN = 'reinstate';
|
const REINSTATE_BTN = 'reinstate';
|
||||||
@@ -71,7 +72,7 @@ describe('DsoEditMetadataComponent', () => {
|
|||||||
notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']);
|
notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [DsoEditMetadataComponent, VarDirective],
|
declarations: [DsoEditMetadataComponent, VarDirective, BtnDisabledDirective],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
providers: [
|
providers: [
|
||||||
TestDataService,
|
TestDataService,
|
||||||
@@ -180,7 +181,13 @@ describe('DsoEditMetadataComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should${disabled ? ' ' : ' not '}be disabled`, () => {
|
it(`should${disabled ? ' ' : ' not '}be disabled`, () => {
|
||||||
expect(btn.nativeElement.disabled).toBe(disabled);
|
if (disabled) {
|
||||||
|
expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(btn.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
|
} else {
|
||||||
|
expect(btn.nativeElement.getAttribute('aria-disabled')).not.toBe('true');
|
||||||
|
expect(btn.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
it('should not exist', () => {
|
it('should not exist', () => {
|
||||||
|
@@ -12,4 +12,9 @@
|
|||||||
<a [routerLink]="[itemPageRoute]"
|
<a [routerLink]="[itemPageRoute]"
|
||||||
[innerHTML]="mdRepresentation.getValue()"
|
[innerHTML]="mdRepresentation.getValue()"
|
||||||
[ngbTooltip]="mdRepresentation.allMetadata(['person.jobTitle']).length > 0 ? descTemplate : null"></a>
|
[ngbTooltip]="mdRepresentation.allMetadata(['person.jobTitle']).length > 0 ? descTemplate : null"></a>
|
||||||
|
<ds-orcid-badge-and-tooltip class="ml-1"
|
||||||
|
*ngIf="mdRepresentation.firstMetadata('person.identifier.orcid')"
|
||||||
|
[orcid]="mdRepresentation.firstMetadata('person.identifier.orcid')"
|
||||||
|
[authenticatedTimestamp]="mdRepresentation.firstMetadata('dspace.orcid.authenticated')">
|
||||||
|
</ds-orcid-badge-and-tooltip>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
@@ -28,7 +28,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button
|
<button
|
||||||
[disabled]="isInValid"
|
[dsBtnDisabled]="isInValid"
|
||||||
class="btn btn-default btn-primary"
|
class="btn btn-default btn-primary"
|
||||||
(click)="submit()">{{'forgot-password.form.submit' | translate}}</button>
|
(click)="submit()">{{'forgot-password.form.submit' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class="d-flex mt-4">
|
<div class="d-flex mt-4">
|
||||||
<button id="button-cancel" type="button" (click)="cancel()" class="btn btn-outline-secondary mr-auto">{{ 'info.end-user-agreement.buttons.cancel' | translate }}</button>
|
<button id="button-cancel" type="button" (click)="cancel()" class="btn btn-outline-secondary mr-auto">{{ 'info.end-user-agreement.buttons.cancel' | translate }}</button>
|
||||||
<button id="button-save" type="submit" class="btn btn-primary" [disabled]="!accepted">{{ 'info.end-user-agreement.buttons.save' | translate }}</button>
|
<button id="button-save" type="submit" class="btn btn-primary" [dsBtnDisabled]="!accepted">{{ 'info.end-user-agreement.buttons.save' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -11,6 +11,7 @@ import { Store } from '@ngrx/store';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { LogOutAction } from '../../core/auth/auth.actions';
|
import { LogOutAction } from '../../core/auth/auth.actions';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
|
import {BtnDisabledDirective} from '../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
describe('EndUserAgreementComponent', () => {
|
describe('EndUserAgreementComponent', () => {
|
||||||
let component: EndUserAgreementComponent;
|
let component: EndUserAgreementComponent;
|
||||||
@@ -49,7 +50,7 @@ describe('EndUserAgreementComponent', () => {
|
|||||||
init();
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot()],
|
||||||
declarations: [EndUserAgreementComponent],
|
declarations: [EndUserAgreementComponent, BtnDisabledDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: EndUserAgreementService, useValue: endUserAgreementService },
|
{ provide: EndUserAgreementService, useValue: endUserAgreementService },
|
||||||
{ provide: NotificationsService, useValue: notificationsService },
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
@@ -81,7 +82,8 @@ describe('EndUserAgreementComponent', () => {
|
|||||||
|
|
||||||
it('should disable the save button', () => {
|
it('should disable the save button', () => {
|
||||||
const button = fixture.debugElement.query(By.css('#button-save')).nativeElement;
|
const button = fixture.debugElement.query(By.css('#button-save')).nativeElement;
|
||||||
expect(button.disabled).toBeTruthy();
|
expect(button.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(button.classList.contains('disabled')).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="control-group col-sm-12 text-right">
|
<div class="control-group col-sm-12 text-right">
|
||||||
<button [disabled]="!feedbackForm.valid" class="btn btn-primary" name="submit" type="submit">{{ 'info.feedback.send' | translate }}</button>
|
<button [dsBtnDisabled]="!feedbackForm.valid" class="btn btn-primary" name="submit" type="submit">{{ 'info.feedback.send' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -18,6 +18,7 @@ import { Router } from '@angular/router';
|
|||||||
import { RouterMock } from '../../../shared/mocks/router.mock';
|
import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||||
import { NativeWindowService } from '../../../core/services/window.service';
|
import { NativeWindowService } from '../../../core/services/window.service';
|
||||||
import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref';
|
import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref';
|
||||||
|
import {BtnDisabledDirective} from '../../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
|
|
||||||
describe('FeedbackFormComponent', () => {
|
describe('FeedbackFormComponent', () => {
|
||||||
@@ -38,7 +39,7 @@ describe('FeedbackFormComponent', () => {
|
|||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot()],
|
||||||
declarations: [FeedbackFormComponent],
|
declarations: [FeedbackFormComponent, BtnDisabledDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RouteService, useValue: routeServiceStub },
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
{ provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() },
|
{ provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() },
|
||||||
@@ -72,7 +73,8 @@ describe('FeedbackFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have disabled button', () => {
|
it('should have disabled button', () => {
|
||||||
expect(de.query(By.css('button')).nativeElement.disabled).toBeTrue();
|
expect(de.query(By.css('button')).nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(de.query(By.css('button')).nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when message is inserted', () => {
|
describe('when message is inserted', () => {
|
||||||
@@ -83,7 +85,8 @@ describe('FeedbackFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not have disabled button', () => {
|
it('should not have disabled button', () => {
|
||||||
expect(de.query(By.css('button')).nativeElement.disabled).toBeFalse();
|
expect(de.query(By.css('button')).nativeElement.getAttribute('aria-disabled')).toBe('false');
|
||||||
|
expect(de.query(By.css('button')).nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('on submit should call createFeedback of feedbackDataServiceStub service', () => {
|
it('on submit should call createFeedback of feedbackDataServiceStub service', () => {
|
||||||
|
@@ -79,7 +79,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
[disabled]="requestCopyForm.invalid"
|
[dsBtnDisabled]="requestCopyForm.invalid"
|
||||||
class="btn btn-default btn-primary"
|
class="btn btn-default btn-primary"
|
||||||
(click)="onSubmit()">{{'bitstream-request-a-copy.submit' | translate}}</button>
|
(click)="onSubmit()">{{'bitstream-request-a-copy.submit' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
class="fas fa-undo-alt"></i>
|
class="fas fa-undo-alt"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting"
|
<button class="btn btn-primary" [dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
||||||
(click)="submit()"><i
|
(click)="submit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
||||||
[disabled]="(hasChanges$ | async) !== true || submitting"
|
[dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i>
|
class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||||
@@ -39,6 +39,9 @@
|
|||||||
[isFirstTable]="isFirst"
|
[isFirstTable]="isFirst"
|
||||||
aria-describedby="reorder-description">
|
aria-describedby="reorder-description">
|
||||||
</ds-item-edit-bitstream-bundle>
|
</ds-item-edit-bitstream-bundle>
|
||||||
|
<div class="d-flex justify-content-center" *ngIf="showLoadMoreLink$ | async">
|
||||||
|
<button class="btn btn-link my-3" (click)="loadBundles()"> {{'item.edit.bitstreams.load-more.link' | translate}}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="bundles?.length === 0"
|
<div *ngIf="bundles?.length === 0"
|
||||||
class="alert alert-info w-100 d-inline-block mt-4" role="alert">
|
class="alert alert-info w-100 d-inline-block mt-4" role="alert">
|
||||||
@@ -54,7 +57,7 @@
|
|||||||
class="fas fa-undo-alt"></i>
|
class="fas fa-undo-alt"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting"
|
<button class="btn btn-primary" [dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
||||||
(click)="submit()"><i
|
(click)="submit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
@@ -62,7 +65,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
||||||
[disabled]="(hasChanges$ | async) !== true || submitting"
|
[dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i>
|
class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, HostListener } from '@angular/core';
|
import { ChangeDetectorRef, Component, NgZone, OnDestroy, HostListener } from '@angular/core';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { map, switchMap, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { Observable, Subscription, combineLatest } from 'rxjs';
|
import { Observable, Subscription, combineLatest, BehaviorSubject, tap } from 'rxjs';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
@@ -23,6 +23,7 @@ import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/res
|
|||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import { ItemBitstreamsService } from './item-bitstreams.service';
|
import { ItemBitstreamsService } from './item-bitstreams.service';
|
||||||
import { AlertType } from '../../../shared/alert/alert-type';
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-bitstreams',
|
selector: 'ds-item-bitstreams',
|
||||||
@@ -40,7 +41,16 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
/**
|
/**
|
||||||
* The currently listed bundles
|
* The currently listed bundles
|
||||||
*/
|
*/
|
||||||
bundles$: Observable<Bundle[]>;
|
private bundlesSubject = new BehaviorSubject<Bundle[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The page options to use for fetching the bundles
|
||||||
|
*/
|
||||||
|
bundlesOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'bundles-pagination-options',
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bootstrap sizes used for the columns within this table
|
* The bootstrap sizes used for the columns within this table
|
||||||
@@ -64,6 +74,18 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
*/
|
*/
|
||||||
isProcessingMoveRequest: Observable<boolean>;
|
isProcessingMoveRequest: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The flag indicating to show the load more link
|
||||||
|
*/
|
||||||
|
showLoadMoreLink$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of bundles for the current item as an observable
|
||||||
|
*/
|
||||||
|
get bundles$(): Observable<Bundle[]> {
|
||||||
|
return this.bundlesSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public itemService: ItemDataService,
|
public itemService: ItemDataService,
|
||||||
public objectUpdatesService: ObjectUpdatesService,
|
public objectUpdatesService: ObjectUpdatesService,
|
||||||
@@ -88,14 +110,8 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
* Actions to perform after the item has been initialized
|
* Actions to perform after the item has been initialized
|
||||||
*/
|
*/
|
||||||
postItemInit(): void {
|
postItemInit(): void {
|
||||||
const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions();
|
|
||||||
this.isProcessingMoveRequest = this.itemBitstreamsService.getPerformingMoveRequest$();
|
this.isProcessingMoveRequest = this.itemBitstreamsService.getPerformingMoveRequest$();
|
||||||
|
this.loadBundles(1);
|
||||||
this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,6 +176,26 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
this.notificationsPrefix = 'item.edit.bitstreams.notifications.';
|
this.notificationsPrefix = 'item.edit.bitstreams.notifications.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load bundles for the current item
|
||||||
|
* @param currentPage The current page to load
|
||||||
|
*/
|
||||||
|
loadBundles(currentPage?: number) {
|
||||||
|
this.bundlesOptions = Object.assign(new PaginationComponentOptions(), this.bundlesOptions, {
|
||||||
|
currentPage: currentPage || this.bundlesOptions.currentPage + 1,
|
||||||
|
});
|
||||||
|
this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: this.bundlesOptions})).pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
tap((bundlesPL: PaginatedList<Bundle>) =>
|
||||||
|
this.showLoadMoreLink$.next(bundlesPL.pageInfo.currentPage < bundlesPL.pageInfo.totalPages)
|
||||||
|
),
|
||||||
|
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page),
|
||||||
|
).subscribe((bundles: Bundle[]) => {
|
||||||
|
this.bundlesSubject.next([...this.bundlesSubject.getValue(), ...bundles]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit the current changes
|
* Submit the current changes
|
||||||
|
@@ -86,10 +86,10 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div class="space-children-mr">
|
<div class="space-children-mr">
|
||||||
<button [disabled]="isDeleting$ | async" (click)="performAction()"
|
<button [dsBtnDisabled]="isDeleting$ | async" (click)="performAction()"
|
||||||
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
|
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button [disabled]="isDeleting$ | async" [routerLink]="[itemPageRoute, 'edit']"
|
<button [dsBtnDisabled]="isDeleting$ | async" [routerLink]="[itemPageRoute, 'edit']"
|
||||||
class="btn btn-outline-secondary cancel">
|
class="btn btn-outline-secondary cancel">
|
||||||
{{cancelMessage| translate}}
|
{{cancelMessage| translate}}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -40,7 +40,7 @@
|
|||||||
<button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary">
|
<button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}}
|
<i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="!canMove" (click)="moveToCollection()">
|
<button class="btn btn-primary" [dsBtnDisabled]="!canMove" (click)="moveToCollection()">
|
||||||
<span *ngIf="!processing">
|
<span *ngIf="!processing">
|
||||||
<i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}}
|
<i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}}
|
||||||
</span>
|
</span>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
<i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.move.processing' | translate}}
|
<i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.move.processing' | translate}}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" [disabled]="!canSubmit" (click)="discard()">
|
<button class="btn btn-danger" [dsBtnDisabled]="!canSubmit" (click)="discard()">
|
||||||
<i class="fas fa-times"></i> {{"item.edit.move.discard-button" | translate}}
|
<i class="fas fa-times"></i> {{"item.edit.move.discard-button" | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -5,12 +5,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-9 float-left action-button">
|
<div class="col-12 col-md-9 float-left action-button">
|
||||||
<span *ngIf="operation.authorized">
|
<span *ngIf="operation.authorized">
|
||||||
<button class="btn btn-outline-primary" [disabled]="operation.disabled" [routerLink]="operation.operationUrl" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate">
|
<button class="btn btn-outline-primary" [dsBtnDisabled]="operation.disabled" [routerLink]="operation.operationUrl" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate">
|
||||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="!operation.authorized" [ngbTooltip]="'item.edit.tabs.status.buttons.unauthorized' | translate">
|
<span *ngIf="!operation.authorized" [ngbTooltip]="'item.edit.tabs.status.buttons.unauthorized' | translate">
|
||||||
<button class="btn btn-outline-primary" [disabled]="true" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate">
|
<button class="btn btn-outline-primary" [dsBtnDisabled]="true" [attr.aria-label]="'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate">
|
||||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -4,6 +4,7 @@ import { ItemOperationComponent } from './item-operation.component';
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import {BtnDisabledDirective} from '../../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
describe('ItemOperationComponent', () => {
|
describe('ItemOperationComponent', () => {
|
||||||
let itemOperation: ItemOperation;
|
let itemOperation: ItemOperation;
|
||||||
@@ -14,7 +15,7 @@ describe('ItemOperationComponent', () => {
|
|||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
declarations: [ItemOperationComponent]
|
declarations: [ItemOperationComponent, BtnDisabledDirective]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -40,7 +41,8 @@ describe('ItemOperationComponent', () => {
|
|||||||
const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement;
|
const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement;
|
||||||
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
|
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
|
||||||
const button = fixture.debugElement.query(By.css('button')).nativeElement;
|
const button = fixture.debugElement.query(By.css('button')).nativeElement;
|
||||||
expect(button.disabled).toBeTrue();
|
expect(button.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(button.classList.contains('disabled')).toBeTrue();
|
||||||
expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<h2 class="h4">
|
<h2 class="h4">
|
||||||
{{relationshipMessageKey$ | async | translate}}
|
{{relationshipMessageKey$ | async | translate}}
|
||||||
<button class="ml-2 btn btn-success" [disabled]="(hasChanges | async)" (click)="openLookup()">
|
<button class="ml-2 btn btn-success" [dsBtnDisabled]="(hasChanges | async)" (click)="openLookup()">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.relationships.edit.buttons.add" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.relationships.edit.buttons.add" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -356,7 +356,8 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
comp.hasChanges = observableOf(true);
|
comp.hasChanges = observableOf(true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const element = de.query(By.css('.btn-success'));
|
const element = de.query(By.css('.btn-success'));
|
||||||
expect(element.nativeElement?.disabled).toBeTrue();
|
expect(element.nativeElement?.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(element.nativeElement?.classList.contains('disabled')).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -9,12 +9,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<div class="btn-group relationship-action-buttons">
|
<div class="btn-group relationship-action-buttons">
|
||||||
<button [disabled]="!canRemove()" (click)="openVirtualMetadataModal(virtualMetadataModal)"
|
<button [dsBtnDisabled]="!canRemove()" (click)="openVirtualMetadataModal(virtualMetadataModal)"
|
||||||
class="btn btn-outline-danger btn-sm"
|
class="btn btn-outline-danger btn-sm"
|
||||||
title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
|
title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button [disabled]="!canUndo()" (click)="undo()"
|
<button [dsBtnDisabled]="!canUndo()" (click)="undo()"
|
||||||
class="btn btn-outline-warning btn-sm"
|
class="btn btn-outline-warning btn-sm"
|
||||||
title="{{'item.edit.metadata.edit.buttons.undo' | translate}}">
|
title="{{'item.edit.metadata.edit.buttons.undo' | translate}}">
|
||||||
<i class="fas fa-undo-alt fa-fw"></i>
|
<i class="fas fa-undo-alt fa-fw"></i>
|
||||||
|
@@ -35,7 +35,7 @@
|
|||||||
<ng-template #buttons>
|
<ng-template #buttons>
|
||||||
<div class="d-flex space-children-mr justify-content-end">
|
<div class="d-flex space-children-mr justify-content-end">
|
||||||
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||||
[disabled]="(hasChanges$ | async) !== true"
|
[dsBtnDisabled]="(hasChanges$ | async) !== true"
|
||||||
(click)="discard()">
|
(click)="discard()">
|
||||||
<i aria-hidden="true" class="fas fa-times"></i>
|
<i aria-hidden="true" class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{ 'item.edit.metadata.discard-button' | translate }}</span>
|
<span class="d-none d-sm-inline"> {{ 'item.edit.metadata.discard-button' | translate }}</span>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<span class="d-none d-sm-inline"> {{ 'item.edit.metadata.reinstate-button' | translate }}</span>
|
<span class="d-none d-sm-inline"> {{ 'item.edit.metadata.reinstate-button' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary"
|
||||||
[disabled]="(hasChanges$ | async) !== true || (isSaving$ | async) === true"
|
[dsBtnDisabled]="(hasChanges$ | async) !== true || (isSaving$ | async) === true"
|
||||||
(click)="submit()">
|
(click)="submit()">
|
||||||
<span *ngIf="isSaving$ | async" aria-hidden="true" class="spinner-border spinner-border-sm" role="status"></span>
|
<span *ngIf="isSaving$ | async" aria-hidden="true" class="spinner-border spinner-border-sm" role="status"></span>
|
||||||
<i *ngIf="(isSaving$ | async) !== true" aria-hidden="true" class="fas fa-save"></i>
|
<i *ngIf="(isSaving$ | async) !== true" aria-hidden="true" class="fas fa-save"></i>
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
<div class="buttons" *ngIf="medias?.length > 1">
|
<div class="buttons" *ngIf="medias?.length > 1">
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary previous"
|
class="btn btn-primary previous"
|
||||||
[disabled]="currentIndex === 0"
|
[dsBtnDisabled]="currentIndex === 0"
|
||||||
(click)="prevMedia()"
|
(click)="prevMedia()"
|
||||||
>
|
>
|
||||||
{{ "media-viewer.previous" | translate }}
|
{{ "media-viewer.previous" | translate }}
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary next"
|
class="btn btn-primary next"
|
||||||
[disabled]="currentIndex === medias.length - 1"
|
[dsBtnDisabled]="currentIndex === medias.length - 1"
|
||||||
(click)="nextMedia()"
|
(click)="nextMedia()"
|
||||||
>
|
>
|
||||||
{{ "media-viewer.next" | translate }}
|
{{ "media-viewer.next" | translate }}
|
||||||
|
@@ -48,7 +48,7 @@
|
|||||||
<div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner">
|
<div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()"
|
<button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()"
|
||||||
[disabled]="(unlinkProcessing | async)">
|
[dsBtnDisabled]="(unlinkProcessing | async)">
|
||||||
<span *ngIf="!(unlinkProcessing | async)"><i
|
<span *ngIf="!(unlinkProcessing | async)"><i
|
||||||
class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span>
|
class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span>
|
||||||
<span *ngIf="(unlinkProcessing | async)"><i
|
<span *ngIf="(unlinkProcessing | async)"><i
|
||||||
|
@@ -7,12 +7,12 @@
|
|||||||
<ds-themed-loading *ngIf="(i + 1) === objects.length && (itemsRD || i > 0) && !(itemsRD?.hasSucceeded && itemsRD?.payload && itemsRD?.payload?.page?.length > 0)" message="{{'loading.default' | translate}}"></ds-themed-loading>
|
<ds-themed-loading *ngIf="(i + 1) === objects.length && (itemsRD || i > 0) && !(itemsRD?.hasSucceeded && itemsRD?.payload && itemsRD?.payload?.page?.length > 0)" message="{{'loading.default' | translate}}"></ds-themed-loading>
|
||||||
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && itemsRD?.payload?.page?.length > 0">
|
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && itemsRD?.payload?.page?.length > 0">
|
||||||
<div *ngIf="itemsRD?.payload?.totalPages > objects.length" class="float-left" id="view-more">
|
<div *ngIf="itemsRD?.payload?.totalPages > objects.length" class="float-left" id="view-more">
|
||||||
<button class="btn btn-link btn-link-inline" (click)="increase()">{{'item.page.related-items.view-more' |
|
<button class="btn btn-link btn-link-inline text-capitalize" (click)="increase()">{{'item.page.related-items.view-more' |
|
||||||
translate:{ amount: (itemsRD?.payload?.totalElements - (incrementBy * objects.length) < incrementBy) ? itemsRD?.payload?.totalElements - (incrementBy * objects.length) : incrementBy } }}</button>
|
translate:{ amount: (itemsRD?.payload?.totalElements - (incrementBy * objects.length) < incrementBy) ? itemsRD?.payload?.totalElements - (incrementBy * objects.length) : incrementBy } }} {{label}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="objects.length > 1" class="float-right" id="view-less">
|
<div *ngIf="objects.length > 1" class="float-right" id="view-less">
|
||||||
<button class="btn btn-link btn-link-inline" (click)="decrease()">{{'item.page.related-items.view-less' |
|
<button class="btn btn-link btn-link-inline text-capitalize" (click)="decrease()">{{'item.page.related-items.view-less' |
|
||||||
translate:{ amount: itemsRD?.payload?.page?.length } }}</button>
|
translate:{ amount: itemsRD?.payload?.page?.length } }} {{label}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -65,7 +65,7 @@
|
|||||||
<!--CREATE-->
|
<!--CREATE-->
|
||||||
<ng-container *ngIf="canCreateVersion$ | async">
|
<ng-container *ngIf="canCreateVersion$ | async">
|
||||||
<button class="btn btn-outline-primary btn-sm version-row-element-create"
|
<button class="btn btn-outline-primary btn-sm version-row-element-create"
|
||||||
[disabled]="isAnyBeingEdited() || (hasDraftVersion$ | async)"
|
[dsBtnDisabled]="isAnyBeingEdited() || (hasDraftVersion$ | async)"
|
||||||
(click)="createNewVersion(versionDTO.version)"
|
(click)="createNewVersion(versionDTO.version)"
|
||||||
title="{{createVersionTitle$ | async | translate }}">
|
title="{{createVersionTitle$ | async | translate }}">
|
||||||
<i class="fas fa-code-branch fa-fw"></i>
|
<i class="fas fa-code-branch fa-fw"></i>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<ng-container *ngIf="versionDTO.canDeleteVersion">
|
<ng-container *ngIf="versionDTO.canDeleteVersion">
|
||||||
<button class="btn btn-sm version-row-element-delete"
|
<button class="btn btn-sm version-row-element-delete"
|
||||||
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
|
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
|
||||||
[disabled]="isAnyBeingEdited()"
|
[dsBtnDisabled]="isAnyBeingEdited()"
|
||||||
(click)="deleteVersion(versionDTO.version, versionDTO.version.id==itemVersion.id)"
|
(click)="deleteVersion(versionDTO.version, versionDTO.version.id==itemVersion.id)"
|
||||||
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
|
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
|
||||||
<i class="fas fa-trash fa-fw"></i>
|
<i class="fas fa-trash fa-fw"></i>
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
<ng-container *ngIf="versionDTO.canEditVersion | async">
|
<ng-container *ngIf="versionDTO.canEditVersion | async">
|
||||||
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
|
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
|
||||||
*ngIf="!isThisBeingEdited(versionDTO.version)"
|
*ngIf="!isThisBeingEdited(versionDTO.version)"
|
||||||
[disabled]="isAnyBeingEdited()"
|
[dsBtnDisabled]="isAnyBeingEdited()"
|
||||||
(click)="enableVersionEditing(versionDTO.version)"
|
(click)="enableVersionEditing(versionDTO.version)"
|
||||||
title="{{'item.version.history.table.action.editSummary' | translate}}">
|
title="{{'item.version.history.table.action.editSummary' | translate}}">
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
|
@@ -29,6 +29,7 @@ import { ConfigurationDataService } from '../../core/data/configuration-data.ser
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ItemSharedModule } from '../item-shared.module';
|
import { ItemSharedModule } from '../item-shared.module';
|
||||||
|
import {BtnDisabledDirective} from '../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
describe('ItemVersionsComponent', () => {
|
describe('ItemVersionsComponent', () => {
|
||||||
let component: ItemVersionsComponent;
|
let component: ItemVersionsComponent;
|
||||||
@@ -136,7 +137,7 @@ describe('ItemVersionsComponent', () => {
|
|||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ItemVersionsComponent, VarDirective],
|
declarations: [ItemVersionsComponent, VarDirective, BtnDisabledDirective],
|
||||||
imports: [TranslateModule.forRoot(), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemSharedModule],
|
imports: [TranslateModule.forRoot(), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemSharedModule],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: PaginationService, useValue: new PaginationServiceStub()},
|
{provide: PaginationService, useValue: new PaginationServiceStub()},
|
||||||
@@ -222,17 +223,20 @@ describe('ItemVersionsComponent', () => {
|
|||||||
it('should not disable the delete button', () => {
|
it('should not disable the delete button', () => {
|
||||||
const deleteButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-delete`));
|
const deleteButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-delete`));
|
||||||
deleteButtons.forEach((btn) => {
|
deleteButtons.forEach((btn) => {
|
||||||
expect(btn.nativeElement.disabled).toBe(false);
|
expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('false');
|
||||||
|
expect(btn.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should disable other buttons', () => {
|
it('should disable other buttons', () => {
|
||||||
const createButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-create`));
|
const createButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-create`));
|
||||||
createButtons.forEach((btn) => {
|
createButtons.forEach((btn) => {
|
||||||
expect(btn.nativeElement.disabled).toBe(true);
|
expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(btn.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
});
|
});
|
||||||
const editButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-create`));
|
const editButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-create`));
|
||||||
editButtons.forEach((btn) => {
|
editButtons.forEach((btn) => {
|
||||||
expect(btn.nativeElement.disabled).toBe(true);
|
expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(btn.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<div class="add" *ngIf="!(moreThanOne$ | async)">
|
<div class="add" *ngIf="!(moreThanOne$ | async)">
|
||||||
<button class="btn btn-lg btn-outline-primary mt-1 ml-2"
|
<button class="btn btn-lg btn-outline-primary mt-1 ml-2"
|
||||||
[attr.aria-label]="'mydspace.new-submission-external' | translate" [disabled]="!(initialized$|async)"
|
[attr.aria-label]="'mydspace.new-submission-external' | translate" [dsBtnDisabled]="!(initialized$|async)"
|
||||||
(click)="openPage(singleEntity)" role="button"
|
(click)="openPage(singleEntity)" role="button"
|
||||||
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>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
ngbDropdown
|
ngbDropdown
|
||||||
*ngIf="(moreThanOne$ | async)">
|
*ngIf="(moreThanOne$ | async)">
|
||||||
<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" [dsBtnDisabled]="!(initialized$|async)"
|
||||||
[attr.aria-label]="'mydspace.new-submission-external' | translate"
|
[attr.aria-label]="'mydspace.new-submission-external' | translate"
|
||||||
[attr.data-test]="'import-dropdown' | dsBrowserOnly"
|
[attr.data-test]="'import-dropdown' | dsBrowserOnly"
|
||||||
title="{{'mydspace.new-submission-external' | translate}}">
|
title="{{'mydspace.new-submission-external' | translate}}">
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<div class="add" *ngIf="!(moreThanOne$ | async)">
|
<div class="add" *ngIf="!(moreThanOne$ | async)">
|
||||||
<button class="btn btn-lg btn-primary mt-1 ml-2" [attr.aria-label]="'mydspace.new-submission' | translate"
|
<button class="btn btn-lg btn-primary mt-1 ml-2" [attr.aria-label]="'mydspace.new-submission' | translate"
|
||||||
[disabled]="!(initialized$|async)" (click)="openDialog(singleEntity)" role="button">
|
[dsBtnDisabled]="!(initialized$|async)" (click)="openDialog(singleEntity)" role="button">
|
||||||
<i class="fa fa-plus-circle" aria-hidden="true"></i>
|
<i class="fa fa-plus-circle" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
ngbDropdown
|
ngbDropdown
|
||||||
*ngIf="(moreThanOne$ | async)">
|
*ngIf="(moreThanOne$ | async)">
|
||||||
<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" [dsBtnDisabled]="!(initialized$|async)"
|
||||||
[attr.aria-label]="'mydspace.new-submission' | translate"
|
[attr.aria-label]="'mydspace.new-submission' | translate"
|
||||||
[attr.data-test]="'submission-dropdown' | dsBrowserOnly"
|
[attr.data-test]="'submission-dropdown' | dsBrowserOnly"
|
||||||
title="{{'mydspace.new-submission' | translate}}">
|
title="{{'mydspace.new-submission' | translate}}">
|
||||||
|
@@ -1,35 +1,37 @@
|
|||||||
<div class="ds-menu-item-wrapper text-md-center"
|
<div class="ds-menu-item-wrapper text-md-center"
|
||||||
[id]="'expandable-navbar-section-' + section.id"
|
[id]="'expandable-navbar-section-' + section.id"
|
||||||
(mouseenter)="onMouseEnter($event, isActive)"
|
(mouseenter)="onMouseEnter($event)"
|
||||||
(mouseleave)="onMouseLeave($event, isActive)"
|
(mouseleave)="onMouseLeave($event)"
|
||||||
data-test="navbar-section-wrapper"
|
data-test="navbar-section-wrapper">
|
||||||
*ngVar="(active | async) as isActive">
|
|
||||||
<a href="javascript:void(0);" routerLinkActive="active"
|
<a href="javascript:void(0);" routerLinkActive="active"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
(keyup.enter)="toggleSection($event)"
|
(keyup.enter)="toggleSection($event)"
|
||||||
(keyup.space)="toggleSection($event)"
|
(keyup.space)="toggleSection($event)"
|
||||||
(click)="toggleSection($event)"
|
(click)="toggleSection($event)"
|
||||||
(keydown.space)="$event.preventDefault()"
|
(keydown)="keyDown($event)"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
data-test="navbar-section-toggler"
|
data-test="navbar-section-toggler"
|
||||||
[attr.aria-expanded]="isActive"
|
[attr.aria-expanded]="(active$ | async).valueOf()"
|
||||||
[attr.aria-controls]="expandableNavbarSectionId(section.id)"
|
[attr.aria-controls]="expandableNavbarSectionId()"
|
||||||
class="d-flex flex-row flex-nowrap align-items-center gapx-1 ds-menu-toggler-wrapper"
|
class="d-flex flex-row flex-nowrap align-items-center gapx-1 ds-menu-toggler-wrapper"
|
||||||
[class.disabled]="section.model?.disabled">
|
[class.disabled]="section.model?.disabled">
|
||||||
<span class="flex-fill">
|
<span class="flex-fill">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
<!-- <span class="sr-only">{{'nav.expandable-navbar-section-suffix' | translate}}</span>-->
|
|
||||||
</span>
|
</span>
|
||||||
<i class="fas fa-caret-down fa-xs toggle-menu-icon" aria-hidden="true"></i>
|
<i class="fas fa-caret-down fa-xs toggle-menu-icon" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<div @slide *ngIf="isActive" (click)="deactivateSection($event)"
|
<div *ngIf="(active$ | async).valueOf() === true" (click)="deactivateSection($event)"
|
||||||
[id]="expandableNavbarSectionId(section.id)"
|
[id]="expandableNavbarSectionId()"
|
||||||
|
[dsHoverOutsideOfParentSelector]="'#expandable-navbar-section-' + section.id"
|
||||||
|
(dsHoverOutside)="deactivateSection($event, false)"
|
||||||
role="menu"
|
role="menu"
|
||||||
class="dropdown-menu show nav-dropdown-menu m-0 shadow-none border-top-0 px-3 px-md-0 pt-0 pt-md-1">
|
class="dropdown-menu show nav-dropdown-menu m-0 shadow-none border-top-0 px-3 px-md-0 pt-0 pt-md-1">
|
||||||
|
<div @slide role="presentation">
|
||||||
<div *ngFor="let subSection of (subSections$ | async)" class="text-nowrap" role="presentation">
|
<div *ngFor="let subSection of (subSections$ | async)" class="text-nowrap" role="presentation">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,15 +1,17 @@
|
|||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync, fakeAsync, flush } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ExpandableNavbarSectionComponent } from './expandable-navbar-section.component';
|
import { ExpandableNavbarSectionComponent } from './expandable-navbar-section.component';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
||||||
import { Component } from '@angular/core';
|
import { Component, DebugElement } from '@angular/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { HostWindowService } from '../../shared/host-window.service';
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
|
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
||||||
|
import { MenuSection } from '../../shared/menu/menu-section.model';
|
||||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { HoverOutsideDirective } from '../../shared/utils/hover-outside.directive';
|
||||||
|
|
||||||
describe('ExpandableNavbarSectionComponent', () => {
|
describe('ExpandableNavbarSectionComponent', () => {
|
||||||
let component: ExpandableNavbarSectionComponent;
|
let component: ExpandableNavbarSectionComponent;
|
||||||
@@ -20,18 +22,18 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NoopAnimationsModule],
|
imports: [NoopAnimationsModule],
|
||||||
declarations: [ExpandableNavbarSectionComponent, TestComponent, VarDirective],
|
declarations: [
|
||||||
|
ExpandableNavbarSectionComponent,
|
||||||
|
HoverOutsideDirective,
|
||||||
|
TestComponent,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: 'sectionDataProvider', useValue: {} },
|
{ provide: 'sectionDataProvider', useValue: {} },
|
||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
]
|
TestComponent,
|
||||||
}).overrideComponent(ExpandableNavbarSectionComponent, {
|
],
|
||||||
set: {
|
}).compileComponents();
|
||||||
entryComponents: [TestComponent]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -43,10 +45,6 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the mouse enters the section header (while inactive)', () => {
|
describe('when the mouse enters the section header (while inactive)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(component, 'onMouseEnter').and.callThrough();
|
spyOn(component, 'onMouseEnter').and.callThrough();
|
||||||
@@ -143,6 +141,8 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when spacebar is pressed on section header (while inactive)', () => {
|
describe('when spacebar is pressed on section header (while inactive)', () => {
|
||||||
|
let sidebarToggler: DebugElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(component, 'toggleSection').and.callThrough();
|
spyOn(component, 'toggleSection').and.callThrough();
|
||||||
spyOn(menuService, 'toggleActiveSection');
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
@@ -151,15 +151,27 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
// dispatch the (keyup.space) action used in our component HTML
|
|
||||||
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call toggleSection on the menuService', () => {
|
it('should call toggleSection on the menuService', () => {
|
||||||
|
// dispatch the (keyup.space) action used in our component HTML
|
||||||
|
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space', key: ' ' }));
|
||||||
|
|
||||||
expect(component.toggleSection).toHaveBeenCalled();
|
expect(component.toggleSection).toHaveBeenCalled();
|
||||||
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Should not do anything in order to work correctly with NVDA: https://www.nvaccess.org/
|
||||||
|
it('should not do anything on keydown space', () => {
|
||||||
|
const event: Event = new KeyboardEvent('keydown', { code: 'Space', key: ' ' });
|
||||||
|
spyOn(event, 'preventDefault').and.callThrough();
|
||||||
|
|
||||||
|
// dispatch the (keyup.space) action used in our component HTML
|
||||||
|
sidebarToggler.nativeElement.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when spacebar is pressed on section header (while active)', () => {
|
describe('when spacebar is pressed on section header (while active)', () => {
|
||||||
@@ -181,13 +193,116 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when enter is pressed on section header (while inactive)', () => {
|
||||||
|
let sidebarToggler: DebugElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(component, 'toggleSection').and.callThrough();
|
||||||
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
|
// Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property.
|
||||||
|
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false));
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not do anything in order to work correctly with NVDA: https://www.nvaccess.org/
|
||||||
|
it('should not do anything on keydown space', () => {
|
||||||
|
const event: Event = new KeyboardEvent('keydown', { code: 'Enter' });
|
||||||
|
spyOn(event, 'preventDefault').and.callThrough();
|
||||||
|
|
||||||
|
// dispatch the (keyup.space) action used in our component HTML
|
||||||
|
sidebarToggler.nativeElement.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when arrow down is pressed on section header', () => {
|
||||||
|
it('should call activateSection', () => {
|
||||||
|
spyOn(component, 'activateSection').and.callThrough();
|
||||||
|
|
||||||
|
const sidebarToggler: DebugElement = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
|
// dispatch the (keydown.ArrowDown) action used in our component HTML
|
||||||
|
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowDown' }));
|
||||||
|
|
||||||
|
expect(component.focusOnFirstChildSection).toBe(true);
|
||||||
|
expect(component.activateSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when tab is pressed on section header', () => {
|
||||||
|
it('should call deactivateSection', () => {
|
||||||
|
spyOn(component, 'deactivateSection').and.callThrough();
|
||||||
|
|
||||||
|
const sidebarToggler: DebugElement = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
|
// dispatch the (keydown.ArrowDown) action used in our component HTML
|
||||||
|
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Tab' }));
|
||||||
|
|
||||||
|
expect(component.deactivateSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('navigateDropdown', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
|
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([
|
||||||
|
Object.assign(new MenuSection(), {
|
||||||
|
id: 'subSection1',
|
||||||
|
model: Object.assign(new LinkMenuItemModel(), {
|
||||||
|
type: 'TEST_LINK',
|
||||||
|
}),
|
||||||
|
parentId: component.section.id,
|
||||||
|
}),
|
||||||
|
Object.assign(new MenuSection(), {
|
||||||
|
id: 'subSection2',
|
||||||
|
model: Object.assign(new LinkMenuItemModel(), {
|
||||||
|
type: 'TEST_LINK',
|
||||||
|
}),
|
||||||
|
parentId: component.section.id,
|
||||||
|
}),
|
||||||
|
]));
|
||||||
|
component.ngOnInit();
|
||||||
|
flush();
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.focusOnFirstChildSection = true;
|
||||||
|
component.active$.next(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should close the modal on Tab', () => {
|
||||||
|
spyOn(menuService, 'deactivateSection').and.callThrough();
|
||||||
|
|
||||||
|
const firstSubsection: DebugElement = fixture.debugElement.queryAll(By.css('.dropdown-menu a[role="menuitem"]'))[0];
|
||||||
|
firstSubsection.nativeElement.focus();
|
||||||
|
firstSubsection.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Tab' }));
|
||||||
|
|
||||||
|
expect(menuService.deactivateSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the modal on Escape', () => {
|
||||||
|
spyOn(menuService, 'deactivateSection').and.callThrough();
|
||||||
|
|
||||||
|
const firstSubsection: DebugElement = fixture.debugElement.queryAll(By.css('.dropdown-menu a[role="menuitem"]'))[0];
|
||||||
|
firstSubsection.nativeElement.focus();
|
||||||
|
firstSubsection.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape' }));
|
||||||
|
|
||||||
|
expect(menuService.deactivateSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on smaller, mobile screens', () => {
|
describe('on smaller, mobile screens', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NoopAnimationsModule],
|
imports: [NoopAnimationsModule],
|
||||||
declarations: [ExpandableNavbarSectionComponent, TestComponent, VarDirective],
|
declarations: [
|
||||||
|
ExpandableNavbarSectionComponent,
|
||||||
|
HoverOutsideDirective,
|
||||||
|
TestComponent,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: 'sectionDataProvider', useValue: {} },
|
{ provide: 'sectionDataProvider', useValue: {} },
|
||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
@@ -261,7 +376,9 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
// declare a test component
|
// declare a test component
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-test-cmp',
|
selector: 'ds-test-cmp',
|
||||||
template: ``
|
template: `
|
||||||
|
<a role="menuitem">link</a>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
class TestComponent {
|
class TestComponent {
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, HostListener, Inject, Injector, OnInit } from '@angular/core';
|
import { Component, HostListener, Inject, Injector, OnInit, AfterViewChecked, OnDestroy } from '@angular/core';
|
||||||
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
|
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { slide } from '../../shared/animations/slide';
|
import { slide } from '../../shared/animations/slide';
|
||||||
@@ -6,6 +6,7 @@ import { first } from 'rxjs/operators';
|
|||||||
import { HostWindowService } from '../../shared/host-window.service';
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { MenuSection } from '../../shared/menu/menu-section.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an expandable section in the navbar
|
* Represents an expandable section in the navbar
|
||||||
@@ -16,7 +17,8 @@ import { Observable } from 'rxjs';
|
|||||||
styleUrls: ['./expandable-navbar-section.component.scss'],
|
styleUrls: ['./expandable-navbar-section.component.scss'],
|
||||||
animations: [slide]
|
animations: [slide]
|
||||||
})
|
})
|
||||||
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit {
|
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements AfterViewChecked, OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This section resides in the Public Navbar
|
* This section resides in the Public Navbar
|
||||||
*/
|
*/
|
||||||
@@ -27,6 +29,11 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
*/
|
*/
|
||||||
mouseEntered = false;
|
mouseEntered = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the section was expanded
|
||||||
|
*/
|
||||||
|
focusOnFirstChildSection = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if screen size was small before a resize event
|
* True if screen size was small before a resize event
|
||||||
*/
|
*/
|
||||||
@@ -37,6 +44,18 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
*/
|
*/
|
||||||
isMobile$: Observable<boolean>;
|
isMobile$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean used to add the event listeners to the items in the expandable menu when expanded. This is done for
|
||||||
|
* performance reasons, there is currently an *ngIf on the menu to prevent the {@link HoverOutsideDirective} to tank
|
||||||
|
* performance when not expanded.
|
||||||
|
*/
|
||||||
|
addArrowEventListeners = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of current dropdown items who have event listeners
|
||||||
|
*/
|
||||||
|
private dropdownItems: NodeListOf<HTMLElement>;
|
||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
@HostListener('window:resize', ['$event'])
|
||||||
onResize() {
|
onResize() {
|
||||||
this.isMobile$.pipe(
|
this.isMobile$.pipe(
|
||||||
@@ -51,29 +70,80 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(@Inject('sectionDataProvider') menuSection,
|
constructor(
|
||||||
|
@Inject('sectionDataProvider') public section: MenuSection,
|
||||||
protected menuService: MenuService,
|
protected menuService: MenuService,
|
||||||
protected injector: Injector,
|
protected injector: Injector,
|
||||||
private windowService: HostWindowService
|
protected windowService: HostWindowService,
|
||||||
) {
|
) {
|
||||||
super(menuSection, menuService, injector);
|
super(section, menuService, injector);
|
||||||
this.isMobile$ = this.windowService.isMobile();
|
this.isMobile$ = this.windowService.isMobile();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
|
this.subs.push(this.active$.subscribe((active: boolean) => {
|
||||||
|
if (active === true) {
|
||||||
|
this.addArrowEventListeners = true;
|
||||||
|
} else {
|
||||||
|
this.focusOnFirstChildSection = undefined;
|
||||||
|
this.unsubscribeFromEventListeners();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewChecked(): void {
|
||||||
|
if (this.addArrowEventListeners) {
|
||||||
|
this.dropdownItems = document.querySelectorAll(`#${this.expandableNavbarSectionId()} *[role="menuitem"]`);
|
||||||
|
this.dropdownItems.forEach((item: HTMLElement) => {
|
||||||
|
item.addEventListener('keydown', this.navigateDropdown.bind(this));
|
||||||
|
});
|
||||||
|
if (this.focusOnFirstChildSection && this.dropdownItems.length > 0) {
|
||||||
|
this.dropdownItems.item(0).focus();
|
||||||
|
}
|
||||||
|
this.addArrowEventListeners = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
this.unsubscribeFromEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate this section if it's currently inactive, deactivate it when it's currently active.
|
||||||
|
* Also saves whether this toggle was performed by a keyboard event (non-click event) in order to know if thi first
|
||||||
|
* item should be focussed when activating a section.
|
||||||
|
*
|
||||||
|
* @param {Event} event The user event that triggered this method
|
||||||
|
*/
|
||||||
|
override toggleSection(event: Event): void {
|
||||||
|
this.focusOnFirstChildSection = event.type !== 'click';
|
||||||
|
super.toggleSection(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all the current event listeners on the dropdown items (called when the menu is closed & on component
|
||||||
|
* destruction)
|
||||||
|
*/
|
||||||
|
unsubscribeFromEventListeners(): void {
|
||||||
|
if (this.dropdownItems) {
|
||||||
|
this.dropdownItems.forEach((item: HTMLElement) => {
|
||||||
|
item.removeEventListener('keydown', this.navigateDropdown.bind(this));
|
||||||
|
});
|
||||||
|
this.dropdownItems = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the mouse enters the section toggler activate the menu section
|
* When the mouse enters the section toggler activate the menu section
|
||||||
* @param $event
|
* @param $event
|
||||||
* @param isActive
|
|
||||||
*/
|
*/
|
||||||
onMouseEnter($event: Event, isActive: boolean) {
|
onMouseEnter($event: Event): void {
|
||||||
this.isMobile$.pipe(
|
this.isMobile$.pipe(
|
||||||
first()
|
first()
|
||||||
).subscribe((isMobile) => {
|
).subscribe((isMobile) => {
|
||||||
if (!isMobile && !isActive && !this.mouseEntered) {
|
if (!isMobile && !this.active$.value && !this.mouseEntered) {
|
||||||
this.activateSection($event);
|
this.activateSection($event);
|
||||||
}
|
}
|
||||||
this.mouseEntered = true;
|
this.mouseEntered = true;
|
||||||
@@ -83,13 +153,12 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
/**
|
/**
|
||||||
* When the mouse leaves the section toggler deactivate the menu section
|
* When the mouse leaves the section toggler deactivate the menu section
|
||||||
* @param $event
|
* @param $event
|
||||||
* @param isActive
|
|
||||||
*/
|
*/
|
||||||
onMouseLeave($event: Event, isActive: boolean) {
|
onMouseLeave($event: Event): void {
|
||||||
this.isMobile$.pipe(
|
this.isMobile$.pipe(
|
||||||
first()
|
first()
|
||||||
).subscribe((isMobile) => {
|
).subscribe((isMobile) => {
|
||||||
if (!isMobile && isActive && this.mouseEntered) {
|
if (!isMobile && this.active$.value && this.mouseEntered) {
|
||||||
this.deactivateSection($event);
|
this.deactivateSection($event);
|
||||||
}
|
}
|
||||||
this.mouseEntered = false;
|
this.mouseEntered = false;
|
||||||
@@ -98,9 +167,60 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* returns the ID of the DOM element representing the navbar section
|
* returns the ID of the DOM element representing the navbar section
|
||||||
* @param sectionId
|
|
||||||
*/
|
*/
|
||||||
expandableNavbarSectionId(sectionId: string) {
|
expandableNavbarSectionId(): string {
|
||||||
return `expandable-navbar-section-${sectionId}-dropdown`;
|
return `expandable-navbar-section-${this.section.id}-dropdown`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the navigation between the menu items
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
navigateDropdown(event: KeyboardEvent): void {
|
||||||
|
if (event.code === 'Tab') {
|
||||||
|
this.deactivateSection(event, false);
|
||||||
|
return;
|
||||||
|
} else if (event.code === 'Escape') {
|
||||||
|
this.deactivateSection(event, false);
|
||||||
|
(document.querySelector(`a[aria-controls="${this.expandableNavbarSectionId()}"]`) as HTMLElement)?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const items: NodeListOf<Element> = document.querySelectorAll(`#${this.expandableNavbarSectionId()} *[role="menuitem"]`);
|
||||||
|
if (items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentIndex: number = Array.from(items).findIndex((item: Element) => item === event.target);
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
(items[(currentIndex + 1) % items.length] as HTMLElement).focus();
|
||||||
|
} else if (event.key === 'ArrowUp') {
|
||||||
|
(items[(currentIndex - 1 + items.length) % items.length] as HTMLElement).focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles all the keydown events on the dropdown toggle
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
keyDown(event: KeyboardEvent): void {
|
||||||
|
switch (event.code) {
|
||||||
|
// Works for both Tab & Shift Tab
|
||||||
|
case 'Tab':
|
||||||
|
this.deactivateSection(event, false);
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
this.focusOnFirstChildSection = true;
|
||||||
|
this.activateSection(event);
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
case 'Enter':
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import { MenuModule } from '../shared/menu/menu.module';
|
|||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ThemedNavbarComponent } from './themed-navbar.component';
|
import { ThemedNavbarComponent } from './themed-navbar.component';
|
||||||
|
import { HoverOutsideDirective } from '../shared/utils/hover-outside.directive';
|
||||||
|
|
||||||
const effects = [
|
const effects = [
|
||||||
NavbarEffects
|
NavbarEffects
|
||||||
@@ -25,6 +26,10 @@ const ENTRY_COMPONENTS = [
|
|||||||
ThemedExpandableNavbarSectionComponent,
|
ThemedExpandableNavbarSectionComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DIRECTIVES = [
|
||||||
|
HoverOutsideDirective,
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -35,12 +40,14 @@ const ENTRY_COMPONENTS = [
|
|||||||
CoreModule.forRoot()
|
CoreModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
...DIRECTIVES,
|
||||||
...ENTRY_COMPONENTS,
|
...ENTRY_COMPONENTS,
|
||||||
NavbarComponent,
|
NavbarComponent,
|
||||||
ThemedNavbarComponent,
|
ThemedNavbarComponent,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [
|
exports: [
|
||||||
|
...DIRECTIVES,
|
||||||
ThemedNavbarComponent,
|
ThemedNavbarComponent,
|
||||||
NavbarSectionComponent,
|
NavbarSectionComponent,
|
||||||
ThemedExpandableNavbarSectionComponent
|
ThemedExpandableNavbarSectionComponent
|
||||||
|
@@ -1,20 +1,47 @@
|
|||||||
<div class="form-group" *ngIf="scripts$ | async">
|
<div class="d-flex w-100 flex-column gap-3">
|
||||||
<label for="process-script">{{'process.new.select-script' | translate}}</label>
|
<div>
|
||||||
<select required id="process-script"
|
<div ngbDropdown class="d-flex">
|
||||||
|
<input id="process-script"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="script"
|
required
|
||||||
[(ngModel)]="selectedScript"
|
[ngModel]="selectedScript"
|
||||||
|
placeholder="{{'process.new.select-script.placeholder' | translate}}"
|
||||||
|
[ngModelOptions]="{standalone: true}"
|
||||||
|
ngbDropdownToggle
|
||||||
|
role="combobox"
|
||||||
#script="ngModel">
|
#script="ngModel">
|
||||||
<option [ngValue]="undefined">{{'process.new.select-script.placeholder' | translate}}</option>
|
<div ngbDropdownMenu aria-labelledby="process-script" class="w-100 scrollable-menu"
|
||||||
<option *ngFor="let script of scripts$ | async" [ngValue]="script.id">
|
role="menu"
|
||||||
{{script.name}}
|
(scroll)="onScroll($event)"
|
||||||
</option>
|
infiniteScroll
|
||||||
</select>
|
[infiniteScrollDistance]="5"
|
||||||
|
[infiniteScrollThrottle]="300"
|
||||||
|
[infiniteScrollUpDistance]="1.5"
|
||||||
|
[fromRoot]="true"
|
||||||
|
[scrollWindow]="false">
|
||||||
|
<button class="dropdown-item"
|
||||||
|
*ngFor="let script of scripts"
|
||||||
|
role="menuitem"
|
||||||
|
type="button"
|
||||||
|
title="{{ script.name }}"
|
||||||
|
(click)="onSelect(script);">
|
||||||
|
<span class="text-truncate">{{ script.name }}</span>
|
||||||
|
</button>
|
||||||
|
<ng-container *ngIf="(isLoading$ | async)">
|
||||||
|
<button class="dropdown-item disabled" role="menuitem">
|
||||||
|
<ds-loading message="{{'loading.default' | translate}}">
|
||||||
|
</ds-loading>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<div *ngIf="script.invalid && (script.dirty || script.touched)"
|
<div *ngIf="script.invalid && (script.dirty || script.touched)"
|
||||||
class="alert alert-danger validation-error">
|
class="alert alert-danger validation-error">
|
||||||
<div *ngIf="script.errors.required">
|
<div *ngIf="script.errors.required">
|
||||||
{{'process.new.select-script.required' | translate}}
|
{{ 'process.new.select-script.required' | translate }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,23 @@
|
|||||||
|
.dropdown-item {
|
||||||
|
padding: 0.35rem 1rem;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-menu {
|
||||||
|
height: auto;
|
||||||
|
max-height: var(--ds-dropdown-menu-max-height);
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:not(:last-of-type) .dropdown-item {
|
||||||
|
border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#entityControlsDropdownMenu {
|
||||||
|
outline: 0;
|
||||||
|
left: 0 !important;
|
||||||
|
box-shadow: var(--bs-btn-focus-box-shadow);
|
||||||
|
}
|
||||||
|
@@ -74,7 +74,7 @@ describe('ScriptsSelectComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
const select = fixture.debugElement.query(By.css('select'));
|
const select = fixture.debugElement.query(By.css('#process-script'));
|
||||||
select.triggerEventHandler('blur', null);
|
select.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -88,7 +88,7 @@ describe('ScriptsSelectComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
const select = fixture.debugElement.query(By.css('select'));
|
const select = fixture.debugElement.query(By.css('#process-script'));
|
||||||
select.triggerEventHandler('blur', null);
|
select.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Optional, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Optional, Output } from '@angular/core';
|
||||||
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
||||||
import { Script } from '../../scripts/script.model';
|
import { Script } from '../../scripts/script.model';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, Subscription, tap } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
import {
|
||||||
|
getRemoteDataPayload,
|
||||||
|
getFirstCompletedRemoteData
|
||||||
|
} from '../../../core/shared/operators';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { ControlContainer, NgForm } from '@angular/forms';
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
import { controlContainerFactory } from '../process-form.component';
|
import { controlContainerFactory } from '../process-form.component';
|
||||||
|
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||||
|
|
||||||
const SCRIPT_QUERY_PARAMETER = 'script';
|
const SCRIPT_QUERY_PARAMETER = 'script';
|
||||||
|
|
||||||
@@ -31,10 +35,20 @@ export class ScriptsSelectComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* All available scripts
|
* All available scripts
|
||||||
*/
|
*/
|
||||||
scripts$: Observable<Script[]>;
|
scripts: Script[] = [];
|
||||||
|
|
||||||
private _selectedScript: Script;
|
private _selectedScript: Script;
|
||||||
private routeSub: Subscription;
|
private routeSub: Subscription;
|
||||||
|
|
||||||
|
private _isLastPage = false;
|
||||||
|
|
||||||
|
scriptOptions: FindListOptions = {
|
||||||
|
elementsPerPage: 20,
|
||||||
|
currentPage: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
isLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private scriptService: ScriptDataService,
|
private scriptService: ScriptDataService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -47,33 +61,48 @@ export class ScriptsSelectComponent implements OnInit, OnDestroy {
|
|||||||
* Checks if the route contains a script ID and auto selects this scripts
|
* Checks if the route contains a script ID and auto selects this scripts
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.scripts$ = this.scriptService.findAll({ elementsPerPage: 9999 })
|
this.loadScripts();
|
||||||
.pipe(
|
}
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
map((paginatedList: PaginatedList<Script>) => paginatedList.page)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.routeSub = this.route.queryParams
|
/**
|
||||||
.pipe(
|
* Load the scripts and check if the route contains a script
|
||||||
filter((params: Params) => hasNoValue(params.id)),
|
*/
|
||||||
map((params: Params) => params[SCRIPT_QUERY_PARAMETER]),
|
loadScripts() {
|
||||||
distinctUntilChanged(),
|
if (this.isLoading$.value) {return;}
|
||||||
switchMap((id: string) =>
|
this.isLoading$.next(true);
|
||||||
this.scripts$
|
|
||||||
.pipe(
|
this.routeSub = this.scriptService.findAll(this.scriptOptions).pipe(
|
||||||
take(1),
|
getFirstCompletedRemoteData(),
|
||||||
map((scripts) =>
|
getRemoteDataPayload(),
|
||||||
scripts.find((script) => script.id === id)
|
tap((paginatedList: PaginatedList<Script>) => {
|
||||||
)
|
this._isLastPage = paginatedList?.pageInfo?.currentPage >= paginatedList?.pageInfo?.totalPages;
|
||||||
)
|
}),
|
||||||
)
|
map((paginatedList: PaginatedList<Script>) => paginatedList.page),
|
||||||
).subscribe((script: Script) => {
|
).subscribe((newScripts: Script[]) => {
|
||||||
this._selectedScript = script;
|
this.scripts = [...this.scripts, ...newScripts];
|
||||||
this.select.emit(script);
|
this.isLoading$.next(false);
|
||||||
|
|
||||||
|
const param = this.route.snapshot.queryParams[SCRIPT_QUERY_PARAMETER];
|
||||||
|
if (hasValue(param)) {
|
||||||
|
this._selectedScript = this.scripts.find((script) => script.id === param);
|
||||||
|
this.select.emit(this._selectedScript);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load more scripts when the user scrolls to the bottom of the list
|
||||||
|
* @param event The scroll event
|
||||||
|
*/
|
||||||
|
onScroll(event: any) {
|
||||||
|
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) {
|
||||||
|
if (!this.isLoading$.value && !this._isLastPage) {
|
||||||
|
this.scriptOptions.currentPage++;
|
||||||
|
this.loadScripts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the identifier of the selected script
|
* Returns the identifier of the selected script
|
||||||
*/
|
*/
|
||||||
@@ -93,6 +122,17 @@ export class ScriptsSelectComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectScript(script: Script) {
|
||||||
|
this._selectedScript = script;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(newScript: Script) {
|
||||||
|
this.selectScript(newScript);
|
||||||
|
// this._selectedScript = newScript;
|
||||||
|
this.select.emit(newScript);
|
||||||
|
this.selectedScript = newScript.name;
|
||||||
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set script(value: Script) {
|
set script(value: Script) {
|
||||||
this._selectedScript = value;
|
this._selectedScript = value;
|
||||||
|
@@ -77,10 +77,10 @@
|
|||||||
<span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span>
|
<span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button class="btn btn-primary mr-2" [disabled]="processBulkDeleteService.isProcessing$() |async"
|
<button class="btn btn-primary mr-2" [dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async"
|
||||||
(click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
|
(click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
|
||||||
<button id="delete-confirm" class="btn btn-danger"
|
<button id="delete-confirm" class="btn btn-danger"
|
||||||
[disabled]="processBulkDeleteService.isProcessing$() |async"
|
[dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async"
|
||||||
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
|
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
<input type="checkbox" [checked]="checked" (change)="toggleCheckbox()"/>
|
<input type="checkbox" [checked]="checked" (change)="toggleCheckbox()"/>
|
||||||
{{ 'dso-selector.claim.item.not-mine-label' | translate }}
|
{{ 'dso-selector.claim.item.not-mine-label' | translate }}
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary ml-5 mr-2" (click)="createFromScratch()" [disabled]="!checked">
|
<button type="submit" class="btn btn-primary ml-5 mr-2" (click)="createFromScratch()" [dsBtnDisabled]="!checked">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
{{ 'dso-selector.claim.item.create-from-scratch' | translate }}
|
{{ 'dso-selector.claim.item.create-from-scratch' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
<p>{{'researcher.profile.not.associated' | translate}}</p>
|
<p>{{'researcher.profile.not.associated' | translate}}</p>
|
||||||
</div>
|
</div>
|
||||||
<button *ngIf="!researcherProfile" class="btn btn-primary mr-2"
|
<button *ngIf="!researcherProfile" class="btn btn-primary mr-2"
|
||||||
[disabled]="(isProcessingCreate() | async)"
|
[dsBtnDisabled]="(isProcessingCreate() | async)"
|
||||||
(click)="createProfile()">
|
(click)="createProfile()">
|
||||||
<span *ngIf="(isProcessingCreate() | async)">
|
<span *ngIf="(isProcessingCreate() | async)">
|
||||||
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
|
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<ng-container *ngIf="researcherProfile">
|
<ng-container *ngIf="researcherProfile">
|
||||||
<button class="btn btn-primary mr-2" [disabled]="!researcherProfile" (click)="viewProfile(researcherProfile)">
|
<button class="btn btn-primary mr-2" [dsBtnDisabled]="!researcherProfile" (click)="viewProfile(researcherProfile)">
|
||||||
<i class="fas fa-info-circle"></i> {{'researcher.profile.view' | translate}}
|
<i class="fas fa-info-circle"></i> {{'researcher.profile.view' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" [disabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)">
|
<button class="btn btn-danger" [dsBtnDisabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)">
|
||||||
<span *ngIf="(isProcessingDelete() | async)">
|
<span *ngIf="(isProcessingDelete() | async)">
|
||||||
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
|
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
|
||||||
</span>
|
</span>
|
||||||
|
@@ -173,7 +173,8 @@ export class ProfilePageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
updateSecurity() {
|
updateSecurity() {
|
||||||
const passEntered = isNotEmpty(this.password);
|
const passEntered = isNotEmpty(this.password);
|
||||||
if (this.invalidSecurity) {
|
const validCurrentPassword = isNotEmpty(this.currentPassword);
|
||||||
|
if (validCurrentPassword && !passEntered) {
|
||||||
this.notificationsService.error(this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.general'));
|
this.notificationsService.error(this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.general'));
|
||||||
}
|
}
|
||||||
if (!this.invalidSecurity && passEntered) {
|
if (!this.invalidSecurity && passEntered) {
|
||||||
|
@@ -51,13 +51,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="!((googleRecaptchaService.captchaVersion() | async) === 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible'); else v2Invisible">
|
<ng-container *ngIf="!((googleRecaptchaService.captchaVersion() | async) === 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible'); else v2Invisible">
|
||||||
<button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
|
<button class="btn btn-primary" [dsBtnDisabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
|
||||||
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #v2Invisible>
|
<ng-template #v2Invisible>
|
||||||
<button class="btn btn-primary" [disabled]="form.invalid" (click)="executeRecaptcha()">
|
<button class="btn btn-primary" [dsBtnDisabled]="form.invalid" (click)="executeRecaptcha()">
|
||||||
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@@ -81,7 +81,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button
|
<button
|
||||||
[disabled]="isInValidPassword || userInfoForm.invalid"
|
[dsBtnDisabled]="isInValidPassword || userInfoForm.invalid"
|
||||||
class="btn btn-default btn-primary"
|
class="btn btn-default btn-primary"
|
||||||
(click)="submitEperson()">{{'register-page.create-profile.submit' | translate}}</button>
|
(click)="submitEperson()">{{'register-page.create-profile.submit' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
<div class="d-flex flex-row-reverse">
|
<div class="d-flex flex-row-reverse">
|
||||||
<button (click)="submit()"
|
<button (click)="submit()"
|
||||||
[disabled]="!subject || subject.length === 0"
|
[dsBtnDisabled]="!subject || subject.length === 0"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
title="{{'grant-deny-request-copy.email.send' | translate }}">
|
title="{{'grant-deny-request-copy.email.send' | translate }}">
|
||||||
<i class="fas fa-envelope"></i> {{'grant-deny-request-copy.email.send' | translate }}
|
<i class="fas fa-envelope"></i> {{'grant-deny-request-copy.email.send' | translate }}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
import { SearchComponent } from '../shared/search/search.component';
|
import { SearchComponent } from '../shared/search/search.component';
|
||||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, PLATFORM_ID } from '@angular/core';
|
||||||
import { pushInOut } from '../shared/animations/push';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
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 { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
@@ -35,7 +35,8 @@ export class ConfigurationSearchPageComponent extends SearchComponent {
|
|||||||
protected routeService: RouteService,
|
protected routeService: RouteService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
|
@Inject(PLATFORM_ID) public platformId: any,
|
||||||
) {
|
) {
|
||||||
super(service, sidebarService, windowService, searchConfigService, routeService, router, appConfig);
|
super(service, sidebarService, windowService, searchConfigService, routeService, router, appConfig, platformId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -47,7 +47,7 @@
|
|||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline-secondary fas fa-calendar"
|
class="btn btn-outline-secondary fas fa-calendar"
|
||||||
[disabled]="ngForm.disabled"
|
[dsBtnDisabled]="ngForm.disabled"
|
||||||
(click)="d.toggle()" type="button">
|
(click)="d.toggle()" type="button">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button
|
<button
|
||||||
type="button" class="btn btn-outline-secondary fas fa-calendar"
|
type="button" class="btn btn-outline-secondary fas fa-calendar"
|
||||||
[disabled]="ngForm.disabled"
|
[dsBtnDisabled]="ngForm.disabled"
|
||||||
(click)="d1.toggle()">
|
(click)="d1.toggle()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
|
|
||||||
<button type="button" class="btn btn-outline-danger"
|
<button type="button" class="btn btn-outline-danger"
|
||||||
[attr.aria-label]="'access-control-remove' | translate"
|
[attr.aria-label]="'access-control-remove' | translate"
|
||||||
[disabled]="ngForm.disabled || form.accessControls.length === 1"
|
[dsBtnDisabled]="ngForm.disabled || form.accessControls.length === 1"
|
||||||
(click)="removeAccessControlItem(control.id)">
|
(click)="removeAccessControlItem(control.id)">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<button type="button" id="add-btn-{{type}}" class="btn btn-outline-primary mt-3"
|
<button type="button" id="add-btn-{{type}}" class="btn btn-outline-primary mt-3"
|
||||||
[disabled]="ngForm.disabled"
|
[dsBtnDisabled]="ngForm.disabled"
|
||||||
(click)="addAccessControlItem()">
|
(click)="addAccessControlItem()">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
{{'access-control-add-more' | translate}}
|
{{'access-control-add-more' | translate}}
|
||||||
|
@@ -103,7 +103,7 @@
|
|||||||
<button
|
<button
|
||||||
*ngIf="itemRD"
|
*ngIf="itemRD"
|
||||||
[attr.aria-label]="'access-control-bitstreams-select' | translate"
|
[attr.aria-label]="'access-control-bitstreams-select' | translate"
|
||||||
[disabled]="!state.bitstream.toggleStatus || state.bitstream.changesLimit !== 'selected'"
|
[dsBtnDisabled]="!state.bitstream.toggleStatus || state.bitstream.changesLimit !== 'selected'"
|
||||||
(click)="openSelectBitstreamsModal(itemRD.payload)"
|
(click)="openSelectBitstreamsModal(itemRD.payload)"
|
||||||
class="btn btn-outline-dark border-0" type="button">
|
class="btn btn-outline-dark border-0" type="button">
|
||||||
<i class="fa fa-search"></i>
|
<i class="fa fa-search"></i>
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
{{ 'access-control-cancel' | translate }}
|
{{ 'access-control-cancel' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary"
|
||||||
[disabled]="!state.item.toggleStatus && !state.bitstream.toggleStatus"
|
[dsBtnDisabled]="!state.item.toggleStatus && !state.bitstream.toggleStatus"
|
||||||
(click)="submit()" type="submit">
|
(click)="submit()" type="submit">
|
||||||
{{ 'access-control-execute' | translate }}
|
{{ 'access-control-execute' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
57
src/app/shared/btn-disabled.directive.ts
Normal file
57
src/app/shared/btn-disabled.directive.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Directive, Input, HostBinding, HostListener } from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[dsBtnDisabled]'
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This directive can be added to a html element to disable it (make it non-interactive).
|
||||||
|
* It acts as a replacement for HTML's disabled attribute.
|
||||||
|
*
|
||||||
|
* This directive should always be used instead of the HTML disabled attribute as it is more accessible.
|
||||||
|
*/
|
||||||
|
export class BtnDisabledDirective {
|
||||||
|
|
||||||
|
@Input() set dsBtnDisabled(value: boolean) {
|
||||||
|
this.isDisabled = !!value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the aria-disabled attribute to the directive's isDisabled property.
|
||||||
|
* This is used to make the element accessible to screen readers. If the element is disabled, the screen reader will announce it as such.
|
||||||
|
*/
|
||||||
|
@HostBinding('attr.aria-disabled') isDisabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the class attribute to the directive's isDisabled property.
|
||||||
|
* This is used to style the element when it is disabled (make it look disabled).
|
||||||
|
*/
|
||||||
|
@HostBinding('class.disabled') get disabledClass() { return this.isDisabled; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents the default action and stops the event from propagating when the element is disabled.
|
||||||
|
* This is used to prevent the element from being interacted with when it is disabled (via mouse click).
|
||||||
|
* @param event The click event.
|
||||||
|
*/
|
||||||
|
@HostListener('click', ['$event'])
|
||||||
|
handleClick(event: Event) {
|
||||||
|
if (this.isDisabled) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents the default action and stops the event from propagating when the element is disabled.
|
||||||
|
* This is used to prevent the element from being interacted with when it is disabled (via keystrokes).
|
||||||
|
* @param event The keydown event.
|
||||||
|
*/
|
||||||
|
@HostListener('keydown', ['$event'])
|
||||||
|
handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (this.isDisabled && (event.key === 'Enter' || event.key === 'Space')) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
97
src/app/shared/disabled-directive.spec.ts
Normal file
97
src/app/shared/disabled-directive.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { Component, DebugElement } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { BtnDisabledDirective } from './btn-disabled.directive';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<button [dsBtnDisabled]="isDisabled">Test Button</button>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class TestComponent {
|
||||||
|
isDisabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DisabledDirective', () => {
|
||||||
|
let component: TestComponent;
|
||||||
|
let fixture: ComponentFixture<TestComponent>;
|
||||||
|
let button: DebugElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [TestComponent, BtnDisabledDirective]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(TestComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
button = fixture.debugElement.query(By.css('button'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bind aria-disabled to false initially', () => {
|
||||||
|
expect(button.nativeElement.getAttribute('aria-disabled')).toBe('false');
|
||||||
|
expect(button.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bind aria-disabled to true and add disabled class when isDisabled is true', () => {
|
||||||
|
component.isDisabled = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(button.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(button.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bind aria-disabled to false and not have disabled class when isDisabled is false', () => {
|
||||||
|
component.isDisabled = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(button.nativeElement.getAttribute('aria-disabled')).toBe('false');
|
||||||
|
expect(button.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent click events when disabled', () => {
|
||||||
|
component.isDisabled = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let clickHandled = false;
|
||||||
|
button.nativeElement.addEventListener('click', () => clickHandled = true);
|
||||||
|
|
||||||
|
button.nativeElement.click();
|
||||||
|
|
||||||
|
expect(clickHandled).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent Enter or Space keydown events when disabled', () => {
|
||||||
|
component.isDisabled = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let keydownHandled = false;
|
||||||
|
button.nativeElement.addEventListener('keydown', () => keydownHandled = true);
|
||||||
|
|
||||||
|
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
|
||||||
|
const spaceEvent = new KeyboardEvent('keydown', { key: 'Space' });
|
||||||
|
|
||||||
|
button.nativeElement.dispatchEvent(enterEvent);
|
||||||
|
button.nativeElement.dispatchEvent(spaceEvent);
|
||||||
|
|
||||||
|
expect(keydownHandled).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow click and keydown events when not disabled', () => {
|
||||||
|
let clickHandled = false;
|
||||||
|
let keydownHandled = false;
|
||||||
|
|
||||||
|
button.nativeElement.addEventListener('click', () => clickHandled = true);
|
||||||
|
button.nativeElement.addEventListener('keydown', () => keydownHandled = true);
|
||||||
|
|
||||||
|
button.nativeElement.click();
|
||||||
|
|
||||||
|
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
|
||||||
|
const spaceEvent = new KeyboardEvent('keydown', { key: 'Space' });
|
||||||
|
|
||||||
|
button.nativeElement.dispatchEvent(enterEvent);
|
||||||
|
button.nativeElement.dispatchEvent(spaceEvent);
|
||||||
|
|
||||||
|
expect(clickHandled).toBeTrue();
|
||||||
|
expect(keydownHandled).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
@@ -13,7 +13,7 @@
|
|||||||
class="btn btn-outline-primary selection"
|
class="btn btn-outline-primary selection"
|
||||||
(blur)="close.emit($event)"
|
(blur)="close.emit($event)"
|
||||||
(click)="close.emit($event)"
|
(click)="close.emit($event)"
|
||||||
[disabled]="disabled"
|
[dsBtnDisabled]="disabled"
|
||||||
ngbDropdownToggle>
|
ngbDropdownToggle>
|
||||||
<ng-content select=".selection"></ng-content>
|
<ng-content select=".selection"></ng-content>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<div class="dso-button-menu mb-1" ngbDropdown container="body" placement="bottom-right">
|
<div class="dso-button-menu mb-1" ngbDropdown container="body" placement="bottom-right">
|
||||||
<div class="d-flex flex-row flex-nowrap"
|
<div class="d-flex flex-row flex-nowrap"
|
||||||
[ngbTooltip]="itemModel.text | translate" container="body">
|
[ngbTooltip]="itemModel.text | translate" container="body">
|
||||||
<button [attr.aria-label]="itemModel.text | translate" [title]="itemModel.text | translate" class="btn btn-dark btn-sm" ngbDropdownToggle [disabled]="section.model?.disabled">
|
<button [attr.aria-label]="itemModel.text | translate" [title]="itemModel.text | translate" class="btn btn-dark btn-sm" ngbDropdownToggle [dsBtnDisabled]="section.model?.disabled">
|
||||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul ngbDropdownMenu class="dso-edit-menu-dropdown">
|
<ul ngbDropdownMenu class="dso-edit-menu-dropdown">
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
|
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
|
||||||
<span class="sr-only">{{itemModel.text | translate}}</span>
|
<span class="sr-only">{{itemModel.text | translate}}</span>
|
||||||
</a>
|
</a>
|
||||||
<button *ngIf="section.model.disabled" class="btn btn-dark btn-sm" [disabled]="true">
|
<button *ngIf="section.model.disabled" class="btn btn-dark btn-sm" [dsBtnDisabled]="true">
|
||||||
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
|
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
|
||||||
<span class="sr-only">{{itemModel.text | translate}}</span>
|
<span class="sr-only">{{itemModel.text | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<div *ngIf="canActivate" class="dso-button-menu mb-1"
|
<div *ngIf="canActivate" class="dso-button-menu mb-1"
|
||||||
[ngbTooltip]="itemModel.text | translate">
|
[ngbTooltip]="itemModel.text | translate">
|
||||||
<button class="btn btn-dark btn-sm" [disabled]="section.model.disabled"
|
<button class="btn btn-dark btn-sm" [dsBtnDisabled]="section.model.disabled"
|
||||||
(click)="activate($event)">
|
(click)="activate($event)">
|
||||||
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
|
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
|
||||||
<span class="sr-only">{{itemModel.text | translate}}</span>
|
<span class="sr-only">{{itemModel.text | translate}}</span>
|
||||||
|
@@ -10,6 +10,7 @@ import {
|
|||||||
DsoEditMenuExpandableSectionComponent
|
DsoEditMenuExpandableSectionComponent
|
||||||
} from '../dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component';
|
} from '../dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component';
|
||||||
import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {SharedModule} from '../shared.module';
|
||||||
|
|
||||||
const COMPONENTS = [
|
const COMPONENTS = [
|
||||||
DsoEditMenuComponent,
|
DsoEditMenuComponent,
|
||||||
@@ -26,6 +27,7 @@ const MODULES = [
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
|
SharedModule
|
||||||
];
|
];
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
|
|
||||||
|
@@ -5,9 +5,11 @@
|
|||||||
[ngClass]="getClass('element', 'control')">
|
[ngClass]="getClass('element', 'control')">
|
||||||
|
|
||||||
<!-- Draggable Container -->
|
<!-- Draggable Container -->
|
||||||
<div cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="moveSelection($event)">
|
<div role="listbox" [attr.aria-label]="'dynamic-form-array.sortable-list.label' | translate" #dropList cdkDropList
|
||||||
|
cdkDropListLockAxis="y" (cdkDropListDropped)="moveSelection($event)">
|
||||||
<!-- Draggable Items -->
|
<!-- Draggable Items -->
|
||||||
<div *ngFor="let groupModel of model.groups"
|
<div #sortableElement
|
||||||
|
*ngFor="let groupModel of model.groups; let idx = index; let length = count"
|
||||||
role="group"
|
role="group"
|
||||||
[formGroupName]="groupModel.index"
|
[formGroupName]="groupModel.index"
|
||||||
[ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]"
|
[ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]"
|
||||||
@@ -16,7 +18,14 @@
|
|||||||
[cdkDragPreviewClass]="'ds-submission-reorder-dragging'"
|
[cdkDragPreviewClass]="'ds-submission-reorder-dragging'"
|
||||||
[class.grey-background]="model.isInlineGroupArray">
|
[class.grey-background]="model.isInlineGroupArray">
|
||||||
<!-- Item content -->
|
<!-- Item content -->
|
||||||
<div class="drag-handle" [class.drag-disable]="dragDisabled" tabindex="0" cdkDragHandle>
|
<div class="drag-handle" [class.drag-disable]="dragDisabled" tabindex="0" cdkDragHandle
|
||||||
|
(focus)="addInstructionMessageToLiveRegion(sortableElement)"
|
||||||
|
(keydown.space)="toggleKeyboardDragAndDrop($event, sortableElement, idx, length)"
|
||||||
|
(keydown.enter)="toggleKeyboardDragAndDrop($event, sortableElement, idx, length)"
|
||||||
|
(keydown.tab)="stopKeyboardDragAndDrop(sortableElement, idx, length)"
|
||||||
|
(keydown.escape)="cancelKeyboardDragAndDrop(sortableElement, idx, length)"
|
||||||
|
(keydown.arrowUp)="handleArrowPress($event, dropList, length, idx, 'up')"
|
||||||
|
(keydown.arrowDown)="handleArrowPress($event, dropList, length, idx, 'down')">
|
||||||
<i class="drag-icon fas fa-grip-vertical fa-fw" [class.drag-disable]="dragDisabled" ></i>
|
<i class="drag-icon fas fa-grip-vertical fa-fw" [class.drag-disable]="dragDisabled" ></i>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
|
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
|
||||||
|
@@ -17,7 +17,6 @@
|
|||||||
margin-right: calc(-0.5 * var(--bs-spacer));
|
margin-right: calc(-0.5 * var(--bs-spacer));
|
||||||
padding-right: calc(0.5 * var(--bs-spacer));
|
padding-right: calc(0.5 * var(--bs-spacer));
|
||||||
.drag-icon {
|
.drag-icon {
|
||||||
visibility: hidden;
|
|
||||||
width: calc(2 * var(--bs-spacer));
|
width: calc(2 * var(--bs-spacer));
|
||||||
color: var(--bs-gray-600);
|
color: var(--bs-gray-600);
|
||||||
margin: var(--bs-btn-padding-y) 0;
|
margin: var(--bs-btn-padding-y) 0;
|
||||||
@@ -27,9 +26,6 @@
|
|||||||
|
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
.drag-icon {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -40,18 +36,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
.drag-icon {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cdk-drop-list-dragging {
|
.cdk-drop-list-dragging {
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
.drag-icon {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,3 +53,9 @@
|
|||||||
.cdk-drag-placeholder {
|
.cdk-drag-placeholder {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.sorting-with-keyboard input {
|
||||||
|
background-color: var(--bs-gray-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,169 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { EventEmitter } from '@angular/core';
|
||||||
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
inject,
|
||||||
|
TestBed,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import {
|
||||||
|
DynamicFormLayoutService,
|
||||||
|
DynamicFormService,
|
||||||
|
DynamicFormValidationService,
|
||||||
|
DynamicInputModel,
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import {
|
||||||
|
TranslateLoader,
|
||||||
|
TranslateModule,
|
||||||
|
TranslateService,
|
||||||
|
} from '@ngx-translate/core';
|
||||||
|
import { NgxMaskModule } from 'ngx-mask';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
APP_CONFIG,
|
||||||
|
} from '../../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../../environments/environment.test';
|
||||||
|
import { SubmissionService } from '../../../../../../submission/submission.service';
|
||||||
|
import { DsDynamicFormControlContainerComponent } from '../../ds-dynamic-form-control-container.component';
|
||||||
|
import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model';
|
||||||
|
import { DsDynamicFormArrayComponent } from './dynamic-form-array.component';
|
||||||
|
import { UUIDService } from '../../../../../../core/shared/uuid.service';
|
||||||
|
import { TranslateLoaderMock } from '../../../../../mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('DsDynamicFormArrayComponent', () => {
|
||||||
|
const translateServiceStub = {
|
||||||
|
get: () => of('translated-text'),
|
||||||
|
instant: () => 'translated-text',
|
||||||
|
onLangChange: new EventEmitter(),
|
||||||
|
onTranslationChange: new EventEmitter(),
|
||||||
|
onDefaultLangChange: new EventEmitter(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const uuidServiceStub = {
|
||||||
|
generate: () => 'fake-id'
|
||||||
|
};
|
||||||
|
|
||||||
|
let component: DsDynamicFormArrayComponent;
|
||||||
|
let fixture: ComponentFixture<DsDynamicFormArrayComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
DsDynamicFormArrayComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgxMaskModule.forRoot(),
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
DynamicFormLayoutService,
|
||||||
|
DynamicFormValidationService,
|
||||||
|
provideMockStore(),
|
||||||
|
{ provide: TranslateService, useValue: translateServiceStub },
|
||||||
|
{ provide: HttpClient, useValue: {} },
|
||||||
|
{ provide: SubmissionService, useValue: {} },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
|
{ provide: UUIDService, useValue: uuidServiceStub },
|
||||||
|
],
|
||||||
|
}).overrideComponent(DsDynamicFormArrayComponent, {
|
||||||
|
remove: {
|
||||||
|
imports: [DsDynamicFormControlContainerComponent],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([DynamicFormService], (service: DynamicFormService) => {
|
||||||
|
const formModel = [
|
||||||
|
new DynamicRowArrayModel({
|
||||||
|
id: 'testFormRowArray',
|
||||||
|
initialCount: 5,
|
||||||
|
notRepeatable: false,
|
||||||
|
relationshipConfig: undefined,
|
||||||
|
submissionId: '1234',
|
||||||
|
isDraggable: true,
|
||||||
|
groupFactory: () => {
|
||||||
|
return [
|
||||||
|
new DynamicInputModel({ id: 'testFormRowArrayGroupInput' }),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
metadataKey: 'dc.contributor.author',
|
||||||
|
metadataFields: ['dc.contributor.author'],
|
||||||
|
hasSelectableMetadata: true,
|
||||||
|
showButtons: true,
|
||||||
|
typeBindRelations: [{ match: 'VISIBLE', operator: 'OR', when: [{ id: 'dc.type', value: 'Book' }] }],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DsDynamicFormArrayComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.model = formModel[0] as DynamicRowArrayModel;
|
||||||
|
|
||||||
|
component.group = service.createFormGroup(formModel);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should move element up and maintain focus', () => {
|
||||||
|
const dropList = fixture.debugElement.query(By.css('[cdkDropList]')).nativeElement;
|
||||||
|
component.handleArrowPress(new KeyboardEvent('keydown', { key: 'ArrowUp' }), dropList, 3, 1, 'up');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.model.groups[0]).toBeDefined();
|
||||||
|
expect(document.activeElement).toBe(dropList.querySelectorAll('[cdkDragHandle]')[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should move element down and maintain focus', () => {
|
||||||
|
const dropList = fixture.debugElement.query(By.css('[cdkDropList]')).nativeElement;
|
||||||
|
component.handleArrowPress(new KeyboardEvent('keydown', { key: 'ArrowDown' }), dropList, 3, 1, 'down');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.model.groups[2]).toBeDefined();
|
||||||
|
expect(document.activeElement).toBe(dropList.querySelectorAll('[cdkDragHandle]')[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wrap around when moving up from the first element', () => {
|
||||||
|
const dropList = fixture.debugElement.query(By.css('[cdkDropList]')).nativeElement;
|
||||||
|
component.handleArrowPress(new KeyboardEvent('keydown', { key: 'ArrowUp' }), dropList, 3, 0, 'up');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.model.groups[2]).toBeDefined();
|
||||||
|
expect(document.activeElement).toBe(dropList.querySelectorAll('[cdkDragHandle]')[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wrap around when moving down from the last element', () => {
|
||||||
|
const dropList = fixture.debugElement.query(By.css('[cdkDropList]')).nativeElement;
|
||||||
|
component.handleArrowPress(new KeyboardEvent('keydown', { key: 'ArrowDown' }), dropList, 3, 2, 'down');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.model.groups[0]).toBeDefined();
|
||||||
|
expect(document.activeElement).toBe(dropList.querySelectorAll('[cdkDragHandle]')[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not move element if keyboard drag is not active', () => {
|
||||||
|
const dropList = fixture.debugElement.query(By.css('[cdkDropList]')).nativeElement;
|
||||||
|
component.elementBeingSorted = null;
|
||||||
|
component.handleArrowPress(new KeyboardEvent('keydown', { key: 'ArrowDown' }), dropList, 3, 1, 'down');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.model.groups[1]).toBeDefined();
|
||||||
|
expect(document.activeElement).toBe(dropList.querySelectorAll('[cdkDragHandle]')[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cancel keyboard drag and drop', () => {
|
||||||
|
const dropList = fixture.debugElement.query(By.css('[cdkDropList]')).nativeElement;
|
||||||
|
component.elementBeingSortedStartingIndex = 2;
|
||||||
|
component.elementBeingSorted = dropList.querySelectorAll('[cdkDragHandle]')[2];
|
||||||
|
component.model.moveGroup(2, 1);
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.cancelKeyboardDragAndDrop(dropList, 1, 3);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.elementBeingSorted).toBeNull();
|
||||||
|
expect(component.elementBeingSortedStartingIndex).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@@ -15,6 +15,8 @@ import {
|
|||||||
import { Relationship } from '../../../../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../../../../core/shared/item-relationships/relationship.model';
|
||||||
import { hasValue } from '../../../../../empty.util';
|
import { hasValue } from '../../../../../empty.util';
|
||||||
import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model';
|
import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model';
|
||||||
|
import { LiveRegionService } from '../../../../../live-region/live-region.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dynamic-form-array',
|
selector: 'ds-dynamic-form-array',
|
||||||
@@ -31,6 +33,9 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
|
|||||||
@Input() model: DynamicRowArrayModel;// DynamicRow?
|
@Input() model: DynamicRowArrayModel;// DynamicRow?
|
||||||
@Input() templates: QueryList<DynamicTemplateDirective> | undefined;
|
@Input() templates: QueryList<DynamicTemplateDirective> | undefined;
|
||||||
|
|
||||||
|
elementBeingSorted: HTMLElement;
|
||||||
|
elementBeingSortedStartingIndex: number;
|
||||||
|
|
||||||
/* eslint-disable @angular-eslint/no-output-rename */
|
/* eslint-disable @angular-eslint/no-output-rename */
|
||||||
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||||
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||||
@@ -41,6 +46,8 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
|
|||||||
|
|
||||||
constructor(protected layoutService: DynamicFormLayoutService,
|
constructor(protected layoutService: DynamicFormLayoutService,
|
||||||
protected validationService: DynamicFormValidationService,
|
protected validationService: DynamicFormValidationService,
|
||||||
|
protected liveRegionService: LiveRegionService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
) {
|
) {
|
||||||
super(layoutService, validationService);
|
super(layoutService, validationService);
|
||||||
}
|
}
|
||||||
@@ -94,4 +101,149 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
|
|||||||
}
|
}
|
||||||
return this.control.get([groupModel.startingIndex]);
|
return this.control.get([groupModel.startingIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the keyboard drag and drop feature for the given sortable element.
|
||||||
|
* @param event
|
||||||
|
* @param sortableElement
|
||||||
|
* @param index
|
||||||
|
* @param length
|
||||||
|
*/
|
||||||
|
toggleKeyboardDragAndDrop(event: KeyboardEvent, sortableElement: HTMLDivElement, index: number, length: number) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.elementBeingSorted) {
|
||||||
|
this.stopKeyboardDragAndDrop(sortableElement, index, length);
|
||||||
|
} else {
|
||||||
|
sortableElement.classList.add('sorting-with-keyboard');
|
||||||
|
this.elementBeingSorted = sortableElement;
|
||||||
|
this.elementBeingSortedStartingIndex = index;
|
||||||
|
this.liveRegionService.clear();
|
||||||
|
this.liveRegionService.addMessage(this.translateService.instant('live-region.ordering.status', {
|
||||||
|
itemName: sortableElement.querySelector('input')?.value,
|
||||||
|
index: index + 1,
|
||||||
|
length,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the keyboard drag and drop feature.
|
||||||
|
* @param sortableElement
|
||||||
|
* @param index
|
||||||
|
* @param length
|
||||||
|
*/
|
||||||
|
stopKeyboardDragAndDrop(sortableElement: HTMLDivElement, index: number, length: number) {
|
||||||
|
this.elementBeingSorted?.classList.remove('sorting-with-keyboard');
|
||||||
|
this.liveRegionService.clear();
|
||||||
|
if (this.elementBeingSorted) {
|
||||||
|
this.elementBeingSorted = null;
|
||||||
|
this.elementBeingSortedStartingIndex = null;
|
||||||
|
this.liveRegionService.addMessage(this.translateService.instant('live-region.ordering.dropped', {
|
||||||
|
itemName: sortableElement.querySelector('input')?.value,
|
||||||
|
index: index + 1,
|
||||||
|
length,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the keyboard arrow press event to move the element up or down.
|
||||||
|
* @param event
|
||||||
|
* @param dropList
|
||||||
|
* @param length
|
||||||
|
* @param idx
|
||||||
|
* @param direction
|
||||||
|
*/
|
||||||
|
handleArrowPress(event: KeyboardEvent, dropList: HTMLDivElement, length: number, idx: number, direction: 'up' | 'down') {
|
||||||
|
let newIndex = direction === 'up' ? idx - 1 : idx + 1;
|
||||||
|
if (newIndex < 0) {
|
||||||
|
newIndex = length - 1;
|
||||||
|
} else if (newIndex >= length) {
|
||||||
|
newIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.elementBeingSorted) {
|
||||||
|
this.model.moveGroup(idx, newIndex - idx);
|
||||||
|
if (hasValue(this.model.groups[newIndex]) && hasValue((this.control as any).controls[newIndex])) {
|
||||||
|
this.onCustomEvent({
|
||||||
|
previousIndex: idx,
|
||||||
|
newIndex,
|
||||||
|
arrayModel: this.model,
|
||||||
|
model: this.model.groups[newIndex].group[0],
|
||||||
|
control: (this.control as any).controls[newIndex],
|
||||||
|
}, 'move');
|
||||||
|
this.liveRegionService.clear();
|
||||||
|
this.liveRegionService.addMessage(this.translateService.instant('live-region.ordering.moved', {
|
||||||
|
itemName: this.elementBeingSorted.querySelector('input')?.value,
|
||||||
|
index: newIndex + 1,
|
||||||
|
length,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
// Set focus back to the moved element
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setFocusToDropListElementOfIndex(dropList, newIndex, direction);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setFocusToDropListElementOfIndex(dropList, newIndex, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelKeyboardDragAndDrop(sortableElement: HTMLDivElement, index: number, length: number) {
|
||||||
|
this.model.moveGroup(index, this.elementBeingSortedStartingIndex - index);
|
||||||
|
if (hasValue(this.model.groups[this.elementBeingSortedStartingIndex]) && hasValue((this.control as any).controls[this.elementBeingSortedStartingIndex])) {
|
||||||
|
this.onCustomEvent({
|
||||||
|
previousIndex: index,
|
||||||
|
newIndex: this.elementBeingSortedStartingIndex,
|
||||||
|
arrayModel: this.model,
|
||||||
|
model: this.model.groups[this.elementBeingSortedStartingIndex].group[0],
|
||||||
|
control: (this.control as any).controls[this.elementBeingSortedStartingIndex],
|
||||||
|
}, 'move');
|
||||||
|
this.stopKeyboardDragAndDrop(sortableElement, this.elementBeingSortedStartingIndex, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets focus to the drag handle of the drop list element of the given index.
|
||||||
|
* @param dropList
|
||||||
|
* @param index
|
||||||
|
* @param direction
|
||||||
|
*/
|
||||||
|
setFocusToDropListElementOfIndex(dropList: HTMLDivElement, index: number, direction: 'up' | 'down') {
|
||||||
|
const newDragHandle = dropList.querySelectorAll(`[cdkDragHandle]`)[index] as HTMLElement;
|
||||||
|
if (newDragHandle) {
|
||||||
|
newDragHandle.focus();
|
||||||
|
if (!this.isElementInViewport(newDragHandle)) {
|
||||||
|
newDragHandle.scrollIntoView(direction === 'up');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if an element is in the viewport
|
||||||
|
* @param el
|
||||||
|
*/
|
||||||
|
isElementInViewport(el: HTMLElement) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an instruction message to the live region when the user might want to sort an element.
|
||||||
|
* @param sortableElement
|
||||||
|
*/
|
||||||
|
addInstructionMessageToLiveRegion(sortableElement: HTMLDivElement) {
|
||||||
|
if (!this.elementBeingSorted) {
|
||||||
|
this.liveRegionService.clear();
|
||||||
|
this.liveRegionService.addMessage(this.translateService.instant('live-region.ordering.instructions', {
|
||||||
|
itemName: sortableElement.querySelector('input')?.value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
[attr.aria-labelledby]="'label_' + model.id"
|
[attr.aria-labelledby]="'label_' + model.id"
|
||||||
[class.disabled]="model.disabled"
|
[class.disabled]="model.disabled"
|
||||||
[disabled]="model.disabled"
|
[dsBtnDisabled]="model.disabled"
|
||||||
(click)="datepicker.toggle()">
|
(click)="datepicker.toggle()">
|
||||||
|
|
||||||
<i *ngIf="model.toggleIcon" class="{{model.toggleIcon}}" aria-hidden="true"></i>
|
<i *ngIf="model.toggleIcon" class="{{model.toggleIcon}}" aria-hidden="true"></i>
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
[(ngModel)]="initialYear"
|
[(ngModel)]="initialYear"
|
||||||
[value]="year"
|
[value]="year"
|
||||||
[invalid]="showErrorMessages"
|
[invalid]="showErrorMessages"
|
||||||
[placeholder]='yearPlaceholder'
|
[placeholder]="'form.date-picker.placeholder.year' | translate"
|
||||||
(blur)="onBlur($event)"
|
(blur)="onBlur($event)"
|
||||||
(change)="onChange($event)"
|
(change)="onChange($event)"
|
||||||
(focus)="onFocus($event)"
|
(focus)="onFocus($event)"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
[size]="6"
|
[size]="6"
|
||||||
[(ngModel)]="initialMonth"
|
[(ngModel)]="initialMonth"
|
||||||
[value]="month"
|
[value]="month"
|
||||||
[placeholder]="monthPlaceholder"
|
[placeholder]="'form.date-picker.placeholder.month' | translate"
|
||||||
[disabled]="!year || model.disabled"
|
[disabled]="!year || model.disabled"
|
||||||
(blur)="onBlur($event)"
|
(blur)="onBlur($event)"
|
||||||
(change)="onChange($event)"
|
(change)="onChange($event)"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
[size]="2"
|
[size]="2"
|
||||||
[(ngModel)]="initialDay"
|
[(ngModel)]="initialDay"
|
||||||
[value]="day"
|
[value]="day"
|
||||||
[placeholder]="dayPlaceholder"
|
[placeholder]="'form.date-picker.placeholder.day' | translate"
|
||||||
[disabled]="!month || model.disabled"
|
[disabled]="!month || model.disabled"
|
||||||
(blur)="onBlur($event)"
|
(blur)="onBlur($event)"
|
||||||
(change)="onChange($event)"
|
(change)="onChange($event)"
|
||||||
|
@@ -14,6 +14,7 @@ import {
|
|||||||
mockDynamicFormValidationService
|
mockDynamicFormValidationService
|
||||||
} from '../../../../../testing/dynamic-form-mock-services';
|
} from '../../../../../testing/dynamic-form-mock-services';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
|
||||||
export const DATE_TEST_GROUP = new UntypedFormGroup({
|
export const DATE_TEST_GROUP = new UntypedFormGroup({
|
||||||
@@ -51,7 +52,8 @@ describe('DsDatePickerComponent test suite', () => {
|
|||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
NgbModule
|
NgbModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
DsDatePickerComponent,
|
DsDatePickerComponent,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user