mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'dspace-8_x' into accessibility-settings-8_x
This commit is contained in:
@@ -293,7 +293,8 @@
|
||||
],
|
||||
"rules": {
|
||||
// Custom DSpace Angular rules
|
||||
"dspace-angular-html/themed-component-usages": "error"
|
||||
"dspace-angular-html/themed-component-usages": "error",
|
||||
"dspace-angular-html/no-disabled-attribute-on-button": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -23,6 +23,31 @@ ssr:
|
||||
# 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.
|
||||
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
|
||||
# Enable request performance profiling data collection and printing the results in the server console.
|
||||
# Defaults to false. Enabling in production is NOT recommended
|
||||
#enablePerformanceProfiler: false
|
||||
|
||||
# The REST API server settings
|
||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||
@@ -33,6 +58,9 @@ rest:
|
||||
port: 443
|
||||
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
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
|
||||
cache:
|
||||
@@ -448,6 +476,12 @@ search:
|
||||
enabled: false
|
||||
# List of filters to enable in "Advanced Search" dropdown
|
||||
filter: [ 'title', 'author', 'subject', 'entityType' ]
|
||||
#
|
||||
# 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
|
||||
|
||||
|
||||
# Notify metrics
|
||||
|
@@ -2,3 +2,4 @@
|
||||
_______
|
||||
|
||||
- [`dspace-angular-html/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via the selector of their `ThemedComponent` wrapper class
|
||||
- [`dspace-angular-html/no-disabled-attribute-on-button`](./rules/no-disabled-attribute-on-button.md): Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
|
||||
|
78
docs/lint/html/rules/no-disabled-attribute-on-button.md
Normal file
78
docs/lint/html/rules/no-disabled-attribute-on-button.md
Normal file
@@ -0,0 +1,78 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [HTML rules](../index.md) > `dspace-angular-html/no-disabled-attribute-on-button`
|
||||
_______
|
||||
|
||||
Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
|
||||
This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
|
||||
The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/html/no-disabled-attribute-on-button.ts)
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### should use [dsBtnDisabled] in HTML templates
|
||||
|
||||
```html
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
```
|
||||
|
||||
##### disabled attribute is still valid on non-button elements
|
||||
|
||||
```html
|
||||
<input disabled>
|
||||
```
|
||||
|
||||
##### [disabled] attribute is still valid on non-button elements
|
||||
|
||||
```html
|
||||
<input [disabled]="true">
|
||||
```
|
||||
|
||||
##### angular dynamic attributes that use disabled are still valid
|
||||
|
||||
```html
|
||||
<button [class.disabled]="isDisabled">Submit</button>
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code & automatic fixes
|
||||
|
||||
##### should not use disabled attribute in HTML templates
|
||||
|
||||
```html
|
||||
<button disabled>Submit</button>
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```html
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
```
|
||||
|
||||
|
||||
##### should not use [disabled] attribute in HTML templates
|
||||
|
||||
```html
|
||||
<button [disabled]="true">Submit</button>
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```html
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
```
|
||||
|
||||
|
||||
|
@@ -10,10 +10,13 @@ import {
|
||||
bundle,
|
||||
RuleExports,
|
||||
} from '../../util/structure';
|
||||
import * as noDisabledAttributeOnButton from './no-disabled-attribute-on-button';
|
||||
import * as themedComponentUsages from './themed-component-usages';
|
||||
|
||||
const index = [
|
||||
themedComponentUsages,
|
||||
noDisabledAttributeOnButton,
|
||||
|
||||
] as unknown as RuleExports[];
|
||||
|
||||
export = {
|
||||
|
147
lint/src/rules/html/no-disabled-attribute-on-button.ts
Normal file
147
lint/src/rules/html/no-disabled-attribute-on-button.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
TmplAstBoundAttribute,
|
||||
TmplAstTextAttribute,
|
||||
} from '@angular-eslint/bundled-angular-compiler';
|
||||
import { TemplateParserServices } from '@angular-eslint/utils';
|
||||
import {
|
||||
ESLintUtils,
|
||||
TSESLint,
|
||||
} from '@typescript-eslint/utils';
|
||||
|
||||
import {
|
||||
DSpaceESLintRuleInfo,
|
||||
NamedTests,
|
||||
} from '../../util/structure';
|
||||
import { getSourceCode } from '../../util/typescript';
|
||||
|
||||
export enum Message {
|
||||
USE_DSBTN_DISABLED = 'mustUseDsBtnDisabled',
|
||||
}
|
||||
|
||||
export const info = {
|
||||
name: 'no-disabled-attribute-on-button',
|
||||
meta: {
|
||||
docs: {
|
||||
description: `Buttons should use the \`dsBtnDisabled\` directive instead of the HTML \`disabled\` attribute.
|
||||
This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
|
||||
The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.`,
|
||||
},
|
||||
type: 'problem',
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
messages: {
|
||||
[Message.USE_DSBTN_DISABLED]: 'Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.',
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
||||
const parserServices = getSourceCode(context).parserServices as TemplateParserServices;
|
||||
|
||||
/**
|
||||
* Some dynamic angular inputs will have disabled as name because of how Angular handles this internally (e.g [class.disabled]="isDisabled")
|
||||
* But these aren't actually the disabled attribute we're looking for, we can determine this by checking the details of the keySpan
|
||||
*/
|
||||
function isOtherAttributeDisabled(node: TmplAstBoundAttribute | TmplAstTextAttribute): boolean {
|
||||
// if the details are not null, and the details are not 'disabled', then it's not the disabled attribute we're looking for
|
||||
return node.keySpan?.details !== null && node.keySpan?.details !== 'disabled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the disabled text with [dsBtnDisabled] in the template
|
||||
*/
|
||||
function replaceDisabledText(text: string ): string {
|
||||
const hasBrackets = text.includes('[') && text.includes(']');
|
||||
const newDisabledText = hasBrackets ? 'dsBtnDisabled' : '[dsBtnDisabled]="true"';
|
||||
return text.replace('disabled', newDisabledText);
|
||||
}
|
||||
|
||||
function inputIsChildOfButton(node: any): boolean {
|
||||
return (node.parent?.tagName === 'button' || node.parent?.name === 'button');
|
||||
}
|
||||
|
||||
function reportAndFix(node: TmplAstBoundAttribute | TmplAstTextAttribute) {
|
||||
if (!inputIsChildOfButton(node) || isOtherAttributeDisabled(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceSpan = node.sourceSpan;
|
||||
context.report({
|
||||
messageId: Message.USE_DSBTN_DISABLED,
|
||||
loc: parserServices.convertNodeSourceSpanToLoc(sourceSpan),
|
||||
fix(fixer) {
|
||||
const templateText = sourceSpan.start.file.content;
|
||||
const disabledText = templateText.slice(sourceSpan.start.offset, sourceSpan.end.offset);
|
||||
const newText = replaceDisabledText(disabledText);
|
||||
return fixer.replaceTextRange([sourceSpan.start.offset, sourceSpan.end.offset], newText);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
'BoundAttribute[name="disabled"]'(node: TmplAstBoundAttribute) {
|
||||
reportAndFix(node);
|
||||
},
|
||||
'TextAttribute[name="disabled"]'(node: TmplAstTextAttribute) {
|
||||
reportAndFix(node);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const tests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'should use [dsBtnDisabled] in HTML templates',
|
||||
code: `
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disabled attribute is still valid on non-button elements',
|
||||
code: `
|
||||
<input disabled>
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: '[disabled] attribute is still valid on non-button elements',
|
||||
code: `
|
||||
<input [disabled]="true">
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'angular dynamic attributes that use disabled are still valid',
|
||||
code: `
|
||||
<button [class.disabled]="isDisabled">Submit</button>
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'should not use disabled attribute in HTML templates',
|
||||
code: `
|
||||
<button disabled>Submit</button>
|
||||
`,
|
||||
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
|
||||
output: `
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'should not use [disabled] attribute in HTML templates',
|
||||
code: `
|
||||
<button [disabled]="true">Submit</button>
|
||||
`,
|
||||
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
|
||||
output: `
|
||||
<button [dsBtnDisabled]="true">Submit</button>
|
||||
`,
|
||||
},
|
||||
],
|
||||
} as NamedTests;
|
||||
|
||||
export default rule;
|
23
package.json
23
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dspace-angular",
|
||||
"version": "8.1.0-next",
|
||||
"version": "8.2.0-next",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"config:watch": "nodemon",
|
||||
@@ -67,7 +67,7 @@
|
||||
"@angular/platform-server": "^17.3.11",
|
||||
"@angular/router": "^17.3.11",
|
||||
"@angular/ssr": "^17.3.11",
|
||||
"@babel/runtime": "7.26.0",
|
||||
"@babel/runtime": "7.26.7",
|
||||
"@kolkov/ngx-gallery": "^2.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||
"@ng-dynamic-forms/core": "^16.0.0",
|
||||
@@ -85,7 +85,7 @@
|
||||
"colors": "^1.4.0",
|
||||
"compression": "^1.7.5",
|
||||
"cookie-parser": "1.4.7",
|
||||
"core-js": "^3.39.0",
|
||||
"core-js": "^3.40.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns-tz": "^1.3.7",
|
||||
"deepmerge": "^4.3.1",
|
||||
@@ -96,7 +96,7 @@
|
||||
"filesize": "^6.1.0",
|
||||
"http-proxy-middleware": "^2.0.7",
|
||||
"http-terminator": "^3.2.0",
|
||||
"isbot": "^5.1.17",
|
||||
"isbot": "^5.1.22",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json5": "^2.2.3",
|
||||
@@ -106,7 +106,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.14.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
"mirador": "^3.4.2",
|
||||
"mirador": "^3.4.3",
|
||||
"mirador-dl-plugin": "^0.13.0",
|
||||
"mirador-share-plugin": "^0.16.0",
|
||||
"morgan": "^1.10.0",
|
||||
@@ -114,6 +114,7 @@
|
||||
"ng2-nouislider": "^2.0.0",
|
||||
"ngx-infinite-scroll": "^16.0.0",
|
||||
"ngx-pagination": "6.0.3",
|
||||
"ngx-skeleton-loader": "^9.0.0",
|
||||
"ngx-ui-switch": "^14.1.0",
|
||||
"nouislider": "^15.7.1",
|
||||
"pem": "1.14.8",
|
||||
@@ -146,7 +147,7 @@
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/js-cookie": "2.2.6",
|
||||
"@types/lodash": "^4.17.14",
|
||||
"@types/lodash": "^4.17.15",
|
||||
"@types/node": "^14.14.9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
@@ -158,7 +159,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"csstype": "^3.1.3",
|
||||
"cypress": "^13.17.0",
|
||||
"cypress-axe": "^1.5.0",
|
||||
"cypress-axe": "^1.6.0",
|
||||
"deep-freeze": "0.0.1",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-plugin-deprecation": "^1.4.1",
|
||||
@@ -167,7 +168,7 @@
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-import-newlines": "^1.3.1",
|
||||
"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-rxjs": "^5.0.3",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
@@ -182,10 +183,10 @@
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"ng-mocks": "^14.13.1",
|
||||
"ng-mocks": "^14.13.2",
|
||||
"ngx-mask": "14.2.4",
|
||||
"nodemon": "^2.0.22",
|
||||
"postcss": "^8.4",
|
||||
"postcss": "^8.5",
|
||||
"postcss-import": "^14.0.0",
|
||||
"postcss-loader": "^4.0.3",
|
||||
"postcss-preset-env": "^7.4.2",
|
||||
@@ -194,7 +195,7 @@
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "~1.83.1",
|
||||
"sass": "~1.84.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sass-resources-loader": "^2.2.5",
|
||||
"ts-node": "^8.10.2",
|
||||
|
16
server.ts
16
server.ts
@@ -81,6 +81,9 @@ let anonymousCache: LRU<string, any>;
|
||||
// extend environment with app config for server
|
||||
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.
|
||||
export function app() {
|
||||
|
||||
@@ -156,7 +159,7 @@ export function app() {
|
||||
* Proxy the sitemaps
|
||||
*/
|
||||
router.use('/sitemap**', createProxyMiddleware({
|
||||
target: `${environment.rest.baseUrl}/sitemaps`,
|
||||
target: `${REST_BASE_URL}/sitemaps`,
|
||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||
changeOrigin: true,
|
||||
}));
|
||||
@@ -165,7 +168,7 @@ export function app() {
|
||||
* Proxy the linksets
|
||||
*/
|
||||
router.use('/signposting**', createProxyMiddleware({
|
||||
target: `${environment.rest.baseUrl}`,
|
||||
target: `${REST_BASE_URL}`,
|
||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||
changeOrigin: true,
|
||||
}));
|
||||
@@ -218,7 +221,7 @@ export function app() {
|
||||
* The callback function to serve server side angular
|
||||
*/
|
||||
function ngApp(req, res, next) {
|
||||
if (environment.ssr.enabled) {
|
||||
if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || environment.ssr.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) {
|
||||
// Render the page to user via SSR (server side rendering)
|
||||
serverSideRender(req, res, next);
|
||||
} else {
|
||||
@@ -266,6 +269,11 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
|
||||
})
|
||||
.then((html) => {
|
||||
if (hasValue(html)) {
|
||||
// Replace REST URL with UI URL
|
||||
if (environment.ssr.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
|
||||
html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
|
||||
}
|
||||
|
||||
// save server side rendered page to cache (if any are enabled)
|
||||
saveToCache(req, html);
|
||||
if (sendToUser) {
|
||||
@@ -623,7 +631,7 @@ function start() {
|
||||
* The callback function to serve health check requests
|
||||
*/
|
||||
function healthCheck(req, res) {
|
||||
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
|
||||
const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
|
||||
axios.get(baseUrl)
|
||||
.then((response) => {
|
||||
res.status(response.status).send(response.data);
|
||||
|
@@ -10,7 +10,7 @@
|
||||
<button class="btn btn-outline-primary mr-3" (click)="reset()">
|
||||
{{ 'access-control-cancel' | translate }}
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!canExport()" (click)="submit()">
|
||||
<button class="btn btn-primary" [dsBtnDisabled]="!canExport()" (click)="submit()">
|
||||
{{ 'access-control-execute' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -14,6 +14,7 @@ import {
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
|
||||
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component';
|
||||
@@ -27,6 +28,7 @@ import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.com
|
||||
TranslateModule,
|
||||
BulkAccessSettingsComponent,
|
||||
BulkAccessBrowseComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -42,6 +42,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
|
||||
@@ -151,7 +152,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]),
|
||||
TranslateModule.forRoot(), EPeopleRegistryComponent],
|
||||
TranslateModule.forRoot(), EPeopleRegistryComponent, BtnDisabledDirective],
|
||||
providers: [
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
|
@@ -25,7 +25,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||
<button class="btn btn-primary" [disabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()">
|
||||
<button class="btn btn-primary" [dsBtnDisabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()">
|
||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -43,6 +43,7 @@ import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
import { FormComponent } from '../../../shared/form/form.component';
|
||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||
@@ -221,7 +222,7 @@ describe('EPersonFormComponent', () => {
|
||||
route = new ActivatedRouteStub();
|
||||
router = new RouterStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BtnDisabledDirective, BrowserModule,
|
||||
RouterModule.forRoot([]),
|
||||
TranslateModule.forRoot(),
|
||||
EPersonFormComponent,
|
||||
@@ -516,7 +517,8 @@ describe('EPersonFormComponent', () => {
|
||||
// ePersonDataServiceStub.activeEPerson = eperson;
|
||||
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204));
|
||||
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);
|
||||
fixture.detectChanges();
|
||||
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
||||
|
@@ -65,6 +65,7 @@ import {
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { Registration } from '../../../core/shared/registration.model';
|
||||
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
@@ -92,6 +93,7 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||
PaginationComponent,
|
||||
RouterLink,
|
||||
HasNoValuePipe,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -35,14 +35,14 @@
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
|
||||
*ngIf="epersonDTO.ableToDelete"
|
||||
[disabled]="actionConfig.remove.disabled"
|
||||
[dsBtnDisabled]="actionConfig.remove.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||
</button>
|
||||
<button *ngIf="!epersonDTO.ableToDelete"
|
||||
(click)="addMemberToGroup(epersonDTO.eperson)"
|
||||
[disabled]="actionConfig.add.disabled"
|
||||
[dsBtnDisabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
@@ -122,7 +122,7 @@
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="addMemberToGroup(eperson)"
|
||||
[disabled]="actionConfig.add.disabled"
|
||||
[dsBtnDisabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
|
@@ -54,6 +54,7 @@ import {
|
||||
getFirstCompletedRemoteData,
|
||||
getRemoteDataPayload,
|
||||
} from '../../../../core/shared/operators';
|
||||
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
|
||||
import { ContextHelpDirective } from '../../../../shared/context-help.directive';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||
@@ -113,6 +114,7 @@ export interface EPersonListActionConfig {
|
||||
RouterLink,
|
||||
NgClass,
|
||||
NgForOf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -69,7 +69,7 @@
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button *ngSwitchCase="false"
|
||||
[disabled]="true"
|
||||
[dsBtnDisabled]="true"
|
||||
class="btn btn-outline-primary btn-sm btn-edit"
|
||||
placement="left"
|
||||
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
|
||||
|
@@ -50,6 +50,7 @@ import { RouteService } from '../../core/services/route.service';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { NoContent } from '../../core/shared/NoContent.model';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import {
|
||||
DSONameServiceMock,
|
||||
UNDEFINED_NAME,
|
||||
@@ -208,6 +209,7 @@ describe('GroupsRegistryComponent', () => {
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot(),
|
||||
GroupsRegistryComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
providers: [GroupsRegistryComponent,
|
||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||
@@ -278,7 +280,8 @@ describe('GroupsRegistryComponent', () => {
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||
expect(editButtonsFound.length).toEqual(2);
|
||||
editButtonsFound.forEach((editButtonFound) => {
|
||||
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
||||
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -312,7 +315,8 @@ describe('GroupsRegistryComponent', () => {
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||
expect(editButtonsFound.length).toEqual(2);
|
||||
editButtonsFound.forEach((editButtonFound) => {
|
||||
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
||||
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -331,7 +335,8 @@ describe('GroupsRegistryComponent', () => {
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||
expect(editButtonsFound.length).toEqual(2);
|
||||
editButtonsFound.forEach((editButtonFound) => {
|
||||
expect(editButtonFound.nativeElement.disabled).toBeTrue();
|
||||
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -62,6 +62,7 @@ import {
|
||||
getRemoteDataPayload,
|
||||
} from '../../core/shared/operators';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
@@ -84,6 +85,7 @@ import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
NgSwitchCase,
|
||||
NgbTooltipModule,
|
||||
NgForOf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -54,7 +54,7 @@
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-light" (click)="addQueryPredicate()">+</button>
|
||||
|
||||
<button class="btn btn-light" [disabled]="deleteQueryPredicateDisabled()" (click)="deleteQueryPredicate(i)">–</button>
|
||||
<button class="btn btn-light" [dsBtnDisabled]="deleteQueryPredicateDisabled()" (click)="deleteQueryPredicate(i)">–</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,8 +158,8 @@
|
||||
{{'admin.reports.commons.page' | translate}} {{ currentPage + 1 }} {{'admin.reports.commons.of' | translate}} {{ pageCount() }}
|
||||
</div>
|
||||
<div>
|
||||
<button id="prev" class="btn btn-light" (click)="prevPage()" [disabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button>
|
||||
<button id="next" class="btn btn-light" (click)="nextPage()" [disabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button>
|
||||
<button id="prev" class="btn btn-light" (click)="prevPage()" [dsBtnDisabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button>
|
||||
<button id="next" class="btn btn-light" (click)="nextPage()" [dsBtnDisabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button>
|
||||
<!--
|
||||
<button id="export">{{'admin.reports.commons.export' | translate}}</button>
|
||||
-->
|
||||
|
@@ -43,6 +43,7 @@ import { getFirstSucceededRemoteListPayload } from 'src/app/core/shared/operator
|
||||
import { isEmpty } from 'src/app/shared/empty.util';
|
||||
import { environment } from 'src/environments/environment';
|
||||
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { FiltersComponent } from '../filters-section/filters-section.component';
|
||||
import { FilteredItems } from './filtered-items-model';
|
||||
import { OptionVO } from './option-vo.model';
|
||||
@@ -64,6 +65,7 @@ import { QueryPredicate } from './query-predicate.model';
|
||||
NgIf,
|
||||
NgForOf,
|
||||
FiltersComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -84,7 +84,7 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
||||
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
||||
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(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))),
|
||||
);
|
||||
}
|
||||
|
@@ -54,6 +54,7 @@ import {
|
||||
} from './app-routes';
|
||||
import { BROWSE_BY_DECORATOR_MAP } from './browse-by/browse-by-switcher/browse-by-decorator';
|
||||
import { AuthInterceptor } from './core/auth/auth.interceptor';
|
||||
import { DspaceRestInterceptor } from './core/dspace-rest/dspace-rest.interceptor';
|
||||
import { LocaleInterceptor } from './core/locale/locale.interceptor';
|
||||
import { LogInterceptor } from './core/log/log.interceptor';
|
||||
import {
|
||||
@@ -148,6 +149,11 @@ export const commonAppConfig: ApplicationConfig = {
|
||||
useClass: LogInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: DspaceRestInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
// register the dynamic matcher used by form. MUST be provided by the app module
|
||||
...DYNAMIC_MATCHER_PROVIDERS,
|
||||
provideCore(),
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<ng-container *ngVar="(bitstreamRD$ | async) as bitstreamRD">
|
||||
<div class="container" *ngVar="(bitstreamFormatsRD$ | async) as formatsRD">
|
||||
<div class="row" *ngIf="bitstreamRD?.hasSucceeded && formatsRD?.hasSucceeded">
|
||||
<div class="container">
|
||||
<div class="row" *ngIf="bitstreamRD?.hasSucceeded">
|
||||
<div class="col-md-2">
|
||||
<ds-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-thumbnail>
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading"
|
||||
<ds-loading *ngIf="!bitstreamRD || bitstreamRD?.isLoading"
|
||||
message="{{'loading.bitstream' | translate}}"></ds-loading>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -261,7 +261,7 @@ describe('EditBitstreamPageComponent', () => {
|
||||
});
|
||||
|
||||
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', () => {
|
||||
@@ -292,7 +292,13 @@ describe('EditBitstreamPageComponent', () => {
|
||||
|
||||
describe('when an unknown format is selected', () => {
|
||||
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', () => {
|
||||
@@ -394,9 +400,10 @@ describe('EditBitstreamPageComponent', () => {
|
||||
|
||||
describe('when selected format has changed', () => {
|
||||
beforeEach(() => {
|
||||
comp.formGroup.patchValue({
|
||||
formatContainer: {
|
||||
selectedFormat: allFormats[2].id,
|
||||
comp.onChange({
|
||||
model: {
|
||||
id: 'selectedFormat',
|
||||
value: allFormats[2],
|
||||
},
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
@@ -21,7 +21,6 @@ import {
|
||||
DynamicFormLayout,
|
||||
DynamicFormService,
|
||||
DynamicInputModel,
|
||||
DynamicSelectModel,
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import {
|
||||
TranslateModule,
|
||||
@@ -39,23 +38,24 @@ import {
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { FindAllDataImpl } from '../../core/data/base/find-all-data';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||
import { BITSTREAM_FORMAT } from '../../core/shared/bitstream-format.resource-type';
|
||||
import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level';
|
||||
import { Bundle } from '../../core/shared/bundle.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { Metadata } from '../../core/shared/metadata.utils';
|
||||
import {
|
||||
getAllSucceededRemoteDataPayload,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
@@ -72,6 +72,7 @@ import { ErrorComponent } from '../../shared/error/error.component';
|
||||
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.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 { DynamicScrollableDropdownModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||
import { FormComponent } from '../../shared/form/form.component';
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
@@ -109,12 +110,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -130,11 +125,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
originalFormat: BitstreamFormat;
|
||||
|
||||
/**
|
||||
* A list of all available bitstream formats
|
||||
*/
|
||||
formats: BitstreamFormat[];
|
||||
|
||||
/**
|
||||
* @type {string} Key prefix used to generate form messages
|
||||
*/
|
||||
@@ -178,7 +168,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Options for fetching all bitstream formats
|
||||
*/
|
||||
findAllOptions = { elementsPerPage: 9999 };
|
||||
findAllOptions = {
|
||||
elementsPerPage: 20,
|
||||
currentPage: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* The Dynamic Input Model for the file's name
|
||||
@@ -218,9 +211,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The Dynamic Input Model for the selected format
|
||||
*/
|
||||
selectedFormatModel = new DynamicSelectModel({
|
||||
selectedFormatModel = new DynamicScrollableDropdownModel({
|
||||
id: 'selectedFormat',
|
||||
name: 'selectedFormat',
|
||||
displayKey: 'shortDescription',
|
||||
repeatable: false,
|
||||
metadataFields: [],
|
||||
submissionId: '',
|
||||
hasSelectableMetadata: false,
|
||||
resourceType: BITSTREAM_FORMAT,
|
||||
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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -438,6 +444,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
* @private
|
||||
*/
|
||||
private bundle: Bundle;
|
||||
/**
|
||||
* The currently selected format
|
||||
* @private
|
||||
*/
|
||||
private selectedFormat: BitstreamFormat;
|
||||
|
||||
constructor(private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
@@ -463,18 +474,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
this.itemId = this.route.snapshot.queryParams.itemId;
|
||||
this.entityType = this.route.snapshot.queryParams.entityType;
|
||||
this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream));
|
||||
this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions);
|
||||
|
||||
const bitstream$ = this.bitstreamRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
|
||||
const allFormats$ = this.bitstreamFormatsRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
|
||||
const bundle$ = bitstream$.pipe(
|
||||
switchMap((bitstream: Bitstream) => bitstream.bundle),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
@@ -490,24 +495,31 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
switchMap((bundle: Bundle) => bundle.item),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
const format$ = bitstream$.pipe(
|
||||
switchMap(bitstream => bitstream.format),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
bitstream$,
|
||||
allFormats$,
|
||||
bundle$,
|
||||
primaryBitstream$,
|
||||
item$,
|
||||
).pipe()
|
||||
.subscribe(([bitstream, allFormats, bundle, primaryBitstream, item]) => {
|
||||
this.bitstream = bitstream as Bitstream;
|
||||
this.formats = allFormats.page;
|
||||
this.bundle = bundle;
|
||||
// hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
|
||||
// be a success response, but empty
|
||||
this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
|
||||
this.itemId = item.uuid;
|
||||
this.setIiifStatus(this.bitstream);
|
||||
}),
|
||||
format$,
|
||||
).subscribe(([bitstream, bundle, primaryBitstream, item, format]) => {
|
||||
this.bitstream = bitstream as Bitstream;
|
||||
this.bundle = bundle;
|
||||
this.selectedFormat = format;
|
||||
// hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
|
||||
// be a success response, but empty
|
||||
this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
|
||||
this.itemId = item.uuid;
|
||||
this.setIiifStatus(this.bitstream);
|
||||
}),
|
||||
format$.pipe(take(1)).subscribe(
|
||||
(format) => this.originalFormat = format,
|
||||
),
|
||||
);
|
||||
|
||||
this.subs.push(
|
||||
@@ -523,7 +535,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
setForm() {
|
||||
this.formGroup = this.formService.createFormGroup(this.formModel);
|
||||
this.updateFormatModel();
|
||||
this.updateForm(this.bitstream);
|
||||
this.updateFieldTranslations();
|
||||
}
|
||||
@@ -542,6 +553,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
description: bitstream.firstMetadataValue('dc.description'),
|
||||
},
|
||||
formatContainer: {
|
||||
selectedFormat: this.selectedFormat.shortDescription,
|
||||
newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined,
|
||||
},
|
||||
});
|
||||
@@ -561,36 +573,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
});
|
||||
}
|
||||
this.bitstream.format.pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
).subscribe((format: BitstreamFormat) => {
|
||||
this.originalFormat = format;
|
||||
this.formGroup.patchValue({
|
||||
formatContainer: {
|
||||
selectedFormat: format.id,
|
||||
},
|
||||
});
|
||||
this.updateNewFormatLayout(format.id);
|
||||
});
|
||||
this.updateNewFormatLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param selectedId
|
||||
*/
|
||||
updateNewFormatLayout(selectedId: string) {
|
||||
if (this.isUnknownFormat(selectedId)) {
|
||||
updateNewFormatLayout() {
|
||||
if (this.isUnknownFormat()) {
|
||||
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout;
|
||||
} else {
|
||||
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible';
|
||||
@@ -601,9 +593,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
* Is the provided format (id) part of the list of unknown formats?
|
||||
* @param id
|
||||
*/
|
||||
isUnknownFormat(id: string): boolean {
|
||||
const format = this.formats.find((f: BitstreamFormat) => f.id === id);
|
||||
return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown;
|
||||
isUnknownFormat(): boolean {
|
||||
return hasValue(this.selectedFormat) && this.selectedFormat.supportLevel === BitstreamFormatSupportLevel.Unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -635,7 +626,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
onChange(event) {
|
||||
const model = event.model;
|
||||
if (model.id === this.selectedFormatModel.id) {
|
||||
this.updateNewFormatLayout(model.value);
|
||||
this.selectedFormat = model.value;
|
||||
this.updateNewFormatLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,8 +637,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
onSubmit() {
|
||||
const updatedValues = this.formGroup.getRawValue();
|
||||
const updatedBitstream = this.formToBitstream(updatedValues);
|
||||
const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat);
|
||||
const isNewFormat = selectedFormat.id !== this.originalFormat.id;
|
||||
const isNewFormat = this.selectedFormat.id !== this.originalFormat.id;
|
||||
const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream;
|
||||
const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid;
|
||||
|
||||
@@ -698,7 +689,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
bundle$ = observableOf(this.bundle);
|
||||
}
|
||||
if (isNewFormat) {
|
||||
bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe(
|
||||
bitstream$ = this.bitstreamService.updateFormat(this.bitstream, this.selectedFormat).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((formatResponse: RemoteData<Bitstream>) => {
|
||||
if (hasValue(formatResponse) && formatResponse.hasFailed) {
|
||||
@@ -856,4 +847,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
.forEach((subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
findAllFormatsServiceFactory() {
|
||||
return () => this.bitstreamFormatService as any as FindAllDataImpl<BitstreamFormat>;
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,13 @@ import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
NO_ERRORS_SCHEMA,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
fakeAsync,
|
||||
TestBed,
|
||||
tick,
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import {
|
||||
@@ -26,6 +29,7 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-
|
||||
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component';
|
||||
@@ -123,6 +127,7 @@ describe('BrowseByDateComponent', () => {
|
||||
{ provide: ChangeDetectorRef, useValue: mockCdRef },
|
||||
{ provide: Store, useValue: {} },
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
})
|
||||
@@ -172,4 +177,33 @@ describe('BrowseByDateComponent', () => {
|
||||
//expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
|
||||
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,5 +1,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
isPlatformServer,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
Component,
|
||||
Inject,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
@@ -17,10 +19,11 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
|
||||
|
||||
@@ -28,6 +31,7 @@ import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../config/app-config.interface';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import {
|
||||
@@ -38,13 +42,7 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
|
||||
import { ThemedComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/themed-comcol-page-content.component';
|
||||
import { ThemedComcolPageHandleComponent } from '../../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
|
||||
import { ComcolPageHeaderComponent } from '../../shared/comcol/comcol-page-header/comcol-page-header.component';
|
||||
import { ComcolPageLogoComponent } from '../../shared/comcol/comcol-page-logo/comcol-page-logo.component';
|
||||
import { isValidDate } from '../../shared/date.util';
|
||||
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
@@ -52,7 +50,6 @@ import {
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { StartsWithType } from '../../shared/starts-with/starts-with-type';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import {
|
||||
BrowseByMetadataComponent,
|
||||
browseParamsToOptions,
|
||||
@@ -64,15 +61,8 @@ import {
|
||||
templateUrl: '../browse-by-metadata/browse-by-metadata.component.html',
|
||||
standalone: true,
|
||||
imports: [
|
||||
VarDirective,
|
||||
AsyncPipe,
|
||||
ComcolPageHeaderComponent,
|
||||
ComcolPageLogoComponent,
|
||||
NgIf,
|
||||
ThemedComcolPageHandleComponent,
|
||||
ThemedComcolPageContentComponent,
|
||||
DsoEditMenuComponent,
|
||||
ThemedComcolPageBrowseByComponent,
|
||||
TranslateModule,
|
||||
ThemedLoadingComponent,
|
||||
ThemedBrowseByComponent,
|
||||
@@ -99,27 +89,34 @@ export class BrowseByDateComponent extends BrowseByMetadataComponent implements
|
||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||
public dsoNameService: DSONameService,
|
||||
protected cdRef: ChangeDetectorRef,
|
||||
@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 {
|
||||
if (!this.renderOnServerSide && !environment.ssr.enableBrowseComponent && isPlatformServer(this.platformId)) {
|
||||
this.loading$ = observableOf(false);
|
||||
return;
|
||||
}
|
||||
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
||||
this.startsWithType = StartsWithType.date;
|
||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||
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(
|
||||
observableCombineLatest(
|
||||
[ this.route.params.pipe(take(1)),
|
||||
this.route.queryParams,
|
||||
this.scope$,
|
||||
this.currentPagination$,
|
||||
this.currentSort$,
|
||||
]).pipe(
|
||||
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
|
||||
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
|
||||
}),
|
||||
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
|
||||
observableCombineLatest([
|
||||
routeParams$,
|
||||
this.scope$,
|
||||
this.currentPagination$,
|
||||
this.currentSort$,
|
||||
]).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
|
||||
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<section class="comcol-page-browse-section">
|
||||
<section class="comcol-page-browse-section" *ngIf="(!ssrRenderingDisabled)">
|
||||
<div class="browse-by-metadata w-100">
|
||||
<ds-browse-by *ngIf="(loading$ | async) !== true" class="col-xs-12 w-100"
|
||||
title="{{'browse.title' | translate:{
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
NO_ERRORS_SCHEMA,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
fakeAsync,
|
||||
TestBed,
|
||||
tick,
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
@@ -147,6 +152,7 @@ describe('BrowseByMetadataComponent', () => {
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
{ provide: SelectableListService, useValue: {} },
|
||||
{ provide: HostWindowService, useValue: {} },
|
||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
})
|
||||
@@ -259,6 +265,35 @@ describe('BrowseByMetadataComponent', () => {
|
||||
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>>> {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
isPlatformServer,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
@@ -24,8 +27,8 @@ import {
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
|
||||
|
||||
@@ -33,6 +36,7 @@ import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../config/app-config.interface';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||
@@ -48,12 +52,6 @@ import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||
import { Context } from '../../core/shared/context.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getFirstSucceededRemoteData } from '../../core/shared/operators';
|
||||
import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
|
||||
import { ThemedComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/themed-comcol-page-content.component';
|
||||
import { ThemedComcolPageHandleComponent } from '../../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
|
||||
import { ComcolPageHeaderComponent } from '../../shared/comcol/comcol-page-header/comcol-page-header.component';
|
||||
import { ComcolPageLogoComponent } from '../../shared/comcol/comcol-page-logo/comcol-page-logo.component';
|
||||
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
@@ -61,7 +59,6 @@ import {
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { StartsWithType } from '../../shared/starts-with/starts-with-type';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type';
|
||||
|
||||
export const BBM_PAGINATION_ID = 'bbm';
|
||||
@@ -71,15 +68,8 @@ export const BBM_PAGINATION_ID = 'bbm';
|
||||
styleUrls: ['./browse-by-metadata.component.scss'],
|
||||
templateUrl: './browse-by-metadata.component.html',
|
||||
imports: [
|
||||
VarDirective,
|
||||
AsyncPipe,
|
||||
ComcolPageHeaderComponent,
|
||||
ComcolPageLogoComponent,
|
||||
NgIf,
|
||||
ThemedComcolPageHandleComponent,
|
||||
ThemedComcolPageContentComponent,
|
||||
DsoEditMenuComponent,
|
||||
ThemedComcolPageBrowseByComponent,
|
||||
TranslateModule,
|
||||
ThemedLoadingComponent,
|
||||
ThemedBrowseByComponent,
|
||||
@@ -114,6 +104,11 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
||||
*/
|
||||
@Input() displayTitle = true;
|
||||
|
||||
/**
|
||||
* Defines whether to fetch search results during SSR execution
|
||||
*/
|
||||
@Input() renderOnServerSide: boolean;
|
||||
|
||||
scope$: BehaviorSubject<string> = new BehaviorSubject(undefined);
|
||||
|
||||
/**
|
||||
@@ -194,6 +189,10 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
||||
* Observable determining if the loading animation needs to be shown
|
||||
*/
|
||||
loading$ = observableOf(true);
|
||||
/**
|
||||
* Whether this component should be rendered or not in SSR
|
||||
*/
|
||||
ssrRenderingDisabled = false;
|
||||
|
||||
public constructor(protected route: ActivatedRoute,
|
||||
protected browseService: BrowseService,
|
||||
@@ -202,6 +201,7 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
||||
protected router: Router,
|
||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||
public dsoNameService: DSONameService,
|
||||
@Inject(PLATFORM_ID) public platformId: any,
|
||||
) {
|
||||
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||
@@ -209,27 +209,32 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
||||
currentPage: 1,
|
||||
pageSize: this.appConfig.browseBy.pageSize,
|
||||
});
|
||||
this.ssrRenderingDisabled = !this.renderOnServerSide && !environment.ssr.enableBrowseComponent && isPlatformServer(this.platformId);
|
||||
}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
if (this.ssrRenderingDisabled) {
|
||||
this.loading$ = observableOf(false);
|
||||
return;
|
||||
}
|
||||
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.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.authority === curr.authority && prev.value === curr.value && prev.startsWith === curr.startsWith),
|
||||
);
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
[ this.route.params.pipe(take(1)),
|
||||
this.route.queryParams,
|
||||
this.scope$,
|
||||
this.currentPagination$,
|
||||
this.currentSort$,
|
||||
]).pipe(
|
||||
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
|
||||
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
|
||||
}),
|
||||
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
|
||||
observableCombineLatest([
|
||||
routeParams$,
|
||||
this.scope$,
|
||||
this.currentPagination$,
|
||||
this.currentSort$,
|
||||
]).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.authority = params.authority;
|
||||
|
||||
@@ -257,8 +262,10 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.scope$.next(this.scope);
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (hasValue(changes.scope)) {
|
||||
this.scope$.next(this.scope);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,7 +343,6 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.paginationService.clearPagination(this.paginationConfig.id);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -5,7 +5,9 @@ import {
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
fakeAsync,
|
||||
TestBed,
|
||||
tick,
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import {
|
||||
@@ -23,6 +25,7 @@ import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component';
|
||||
@@ -81,6 +84,7 @@ describe('BrowseByTitleComponent', () => {
|
||||
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({}),
|
||||
queryParams: observableOf({}),
|
||||
data: observableOf({ metadata: 'title' }),
|
||||
});
|
||||
|
||||
@@ -127,4 +131,35 @@ describe('BrowseByTitleComponent', () => {
|
||||
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,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
isPlatformServer,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
@@ -8,26 +9,24 @@ import {
|
||||
} from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
import {
|
||||
SortDirection,
|
||||
SortOptions,
|
||||
} from '../../core/cache/models/sort-options.model';
|
||||
import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component';
|
||||
import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
|
||||
import { ThemedComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/themed-comcol-page-content.component';
|
||||
import { ThemedComcolPageHandleComponent } from '../../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
|
||||
import { ComcolPageHeaderComponent } from '../../shared/comcol/comcol-page-header/comcol-page-header.component';
|
||||
import { ComcolPageLogoComponent } from '../../shared/comcol/comcol-page-logo/comcol-page-logo.component';
|
||||
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import {
|
||||
BrowseByMetadataComponent,
|
||||
browseParamsToOptions,
|
||||
@@ -39,15 +38,8 @@ import {
|
||||
templateUrl: '../browse-by-metadata/browse-by-metadata.component.html',
|
||||
standalone: true,
|
||||
imports: [
|
||||
VarDirective,
|
||||
AsyncPipe,
|
||||
ComcolPageHeaderComponent,
|
||||
ComcolPageLogoComponent,
|
||||
NgIf,
|
||||
ThemedComcolPageHandleComponent,
|
||||
ThemedComcolPageContentComponent,
|
||||
DsoEditMenuComponent,
|
||||
ThemedComcolPageBrowseByComponent,
|
||||
TranslateModule,
|
||||
ThemedLoadingComponent,
|
||||
ThemedBrowseByComponent,
|
||||
@@ -59,21 +51,27 @@ import {
|
||||
export class BrowseByTitleComponent extends BrowseByMetadataComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.renderOnServerSide && !environment.ssr.enableBrowseComponent && isPlatformServer(this.platformId)) {
|
||||
this.loading$ = observableOf(false);
|
||||
return;
|
||||
}
|
||||
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||
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(
|
||||
observableCombineLatest(
|
||||
[ this.route.params.pipe(take(1)),
|
||||
this.route.queryParams,
|
||||
this.scope$,
|
||||
this.currentPagination$,
|
||||
this.currentSort$,
|
||||
]).pipe(
|
||||
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
|
||||
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
|
||||
}),
|
||||
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
|
||||
observableCombineLatest([
|
||||
routeParams$,
|
||||
this.scope$,
|
||||
this.currentPagination$,
|
||||
this.currentSort$,
|
||||
]).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.updatePageWithItems(browseParamsToOptions(params, scope, 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>
|
||||
<div class="form-group row">
|
||||
<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}}
|
||||
</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) !== true"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span>
|
||||
</button>
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
@@ -31,6 +32,7 @@ import { VarDirective } from '../../shared/utils/var.directive';
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
VarDirective,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -19,32 +19,32 @@
|
||||
</div>
|
||||
|
||||
<button *ngIf="(testConfigRunning$ |async) !== true" class="btn btn-secondary"
|
||||
[disabled]="!(isEnabled)"
|
||||
[dsBtnDisabled]="!(isEnabled)"
|
||||
(click)="testConfiguration(contentSource)">
|
||||
<span>{{'collection.source.controls.test.submit' | translate}}</span>
|
||||
</button>
|
||||
<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>{{'collection.source.controls.test.running' | translate}}</span>
|
||||
</button>
|
||||
<button *ngIf="(importRunning$ |async) !== true" class="btn btn-primary"
|
||||
[disabled]="!(isEnabled)"
|
||||
[dsBtnDisabled]="!(isEnabled)"
|
||||
(click)="importNow()">
|
||||
<span class="d-none d-sm-inline">{{'collection.source.controls.import.submit' | translate}}</span>
|
||||
</button>
|
||||
<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="d-none d-sm-inline">{{'collection.source.controls.import.running' | translate}}</span>
|
||||
</button>
|
||||
<button *ngIf="(reImportRunning$ |async) !== true" class="btn btn-primary"
|
||||
[disabled]="!(isEnabled)"
|
||||
[dsBtnDisabled]="!(isEnabled)"
|
||||
(click)="resetAndReimport()">
|
||||
<span class="d-none d-sm-inline"> {{'collection.source.controls.reset.submit' | translate}}</span>
|
||||
</button>
|
||||
<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="d-none d-sm-inline"> {{'collection.source.controls.reset.running' | translate}}</span>
|
||||
</button>
|
||||
|
@@ -22,6 +22,7 @@ import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { ContentSource } from '../../../../core/shared/content-source.model';
|
||||
import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer';
|
||||
import { Process } from '../../../../process-page/processes/process.model';
|
||||
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
@@ -104,7 +105,7 @@ describe('CollectionSourceControlsComponent', () => {
|
||||
requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring', 'setStaleByHrefSubstring']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule, CollectionSourceControlsComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule, CollectionSourceControlsComponent, VarDirective, BtnDisabledDirective],
|
||||
providers: [
|
||||
{ provide: ScriptDataService, useValue: scriptDataService },
|
||||
{ provide: ProcessDataService, useValue: processDataService },
|
||||
@@ -193,9 +194,10 @@ describe('CollectionSourceControlsComponent', () => {
|
||||
|
||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
|
||||
expect(buttons[0].nativeElement.disabled).toBeTrue();
|
||||
expect(buttons[1].nativeElement.disabled).toBeTrue();
|
||||
expect(buttons[2].nativeElement.disabled).toBeTrue();
|
||||
buttons.forEach(button => {
|
||||
expect(button.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||
expect(button.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||
});
|
||||
});
|
||||
it('should be enabled when isEnabled is true', () => {
|
||||
comp.shouldShow = true;
|
||||
@@ -205,9 +207,10 @@ describe('CollectionSourceControlsComponent', () => {
|
||||
|
||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
|
||||
expect(buttons[0].nativeElement.disabled).toBeFalse();
|
||||
expect(buttons[1].nativeElement.disabled).toBeFalse();
|
||||
expect(buttons[2].nativeElement.disabled).toBeFalse();
|
||||
buttons.forEach(button => {
|
||||
expect(button.nativeElement.getAttribute('aria-disabled')).toBe('false');
|
||||
expect(button.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('should call the corresponding button when clicked', () => {
|
||||
spyOn(comp, 'testConfiguration');
|
||||
|
@@ -40,6 +40,7 @@ import {
|
||||
} from '../../../../core/shared/operators';
|
||||
import { Process } from '../../../../process-page/processes/process.model';
|
||||
import { ProcessStatus } from '../../../../process-page/processes/process-status.model';
|
||||
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
|
||||
import { hasValue } from '../../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
||||
@@ -56,6 +57,7 @@ import { VarDirective } from '../../../../shared/utils/var.directive';
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
VarDirective,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="container-fluid">
|
||||
<div class="d-inline-block float-right space-children-mr">
|
||||
<button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||
[disabled]="(hasChanges$ | async) !== true"
|
||||
[dsBtnDisabled]="(hasChanges$ | async) !== true"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<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>
|
||||
</button>
|
||||
<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
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="col-12">
|
||||
<div class="d-inline-block float-right ml-1 space-children-mr">
|
||||
<button class=" btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||
[disabled]="(hasChanges$ | async) !== true"
|
||||
[dsBtnDisabled]="(hasChanges$ | async) !== true"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<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>
|
||||
</button>
|
||||
<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
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
|
@@ -56,6 +56,7 @@ import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
} from '../../../core/shared/operators';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
@@ -81,6 +82,7 @@ import { CollectionSourceControlsComponent } from './collection-source-controls/
|
||||
ThemedLoadingComponent,
|
||||
FormComponent,
|
||||
CollectionSourceControlsComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -6,10 +6,10 @@
|
||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
|
||||
<div class="form-group row">
|
||||
<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}}
|
||||
</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) !== true"><i class="fas fa-trash" aria-hidden="true"></i> {{'community.delete.confirm' | translate}}</span>
|
||||
</button>
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
@@ -31,6 +32,7 @@ import { VarDirective } from '../../shared/utils/var.directive';
|
||||
AsyncPipe,
|
||||
VarDirective,
|
||||
NgIf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -129,12 +129,24 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
*/
|
||||
private sortAuthMethods(authMethodModels: AuthMethod[]): AuthMethod[] {
|
||||
const sortedAuthMethodModels: AuthMethod[] = [];
|
||||
let passwordAuthFound = false;
|
||||
let ldapAuthFound = false;
|
||||
|
||||
authMethodModels.forEach((method) => {
|
||||
if (method.authMethodType === AuthMethodType.Password) {
|
||||
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) => {
|
||||
if (method.authMethodType !== AuthMethodType.Password) {
|
||||
sortedAuthMethodModels.push(method);
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
from as observableFrom,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
shareReplay,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
@@ -283,6 +284,10 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
||||
isNotEmptyOperator(),
|
||||
take(1),
|
||||
map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)),
|
||||
shareReplay({
|
||||
bufferSize: 1,
|
||||
refCount: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const startTime: number = new Date().getTime();
|
||||
@@ -337,6 +342,10 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
||||
isNotEmptyOperator(),
|
||||
take(1),
|
||||
map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)),
|
||||
shareReplay({
|
||||
bufferSize: 1,
|
||||
refCount: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const startTime: number = new Date().getTime();
|
||||
|
@@ -16,6 +16,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||
@@ -176,4 +177,30 @@ describe('BitstreamDataService', () => {
|
||||
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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -241,11 +241,12 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
|
||||
* 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 constains primary bitstream information or null
|
||||
*/
|
||||
public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<Bitstream | null> {
|
||||
return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe(
|
||||
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) {
|
||||
|
@@ -78,10 +78,14 @@ export class BundleDataService extends IdentifiableDataService<Bundle> implement
|
||||
* requested after the response becomes stale
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||
* {@link HALLink}s should be automatically resolved
|
||||
* @param options the {@link FindListOptions} for the request
|
||||
*/
|
||||
// TODO should be implemented rest side
|
||||
findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> {
|
||||
return this.findAllByItem(item, { elementsPerPage: 9999 }, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
|
||||
findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> {
|
||||
//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>>) => {
|
||||
if (hasValue(rd.payload) && hasValue(rd.payload.page)) {
|
||||
const matchingBundle = rd.payload.page.find((bundle: Bundle) =>
|
||||
|
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);
|
||||
}
|
||||
}
|
@@ -50,6 +50,7 @@ import { coreSelector } from '../core.selectors';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { BundleDataService } from '../data/bundle-data.service';
|
||||
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
||||
import { FindListOptions } from '../data/find-list-options.model';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
@@ -331,6 +332,7 @@ export class HeadTagService {
|
||||
'ORIGINAL',
|
||||
true,
|
||||
true,
|
||||
new FindListOptions(),
|
||||
followLink('primaryBitstream'),
|
||||
followLink('bitstreams', {
|
||||
findListOptions: {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { environment } from '../../../environments/environment.test';
|
||||
import { ServerHardRedirectService } from './server-hard-redirect.service';
|
||||
|
||||
describe('ServerHardRedirectService', () => {
|
||||
@@ -7,7 +8,7 @@ describe('ServerHardRedirectService', () => {
|
||||
const mockRequest = jasmine.createSpyObj(['get']);
|
||||
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';
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -68,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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -7,10 +7,15 @@ import {
|
||||
Response,
|
||||
} from 'express';
|
||||
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../config/app-config.interface';
|
||||
import {
|
||||
REQUEST,
|
||||
RESPONSE,
|
||||
} from '../../../express.tokens';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { HardRedirectService } from './hard-redirect.service';
|
||||
|
||||
/**
|
||||
@@ -20,6 +25,7 @@ import { HardRedirectService } from './hard-redirect.service';
|
||||
export class ServerHardRedirectService extends HardRedirectService {
|
||||
|
||||
constructor(
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
@Inject(REQUEST) protected req: Request,
|
||||
@Inject(RESPONSE) protected res: Response,
|
||||
) {
|
||||
@@ -35,17 +41,22 @@ export class ServerHardRedirectService extends HardRedirectService {
|
||||
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
|
||||
*/
|
||||
redirect(url: string, statusCode?: number) {
|
||||
|
||||
if (url === this.req.url) {
|
||||
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) {
|
||||
const req: any = this.req;
|
||||
req._r_count = (req._r_count || 0) + 1;
|
||||
|
||||
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) {
|
||||
console.error('Detected a redirection loop. killing the nodejs process');
|
||||
@@ -59,9 +70,9 @@ export class ServerHardRedirectService extends HardRedirectService {
|
||||
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();
|
||||
// I haven't found a way to correctly stop Angular rendering.
|
||||
// So we just let it end its work, though we have already closed
|
||||
|
@@ -77,29 +77,29 @@
|
||||
<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"
|
||||
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>
|
||||
</button>
|
||||
<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"
|
||||
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>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" data-test="metadata-remove-btn"
|
||||
[title]="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>
|
||||
</button>
|
||||
<button class="btn btn-outline-warning btn-sm" data-test="metadata-undo-btn"
|
||||
[title]="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>
|
||||
</button>
|
||||
<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"
|
||||
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}">
|
||||
<i class="fas fa-grip-vertical fa-fw"></i>
|
||||
|
@@ -34,6 +34,7 @@ import {
|
||||
VIRTUAL_METADATA_PREFIX,
|
||||
} from '../../../core/shared/metadata.models';
|
||||
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
|
||||
import { DsDynamicScrollableDropdownComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
|
||||
import { ThemedTypeBadgeComponent } from '../../../shared/object-collection/shared/badges/type-badge/themed-type-badge.component';
|
||||
@@ -188,6 +189,7 @@ describe('DsoEditMetadataValueComponent', () => {
|
||||
RouterTestingModule.withRoutes([]),
|
||||
DsoEditMetadataValueComponent,
|
||||
VarDirective,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RelationshipDataService, useValue: relationshipService },
|
||||
@@ -524,7 +526,14 @@ describe('DsoEditMetadataValueComponent', () => {
|
||||
});
|
||||
|
||||
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 {
|
||||
it('should not exist', () => {
|
||||
|
@@ -67,6 +67,7 @@ import {
|
||||
import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model';
|
||||
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { isNotEmpty } from '../../../shared/empty.util';
|
||||
import { DsDynamicOneboxComponent } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
|
||||
import {
|
||||
@@ -94,7 +95,7 @@ import {
|
||||
styleUrls: ['./dso-edit-metadata-value.component.scss', '../dso-edit-metadata-shared/dso-edit-metadata-cells.scss'],
|
||||
templateUrl: './dso-edit-metadata-value.component.html',
|
||||
standalone: true,
|
||||
imports: [VarDirective, CdkDrag, NgClass, NgIf, FormsModule, DebounceDirective, RouterLink, ThemedTypeBadgeComponent, NgbTooltipModule, CdkDragHandle, AsyncPipe, TranslateModule, DsDynamicScrollableDropdownComponent, DsDynamicOneboxComponent, AuthorityConfidenceStateDirective],
|
||||
imports: [VarDirective, CdkDrag, NgClass, NgIf, FormsModule, DebounceDirective, RouterLink, ThemedTypeBadgeComponent, NgbTooltipModule, CdkDragHandle, AsyncPipe, TranslateModule, DsDynamicScrollableDropdownComponent, DsDynamicOneboxComponent, AuthorityConfidenceStateDirective, BtnDisabledDirective],
|
||||
})
|
||||
/**
|
||||
* Component displaying a single editable row for a metadata value
|
||||
|
@@ -1,18 +1,18 @@
|
||||
<div class="item-metadata" *ngIf="form">
|
||||
<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"
|
||||
[title]="dsoType + '.edit.metadata.add-button' | translate"
|
||||
(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>
|
||||
</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"
|
||||
[title]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
||||
(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>
|
||||
</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"
|
||||
[title]="dsoType + '.edit.metadata.save-button' | translate"
|
||||
(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"
|
||||
[attr.aria-label]="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>
|
||||
<span class="d-none d-sm-inline"> {{ dsoType + '.edit.metadata.discard-button' | translate }}</span>
|
||||
</button>
|
||||
@@ -77,13 +77,13 @@
|
||||
</div>
|
||||
<div class="button-row bottom d-inline-block w-100">
|
||||
<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"
|
||||
[title]="dsoType + '.edit.metadata.reinstate-button' | translate"
|
||||
(click)="reinstate()">
|
||||
<i class="fas fa-undo-alt" aria-hidden="true"></i> {{ dsoType + '.edit.metadata.reinstate-button' | translate }}
|
||||
</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"
|
||||
[title]="dsoType + '.edit.metadata.save-button' | translate"
|
||||
(click)="submit()">
|
||||
@@ -92,7 +92,7 @@
|
||||
<button class="btn btn-danger" *ngIf="!isReinstatable"
|
||||
[attr.aria-label]="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> {{ dsoType + '.edit.metadata.discard-button' | translate }}
|
||||
</button>
|
||||
|
@@ -22,6 +22,7 @@ import { Item } from '../../core/shared/item.model';
|
||||
import { ITEM } from '../../core/shared/item.resource-type';
|
||||
import { MetadataValue } from '../../core/shared/metadata.models';
|
||||
import { AlertComponent } from '../../shared/alert/alert.component';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TestDataService } from '../../shared/testing/test-data-service.mock';
|
||||
@@ -94,6 +95,7 @@ describe('DsoEditMetadataComponent', () => {
|
||||
RouterTestingModule.withRoutes([]),
|
||||
DsoEditMetadataComponent,
|
||||
VarDirective,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_DATA_SERVICES_MAP, useValue: mockDataServiceMap },
|
||||
@@ -216,7 +218,13 @@ describe('DsoEditMetadataComponent', () => {
|
||||
});
|
||||
|
||||
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 {
|
||||
it('should not exist', () => {
|
||||
|
@@ -47,6 +47,7 @@ import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { ResourceType } from '../../core/shared/resource-type';
|
||||
import { AlertComponent } from '../../shared/alert/alert.component';
|
||||
import { AlertType } from '../../shared/alert/alert-type';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
@@ -66,7 +67,7 @@ import { MetadataFieldSelectorComponent } from './metadata-field-selector/metada
|
||||
styleUrls: ['./dso-edit-metadata.component.scss'],
|
||||
templateUrl: './dso-edit-metadata.component.html',
|
||||
standalone: true,
|
||||
imports: [NgIf, DsoEditMetadataHeadersComponent, MetadataFieldSelectorComponent, DsoEditMetadataValueHeadersComponent, DsoEditMetadataValueComponent, NgFor, DsoEditMetadataFieldValuesComponent, AlertComponent, ThemedLoadingComponent, AsyncPipe, TranslateModule],
|
||||
imports: [NgIf, DsoEditMetadataHeadersComponent, MetadataFieldSelectorComponent, DsoEditMetadataValueHeadersComponent, DsoEditMetadataValueComponent, NgFor, DsoEditMetadataFieldValuesComponent, AlertComponent, ThemedLoadingComponent, AsyncPipe, TranslateModule, BtnDisabledDirective],
|
||||
})
|
||||
/**
|
||||
* Component showing a table of all metadata on a DSpaceObject and options to modify them
|
||||
|
@@ -12,4 +12,9 @@
|
||||
<a [routerLink]="[itemPageRoute]"
|
||||
[innerHTML]="mdRepresentation.getValue()"
|
||||
[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>
|
||||
|
@@ -7,13 +7,14 @@ import { RouterLink } from '@angular/router';
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
|
||||
import { OrcidBadgeAndTooltipComponent } from '../../../../shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component';
|
||||
import { TruncatableComponent } from '../../../../shared/truncatable/truncatable.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-person-item-metadata-list-element',
|
||||
templateUrl: './person-item-metadata-list-element.component.html',
|
||||
standalone: true,
|
||||
imports: [NgIf, NgFor, TruncatableComponent, RouterLink, NgbTooltipModule],
|
||||
imports: [NgIf, NgFor, TruncatableComponent, RouterLink, NgbTooltipModule, OrcidBadgeAndTooltipComponent],
|
||||
})
|
||||
/**
|
||||
* The component for displaying an item of the type Person as a metadata field
|
||||
|
@@ -28,7 +28,7 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<button
|
||||
[disabled]="isInValid"
|
||||
[dsBtnDisabled]="isInValid"
|
||||
class="btn btn-default btn-primary"
|
||||
(click)="submit()">{{'forgot-password.form.submit' | translate}}</button>
|
||||
</div>
|
||||
|
@@ -29,6 +29,7 @@ import {
|
||||
} from '../../core/shared/operators';
|
||||
import { Registration } from '../../core/shared/registration.model';
|
||||
import { ProfilePageSecurityFormComponent } from '../../profile-page/profile-page-security-form/profile-page-security-form.component';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
|
||||
|
||||
@@ -42,6 +43,7 @@ import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
|
||||
ProfilePageSecurityFormComponent,
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -7,7 +7,7 @@
|
||||
|
||||
<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-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>
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs';
|
||||
import { LogOutAction } from '../../core/auth/auth.actions';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||
import { EndUserAgreementComponent } from './end-user-agreement.component';
|
||||
@@ -57,7 +58,7 @@ describe('EndUserAgreementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), EndUserAgreementComponent],
|
||||
imports: [TranslateModule.forRoot(), EndUserAgreementComponent, BtnDisabledDirective],
|
||||
providers: [
|
||||
{ provide: EndUserAgreementService, useValue: endUserAgreementService },
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
@@ -95,7 +96,8 @@ describe('EndUserAgreementComponent', () => {
|
||||
|
||||
it('should disable the save button', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -23,6 +23,7 @@ import { AppState } from '../../app.reducer';
|
||||
import { LogOutAction } from '../../core/auth/auth.actions';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { EndUserAgreementContentComponent } from './end-user-agreement-content/end-user-agreement-content.component';
|
||||
@@ -32,7 +33,7 @@ import { EndUserAgreementContentComponent } from './end-user-agreement-content/e
|
||||
templateUrl: './end-user-agreement.component.html',
|
||||
styleUrls: ['./end-user-agreement.component.scss'],
|
||||
standalone: true,
|
||||
imports: [EndUserAgreementContentComponent, FormsModule, TranslateModule],
|
||||
imports: [EndUserAgreementContentComponent, FormsModule, TranslateModule, BtnDisabledDirective],
|
||||
})
|
||||
/**
|
||||
* Component displaying the End User Agreement and an option to accept it
|
||||
|
@@ -41,7 +41,7 @@
|
||||
|
||||
<div class="row mt-3">
|
||||
<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>
|
||||
</form>
|
||||
|
@@ -18,6 +18,7 @@ import { FeedbackDataService } from '../../../core/feedback/feedback-data.servic
|
||||
import { Feedback } from '../../../core/feedback/models/feedback.model';
|
||||
import { RouteService } from '../../../core/services/route.service';
|
||||
import { NativeWindowService } from '../../../core/services/window.service';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref';
|
||||
import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
@@ -45,7 +46,7 @@ describe('FeedbackFormComponent', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), FeedbackFormComponent],
|
||||
imports: [TranslateModule.forRoot(), FeedbackFormComponent, BtnDisabledDirective],
|
||||
providers: [
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() },
|
||||
@@ -79,7 +80,8 @@ describe('FeedbackFormComponent', () => {
|
||||
});
|
||||
|
||||
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', () => {
|
||||
@@ -90,7 +92,8 @@ describe('FeedbackFormComponent', () => {
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
@@ -30,6 +30,7 @@ import {
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { ErrorComponent } from '../../../shared/error/error.component';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
|
||||
@@ -38,7 +39,7 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
||||
templateUrl: './feedback-form.component.html',
|
||||
styleUrls: ['./feedback-form.component.scss'],
|
||||
standalone: true,
|
||||
imports: [FormsModule, ReactiveFormsModule, NgIf, ErrorComponent, TranslateModule],
|
||||
imports: [FormsModule, ReactiveFormsModule, NgIf, ErrorComponent, TranslateModule, BtnDisabledDirective],
|
||||
})
|
||||
/**
|
||||
* Component displaying the contents of the Feedback Statement
|
||||
|
@@ -79,7 +79,7 @@
|
||||
</a>
|
||||
|
||||
<button
|
||||
[disabled]="requestCopyForm.invalid"
|
||||
[dsBtnDisabled]="requestCopyForm.invalid"
|
||||
class="btn btn-default btn-primary"
|
||||
(click)="onSubmit()">{{'bitstream-request-a-copy.submit' | translate}}</button>
|
||||
</div>
|
||||
|
@@ -55,6 +55,7 @@ import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
@@ -71,6 +72,7 @@ import { getItemPageRoute } from '../../item-page-routing-paths';
|
||||
AsyncPipe,
|
||||
ReactiveFormsModule,
|
||||
NgIf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -16,7 +16,7 @@
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
||||
</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"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i>
|
||||
@@ -24,7 +24,7 @@
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
||||
[disabled]="(hasChanges$ | async) !== true || submitting"
|
||||
[dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||
@@ -39,6 +39,9 @@
|
||||
[isFirstTable]="isFirst"
|
||||
aria-describedby="reorder-description">
|
||||
</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 *ngIf="bundles?.length === 0"
|
||||
class="alert alert-info w-100 d-inline-block mt-4" role="alert">
|
||||
@@ -54,7 +57,7 @@
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
||||
</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"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i>
|
||||
@@ -62,7 +65,7 @@
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
||||
[disabled]="(hasChanges$ | async) !== true || submitting"
|
||||
[dsBtnDisabled]="(hasChanges$ | async) !== true || submitting"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||
|
@@ -1,4 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
AsyncPipe,
|
||||
CommonModule,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
@@ -15,16 +20,22 @@ import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
Observable,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
import { AlertComponent } from 'src/app/shared/alert/alert.component';
|
||||
import { AlertType } from 'src/app/shared/alert/alert-type';
|
||||
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
||||
@@ -40,10 +51,14 @@ import {
|
||||
getFirstSucceededRemoteData,
|
||||
getRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||
import { AlertType } from '../../../shared/alert/alert-type';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../../../shared/empty.util';
|
||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
||||
import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe';
|
||||
@@ -58,12 +73,16 @@ import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle/i
|
||||
templateUrl: './item-bitstreams.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
ItemEditBitstreamBundleComponent,
|
||||
RouterLink,
|
||||
NgIf,
|
||||
VarDirective,
|
||||
NgForOf,
|
||||
ThemedLoadingComponent,
|
||||
AlertComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
providers: [ObjectValuesPipe],
|
||||
standalone: true,
|
||||
@@ -77,9 +96,18 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
protected readonly AlertType = AlertType;
|
||||
|
||||
/**
|
||||
* The currently listed bundles
|
||||
* All bundles for the current item
|
||||
*/
|
||||
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
|
||||
@@ -98,6 +126,18 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
*/
|
||||
itemUpdateSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* An observable which emits a boolean which represents whether the service is currently handling a 'move' request
|
||||
*/
|
||||
@@ -127,14 +167,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
* Actions to perform after the item has been initialized
|
||||
*/
|
||||
postItemInit(): void {
|
||||
const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions();
|
||||
this.isProcessingMoveRequest = this.itemBitstreamsService.getPerformingMoveRequest$();
|
||||
|
||||
this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({ pagination: bundlesOptions })).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page),
|
||||
);
|
||||
this.loadBundles(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,6 +232,26 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
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
|
||||
@@ -208,7 +261,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
submit() {
|
||||
this.submitting = true;
|
||||
|
||||
const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$);
|
||||
const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$.pipe(take(1)));
|
||||
|
||||
// Perform the setup actions from above in order and display notifications
|
||||
removedResponses$.subscribe((responses: RemoteData<NoContent>) => {
|
||||
@@ -217,6 +270,56 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications,
|
||||
* refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will
|
||||
* navigate the user to the correct page)
|
||||
* @param bundle The bundle to send patch requests to
|
||||
* @param event The event containing the index the bitstream came from and was dropped to
|
||||
*/
|
||||
dropBitstream(bundle: Bundle, event: any) {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) {
|
||||
const moveOperation = {
|
||||
op: 'move',
|
||||
from: `/_links/bitstreams/${event.fromIndex}/href`,
|
||||
path: `/_links/bitstreams/${event.toIndex}/href`,
|
||||
} as Operation;
|
||||
this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RemoteData<Bundle>) => {
|
||||
this.zone.run(() => {
|
||||
this.displayNotifications('item.edit.bitstreams.notifications.move', [response]);
|
||||
// Remove all cached requests from this bundle and call the event's callback when the requests are cleared
|
||||
this.requestService.removeByHrefSubstring(bundle.self).pipe(
|
||||
filter((isCached) => isCached),
|
||||
take(1),
|
||||
).subscribe(() => event.finish());
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display notifications
|
||||
* - Error notification for each failed response with their message
|
||||
* - Success notification in case there's at least one successful response
|
||||
* @param key The i18n key for the notification messages
|
||||
* @param responses The returned responses to display notifications for
|
||||
*/
|
||||
displayNotifications(key: string, responses: RemoteData<any>[]) {
|
||||
if (isNotEmpty(responses)) {
|
||||
const failedResponses = responses.filter((response: RemoteData<Bundle>) => hasValue(response) && response.hasFailed);
|
||||
const successfulResponses = responses.filter((response: RemoteData<Bundle>) => hasValue(response) && response.hasSucceeded);
|
||||
|
||||
failedResponses.forEach((response: RemoteData<Bundle>) => {
|
||||
this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage);
|
||||
});
|
||||
if (successfulResponses.length > 0) {
|
||||
this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the object updates service to discard all current changes to this item
|
||||
* Shows a notification to remind the user that they can undo this
|
||||
|
@@ -113,13 +113,13 @@
|
||||
title="{{'item.edit.bitstreams.edit.buttons.edit' | translate}}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button [disabled]="!canRemove(update)" (click)="remove(entry.bitstream)"
|
||||
<button [dsBtnDisabled]="!canRemove(update)" (click)="remove(entry.bitstream)"
|
||||
[attr.aria-label]=" 'item. edit bitstreams.edit.buttons.remove' | translate"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
title="{{'item.edit.bitstreams.edit.buttons.remove' | translate}}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
<button [disabled]="!canUndo(update)" (click)="undo(entry.bitstream)"
|
||||
<button [dsBtnDisabled]="!canUndo(update)" (click)="undo(entry.bitstream)"
|
||||
[attr.aria-label]="'item.edit.bitstreams.edit.buttons.undo' | translate"
|
||||
class="btn btn-outline-warning btn-sm"
|
||||
title="{{'item.edit.bitstreams.edit.buttons.undo' | translate}}">
|
||||
|
@@ -52,6 +52,7 @@ import {
|
||||
getAllSucceededRemoteData,
|
||||
paginatedListToArray,
|
||||
} from '../../../../core/shared/operators';
|
||||
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
@@ -87,6 +88,7 @@ import {
|
||||
NgbDropdownModule,
|
||||
CdkDrag,
|
||||
BrowserOnlyPipe,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -86,10 +86,10 @@
|
||||
</ng-container>
|
||||
|
||||
<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}}
|
||||
</button>
|
||||
<button [disabled]="isDeleting$ | async" [routerLink]="[itemPageRoute, 'edit']"
|
||||
<button [dsBtnDisabled]="isDeleting$ | async" [routerLink]="[itemPageRoute, 'edit']"
|
||||
class="btn btn-outline-secondary cancel">
|
||||
{{cancelMessage| translate}}
|
||||
</button>
|
||||
|
@@ -56,6 +56,7 @@ import {
|
||||
getRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
@@ -109,6 +110,7 @@ class RelationshipDTO {
|
||||
VarDirective,
|
||||
NgForOf,
|
||||
RouterLink,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -40,7 +40,7 @@
|
||||
<button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!canMove" (click)="moveToCollection()">
|
||||
<button class="btn btn-primary" [dsBtnDisabled]="!canMove" (click)="moveToCollection()">
|
||||
<span *ngIf="!processing">
|
||||
<i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}}
|
||||
</span>
|
||||
@@ -48,7 +48,7 @@
|
||||
<i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.move.processing' | translate}}
|
||||
</span>
|
||||
</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}}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -37,6 +37,7 @@ import {
|
||||
getRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import { SearchService } from '../../../core/shared/search/search.service';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { AuthorizedCollectionSelectorComponent } from '../../../shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
@@ -56,6 +57,7 @@ import {
|
||||
AsyncPipe,
|
||||
AuthorizedCollectionSelectorComponent,
|
||||
NgIf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -5,12 +5,12 @@
|
||||
</div>
|
||||
<div class="col-12 col-md-9 float-left action-button">
|
||||
<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}}
|
||||
</button>
|
||||
</span>
|
||||
<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}}
|
||||
</button>
|
||||
</span>
|
||||
|
@@ -6,6 +6,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { ItemOperationComponent } from './item-operation.component';
|
||||
import { ItemOperation } from './itemOperation.model';
|
||||
|
||||
@@ -17,7 +18,7 @@ describe('ItemOperationComponent', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ItemOperationComponent],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ItemOperationComponent, BtnDisabledDirective],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
@@ -43,7 +44,8 @@ describe('ItemOperationComponent', () => {
|
||||
const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement;
|
||||
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
@@ -7,6 +7,7 @@ import { RouterLink } from '@angular/router';
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { ItemOperation } from './itemOperation.model';
|
||||
|
||||
@Component({
|
||||
@@ -17,6 +18,7 @@ import { ItemOperation } from './itemOperation.model';
|
||||
RouterLink,
|
||||
NgbTooltipModule,
|
||||
NgIf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<h2 class="h4">
|
||||
{{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>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.relationships.edit.buttons.add" | translate}}</span>
|
||||
</button>
|
||||
|
@@ -388,7 +388,8 @@ describe('EditRelationshipListComponent', () => {
|
||||
comp.hasChanges = observableOf(true);
|
||||
fixture.detectChanges();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -65,6 +65,7 @@ import {
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getRemoteDataPayload,
|
||||
} from '../../../../core/shared/operators';
|
||||
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
@@ -100,6 +101,7 @@ import { EditRelationshipComponent } from '../edit-relationship/edit-relationshi
|
||||
TranslateModule,
|
||||
NgClass,
|
||||
ThemedLoadingComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -9,12 +9,12 @@
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<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"
|
||||
title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
<button [disabled]="!canUndo()" (click)="undo()"
|
||||
<button [dsBtnDisabled]="!canUndo()" (click)="undo()"
|
||||
class="btn btn-outline-warning btn-sm"
|
||||
title="{{'item.edit.metadata.edit.buttons.undo' | translate}}">
|
||||
<i class="fas fa-undo-alt fa-fw"></i>
|
||||
|
@@ -37,6 +37,7 @@ import {
|
||||
getRemoteDataPayload,
|
||||
} from '../../../../core/shared/operators';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
@@ -54,6 +55,7 @@ import { VirtualMetadataComponent } from '../../virtual-metadata/virtual-metadat
|
||||
NgIf,
|
||||
TranslateModule,
|
||||
VirtualMetadataComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -35,7 +35,7 @@
|
||||
<ng-template #buttons>
|
||||
<div class="d-flex space-children-mr justify-content-end">
|
||||
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||
[disabled]="(hasChanges$ | async) !== true"
|
||||
[dsBtnDisabled]="(hasChanges$ | async) !== true"
|
||||
(click)="discard()">
|
||||
<i aria-hidden="true" class="fas fa-times"></i>
|
||||
<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>
|
||||
</button>
|
||||
<button class="btn btn-primary"
|
||||
[disabled]="(hasChanges$ | async) !== true || (isSaving$ | async) === true"
|
||||
[dsBtnDisabled]="(hasChanges$ | async) !== true || (isSaving$ | async) === true"
|
||||
(click)="submit()">
|
||||
<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>
|
||||
|
@@ -42,6 +42,7 @@ import {
|
||||
} from '../../../core/shared/operators';
|
||||
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||
import { AlertType } from '../../../shared/alert/alert-type';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
@@ -67,6 +68,7 @@ import { EditRelationshipListWrapperComponent } from './edit-relationship-list-w
|
||||
TranslateModule,
|
||||
VarDirective,
|
||||
EditRelationshipListWrapperComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -19,7 +19,7 @@
|
||||
<div class="buttons" *ngIf="medias?.length > 1">
|
||||
<button
|
||||
class="btn btn-primary previous"
|
||||
[disabled]="currentIndex === 0"
|
||||
[dsBtnDisabled]="currentIndex === 0"
|
||||
(click)="prevMedia()"
|
||||
>
|
||||
{{ "media-viewer.previous" | translate }}
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<button
|
||||
class="btn btn-primary next"
|
||||
[disabled]="currentIndex === medias.length - 1"
|
||||
[dsBtnDisabled]="currentIndex === medias.length - 1"
|
||||
(click)="nextMedia()"
|
||||
>
|
||||
{{ "media-viewer.next" | translate }}
|
||||
|
@@ -12,6 +12,7 @@ import { Bitstream } from 'src/app/core/shared/bitstream.model';
|
||||
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { CaptionInfo } from './caption-info';
|
||||
import { languageHelper } from './language-helper';
|
||||
|
||||
@@ -27,6 +28,7 @@ import { languageHelper } from './language-helper';
|
||||
NgbDropdownModule,
|
||||
TranslateModule,
|
||||
NgIf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -48,7 +48,7 @@
|
||||
<div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner">
|
||||
<div class="col">
|
||||
<button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()"
|
||||
[disabled]="(unlinkProcessing | async)">
|
||||
[dsBtnDisabled]="(unlinkProcessing | async)">
|
||||
<span *ngIf="(unlinkProcessing | async) !== true"><i
|
||||
class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span>
|
||||
<span *ngIf="(unlinkProcessing | async)"><i
|
||||
|
@@ -34,6 +34,7 @@ import {
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-data.utils';
|
||||
|
||||
@@ -47,6 +48,7 @@ import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-d
|
||||
NgIf,
|
||||
NgForOf,
|
||||
AlertComponent,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -7,12 +7,12 @@
|
||||
<ds-loading *ngIf="(i + 1) === objects.length && (itemsRD || i > 0) && !(itemsRD?.hasSucceeded && itemsRD?.payload && itemsRD?.payload?.page?.length > 0)" message="{{'loading.default' | translate}}"></ds-loading>
|
||||
<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">
|
||||
<button class="btn btn-link btn-link-inline" (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>
|
||||
<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 } }} {{label}}</button>
|
||||
</div>
|
||||
<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' |
|
||||
translate:{ amount: itemsRD?.payload?.page?.length } }}</button>
|
||||
<button class="btn btn-link btn-link-inline text-capitalize" (click)="decrease()">{{'item.page.related-items.view-less' |
|
||||
translate:{ amount: itemsRD?.payload?.page?.length } }} {{label}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -31,7 +31,7 @@
|
||||
<!--CREATE-->
|
||||
<ng-container *ngIf="canCreateVersion$ | async">
|
||||
<button class="btn btn-outline-primary btn-sm version-row-element-create"
|
||||
[disabled]="isAnyBeingEdited() || hasDraftVersion"
|
||||
[dsBtnDisabled]="isAnyBeingEdited() || hasDraftVersion"
|
||||
(click)="createNewVersion(version)"
|
||||
title="{{createVersionTitle | translate }}">
|
||||
<i class="fas fa-code-branch fa-fw"></i>
|
||||
@@ -41,7 +41,7 @@
|
||||
<ng-container *ngIf="canDeleteVersion$ | async">
|
||||
<button class="btn btn-sm version-row-element-delete"
|
||||
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
|
||||
[disabled]="isAnyBeingEdited()"
|
||||
[dsBtnDisabled]="isAnyBeingEdited()"
|
||||
(click)="deleteVersion(version, version.id === itemVersion.id)"
|
||||
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
|
||||
<i class="fas fa-trash fa-fw"></i>
|
||||
|
@@ -49,6 +49,7 @@ import { VersionHistory } from '../../../core/shared/version-history.model';
|
||||
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
|
||||
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
|
||||
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
|
||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import {
|
||||
getItemEditVersionhistoryRoute,
|
||||
@@ -67,6 +68,7 @@ import { ItemVersionsSummaryModalComponent } from '../item-versions-summary-moda
|
||||
TranslateModule,
|
||||
NgClass,
|
||||
NgIf,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
templateUrl: './item-versions-row-element-version.component.html',
|
||||
styleUrl: './item-versions-row-element-version.component.scss',
|
||||
|
@@ -67,7 +67,7 @@
|
||||
<ng-template #notThisBeingEdited>
|
||||
<!--EDIT-->
|
||||
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
|
||||
[disabled]="isAnyBeingEdited()"
|
||||
[dsBtnDisabled]="isAnyBeingEdited()"
|
||||
(click)="enableVersionEditing(versionDTO.version)"
|
||||
title="{{'item.version.history.table.action.editSummary' | translate}}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
|
@@ -42,6 +42,7 @@ import { VersionHistory } from '../../core/shared/version-history.model';
|
||||
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
|
||||
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
|
||||
import { AlertComponent } from '../../shared/alert/alert.component';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { PaginationComponent } from '../../shared/pagination/pagination.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
@@ -158,7 +159,7 @@ describe('ItemVersionsComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterModule.forRoot([]), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemVersionsComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterModule.forRoot([]), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemVersionsComponent, VarDirective, BtnDisabledDirective],
|
||||
providers: [
|
||||
{ provide: PaginationService, useValue: new PaginationServiceStub() },
|
||||
{ provide: UntypedFormBuilder, useValue: new UntypedFormBuilder() },
|
||||
@@ -234,8 +235,9 @@ describe('ItemVersionsComponent', () => {
|
||||
it('should not disable the delete button', () => {
|
||||
const deleteButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-delete'));
|
||||
expect(deleteButtons.length).not.toBe(0);
|
||||
deleteButtons.forEach((btn: DebugElement) => {
|
||||
expect(btn.nativeElement.disabled).toBe(false);
|
||||
deleteButtons.forEach((btn) => {
|
||||
expect(btn.nativeElement.getAttribute('aria-disabled')).toBe('false');
|
||||
expect(btn.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -50,6 +50,7 @@ import { Version } from '../../core/shared/version.model';
|
||||
import { VersionHistory } from '../../core/shared/version-history.model';
|
||||
import { AlertComponent } from '../../shared/alert/alert.component';
|
||||
import { AlertType } from '../../shared/alert/alert-type';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import {
|
||||
hasValue,
|
||||
hasValueOperator,
|
||||
@@ -78,7 +79,7 @@ interface VersionDTO {
|
||||
templateUrl: './item-versions.component.html',
|
||||
styleUrls: ['./item-versions.component.scss'],
|
||||
standalone: true,
|
||||
imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule, ItemVersionsRowElementVersionComponent],
|
||||
imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule, ItemVersionsRowElementVersionComponent, BtnDisabledDirective],
|
||||
})
|
||||
|
||||
/**
|
||||
|
@@ -169,6 +169,7 @@ export class MenuResolverService {
|
||||
this.createExportMenuSections();
|
||||
this.createImportMenuSections();
|
||||
this.createAccessControlMenuSections();
|
||||
this.createReportMenuSections();
|
||||
return this.waitForMenu$(MenuID.ADMIN);
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<div class="add" *ngIf="(moreThanOne$ | async) !== true">
|
||||
<button class="btn btn-lg btn-outline-primary mt-1 ml-2"
|
||||
[attr.aria-label]="'mydspace.new-submission-external' | translate" [disabled]="(initialized$ | async) !== true"
|
||||
[attr.aria-label]="'mydspace.new-submission-external' | translate" [dsBtnDisabled]="(initialized$ | async) !== true"
|
||||
(click)="openPage(singleEntity)" role="button"
|
||||
title="{{'mydspace.new-submission-external' | translate}}">
|
||||
<i class="fa fa-file-import" aria-hidden="true"></i>
|
||||
@@ -10,7 +10,7 @@
|
||||
ngbDropdown
|
||||
*ngIf="(moreThanOne$ | async)">
|
||||
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownImport" ngbDropdownToggle
|
||||
type="button" [disabled]="(initialized$ | async) !== true"
|
||||
type="button" [dsBtnDisabled]="(initialized$ | async) !== true"
|
||||
[attr.aria-label]="'mydspace.new-submission-external' | translate"
|
||||
[attr.data-test]="'import-dropdown' | dsBrowserOnly"
|
||||
title="{{'mydspace.new-submission-external' | translate}}">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user