Merge remote-tracking branch 'origin/main' into w2p-119612_export-item-limit

# Conflicts:
#	src/assets/i18n/en.json5
This commit is contained in:
Jens Vannerum
2025-02-03 11:13:23 +01:00
221 changed files with 1754 additions and 436 deletions

View File

@@ -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"
}
},
{

View File

@@ -28,11 +28,11 @@ ssr:
# 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,
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,
enableBrowseComponent: false
# The REST API server settings
# NOTE: these settings define which (publicly available) REST API to use. They are usually

View File

@@ -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.

View 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 &amp; 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>
```

View File

@@ -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 = {

View 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;

15
package-lock.json generated
View File

@@ -59,7 +59,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",
@@ -10081,10 +10081,11 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -16623,9 +16624,9 @@
}
},
"node_modules/mirador": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/mirador/-/mirador-3.4.2.tgz",
"integrity": "sha512-Gd7G4NkXq6/qD/De5soYspSo9VykAzrGFunKqUI3x9WShoZP23pYIEPoC/96tvfk3KMv+UbAUxDp99Xeo7vnVQ==",
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/mirador/-/mirador-3.4.3.tgz",
"integrity": "sha512-yHoug0MHy4e9apykbbBhK+4CmbZS94zMxmugw2E2VX6iB0b2PKKY0JfYr/QfXh9P29YnWAbymaXJVpgbHVpTVw==",
"dependencies": {
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.9.1",

View File

@@ -146,7 +146,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",

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -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() },

View File

@@ -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>

View File

@@ -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);

View File

@@ -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,
})

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -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"

View File

@@ -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();
});
});
});

View File

@@ -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,
})

View File

@@ -54,7 +54,7 @@
<div class="col-auto">
<button class="btn btn-light" (click)="addQueryPredicate()">+</button>
&nbsp;
<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>
-->

View File

@@ -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,
})

View File

@@ -85,7 +85,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))),
);
}

View File

@@ -22,8 +22,8 @@ import {
of as observableOf,
} from 'rxjs';
import {
distinctUntilChanged,
map,
take,
} from 'rxjs/operators';
import { ThemedBrowseByComponent } from 'src/app/shared/browse-by/themed-browse-by.component';
@@ -42,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,
@@ -56,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,
@@ -68,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,
@@ -117,18 +103,20 @@ export class BrowseByDateComponent extends BrowseByMetadataComponent implements
this.startsWithType = StartsWithType.date;
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push(
observableCombineLatest(
[ this.route.params.pipe(take(1)),
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([
routeParams$,
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]) => {
]).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;

View File

@@ -11,6 +11,7 @@ import {
OnDestroy,
OnInit,
PLATFORM_ID,
SimpleChanges,
} from '@angular/core';
import {
ActivatedRoute,
@@ -26,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';
@@ -51,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,
@@ -64,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';
@@ -74,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,
@@ -232,21 +219,22 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
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);
this.subs.push(
observableCombineLatest(
[ this.route.params.pipe(take(1)),
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([
routeParams$,
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]) => {
]).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
this.browseId = params.id || this.defaultBrowseId;
this.authority = params.authority;
@@ -274,9 +262,11 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
}
ngOnChanges(): void {
ngOnChanges(changes: SimpleChanges): void {
if (hasValue(changes.scope)) {
this.scope$.next(this.scope);
}
}
/**
* Update the StartsWith options with text values

View File

@@ -11,11 +11,12 @@ import { Params } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import {
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
} from 'rxjs';
import {
distinctUntilChanged,
map,
take,
} from 'rxjs/operators';
import { environment } from '../../../environments/environment';
@@ -24,15 +25,8 @@ import {
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,
@@ -44,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,
@@ -71,18 +58,20 @@ export class BrowseByTitleComponent extends BrowseByMetadataComponent implements
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);
this.subs.push(
observableCombineLatest(
[ this.route.params.pipe(take(1)),
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([
routeParams$,
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]) => {
]).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);

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -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">&nbsp;{{'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">&nbsp;{{'collection.source.controls.reset.running' | translate}}</span>
</button>

View File

@@ -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');

View File

@@ -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,
})

View File

@@ -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">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
@@ -12,7 +12,7 @@
<span class="d-none d-sm-inline">&nbsp;{{"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">&nbsp;{{"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">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
@@ -56,7 +56,7 @@
<span class="d-none d-sm-inline">&nbsp;{{"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">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>

View File

@@ -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,
})

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -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);

View File

@@ -11,6 +11,7 @@ import {
from as observableFrom,
Observable,
of as observableOf,
shareReplay,
} from 'rxjs';
import {
map,
@@ -288,6 +289,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();
@@ -343,6 +348,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();

View File

@@ -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>

View File

@@ -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', () => {

View File

@@ -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

View File

@@ -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">&nbsp;{{ 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">&nbsp;{{ 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">&nbsp;{{ 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>

View File

@@ -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', () => {

View File

@@ -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

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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

View File

@@ -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>

View File

@@ -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', () => {

View File

@@ -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

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -16,7 +16,7 @@
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"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">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>
@@ -57,7 +57,7 @@
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"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>
@@ -65,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">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>

View File

@@ -51,6 +51,7 @@ import {
getFirstSucceededRemoteData,
getRemoteDataPayload,
} from '../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import {
hasValue,
isNotEmpty,
@@ -81,6 +82,7 @@ import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle/i
NgForOf,
ThemedLoadingComponent,
AlertComponent,
BtnDisabledDirective,
],
providers: [ObjectValuesPipe],
standalone: true,

View File

@@ -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}}">

View File

@@ -49,6 +49,7 @@ import {
getAllSucceededRemoteData,
paginatedListToArray,
} from '../../../../core/shared/operators';
import { BtnDisabledDirective } from '../../../../shared/btn-disabled.directive';
import {
hasNoValue,
hasValue,
@@ -83,6 +84,7 @@ import {
NgbDropdownModule,
CdkDrag,
BrowserOnlyPipe,
BtnDisabledDirective,
],
standalone: true,
})

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -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>

View File

@@ -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');
});
});

View File

@@ -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,
})

View File

@@ -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">&nbsp;{{"item.edit.relationships.edit.buttons.add" | translate}}</span>
</button>

View File

@@ -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();
});
});

View File

@@ -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,
})

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -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">&nbsp;{{ 'item.edit.metadata.discard-button' | translate }}</span>
@@ -45,7 +45,7 @@
<span class="d-none d-sm-inline">&nbsp;{{ '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>

View File

@@ -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,
})

View File

@@ -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 }}

View File

@@ -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,
})

View File

@@ -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

View File

@@ -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,
})

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -48,6 +48,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,
@@ -85,6 +86,7 @@ interface VersionDTO {
NgIf,
PaginationComponent,
TranslateModule,
BtnDisabledDirective,
],
})

View File

@@ -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}}">

View File

@@ -27,6 +27,7 @@ import { FindListOptions } from '../../../core/data/find-list-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { hasValue } from '../../../shared/empty.util';
import { EntityDropdownComponent } from '../../../shared/entity-dropdown/entity-dropdown.component';
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
@@ -45,6 +46,7 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
TranslateModule,
BrowserOnlyPipe,
NgIf,
BtnDisabledDirective,
],
standalone: true,
})

View File

@@ -1,6 +1,6 @@
<div class="add" *ngIf="(moreThanOne$ | async) !== true">
<button class="btn btn-lg btn-primary mt-1 ml-2" [attr.aria-label]="'mydspace.new-submission' | translate"
[disabled]="(initialized$ | async) !== true" (click)="openDialog(singleEntity)" role="button">
[dsBtnDisabled]="(initialized$ | async) !== true" (click)="openDialog(singleEntity)" role="button">
<i class="fa fa-plus-circle" aria-hidden="true"></i>
</button>
</div>
@@ -8,7 +8,7 @@
ngbDropdown
*ngIf="(moreThanOne$ | async)">
<button class="btn btn-lg btn-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle
type="button" [disabled]="(initialized$ | async) !== true"
type="button" [dsBtnDisabled]="(initialized$ | async) !== true"
[attr.aria-label]="'mydspace.new-submission' | translate"
[attr.data-test]="'submission-dropdown' | dsBrowserOnly"
title="{{'mydspace.new-submission' | translate}}">

View File

@@ -28,6 +28,7 @@ import { FindListOptions } from '../../../core/data/find-list-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ThemedCreateItemParentSelectorComponent } from '../../../shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
import { hasValue } from '../../../shared/empty.util';
import { EntityDropdownComponent } from '../../../shared/entity-dropdown/entity-dropdown.component';
@@ -47,6 +48,7 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
TranslateModule,
BrowserOnlyPipe,
NgIf,
BtnDisabledDirective,
],
standalone: true,
})

View File

@@ -1,35 +1,37 @@
<div class="ds-menu-item-wrapper text-md-center"
[id]="'expandable-navbar-section-' + section.id"
(mouseenter)="onMouseEnter($event, isActive)"
(mouseleave)="onMouseLeave($event, isActive)"
data-test="navbar-section-wrapper"
*ngVar="(active | async) as isActive">
(mouseenter)="onMouseEnter($event)"
(mouseleave)="onMouseLeave($event)"
data-test="navbar-section-wrapper">
<a href="javascript:void(0);" routerLinkActive="active"
role="menuitem"
(keyup.enter)="toggleSection($event)"
(keyup.space)="toggleSection($event)"
(click)="toggleSection($event)"
(keydown.space)="$event.preventDefault()"
(keydown)="keyDown($event)"
aria-haspopup="menu"
data-test="navbar-section-toggler"
[attr.aria-expanded]="isActive"
[attr.aria-controls]="expandableNavbarSectionId(section.id)"
[attr.aria-expanded]="(active$ | async).valueOf()"
[attr.aria-controls]="expandableNavbarSectionId()"
class="d-flex flex-row flex-nowrap align-items-center gapx-1 ds-menu-toggler-wrapper"
[class.disabled]="section.model?.disabled">
<span class="flex-fill">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
<!-- <span class="sr-only">{{'nav.expandable-navbar-section-suffix' | translate}}</span>-->
</span>
<i class="fas fa-caret-down fa-xs toggle-menu-icon" aria-hidden="true"></i>
</a>
<div @slide *ngIf="isActive" (click)="deactivateSection($event)"
[id]="expandableNavbarSectionId(section.id)"
<div *ngIf="(active$ | async).valueOf() === true" (click)="deactivateSection($event)"
[id]="expandableNavbarSectionId()"
[dsHoverOutsideOfParentSelector]="'#expandable-navbar-section-' + section.id"
(dsHoverOutside)="deactivateSection($event, false)"
role="menu"
class="dropdown-menu show nav-dropdown-menu m-0 shadow-none border-top-0 px-3 px-md-0 pt-0 pt-md-1">
<div @slide role="presentation">
<div *ngFor="let subSection of (subSections$ | async)" class="text-nowrap" role="presentation">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
</div>
</div>
</div>
</div>

View File

@@ -1,6 +1,11 @@
import { Component } from '@angular/core';
import {
Component,
DebugElement,
} from '@angular/core';
import {
ComponentFixture,
fakeAsync,
flush,
TestBed,
waitForAsync,
} from '@angular/core/testing';
@@ -10,9 +15,11 @@ import { of as observableOf } from 'rxjs';
import { HostWindowService } from '../../shared/host-window.service';
import { MenuService } from '../../shared/menu/menu.service';
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
import { MenuSection } from '../../shared/menu/menu-section.model';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
import { VarDirective } from '../../shared/utils/var.directive';
import { HoverOutsideDirective } from '../../shared/utils/hover-outside.directive';
import { ExpandableNavbarSectionComponent } from './expandable-navbar-section.component';
describe('ExpandableNavbarSectionComponent', () => {
@@ -23,11 +30,17 @@ describe('ExpandableNavbarSectionComponent', () => {
describe('on larger screens', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, ExpandableNavbarSectionComponent, TestComponent, VarDirective],
imports: [
ExpandableNavbarSectionComponent,
HoverOutsideDirective,
NoopAnimationsModule,
TestComponent,
],
providers: [
{ provide: 'sectionDataProvider', useValue: {} },
{ provide: MenuService, useValue: menuService },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
TestComponent,
],
}).compileComponents();
}));
@@ -41,10 +54,6 @@ describe('ExpandableNavbarSectionComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('when the mouse enters the section header (while inactive)', () => {
beforeEach(() => {
spyOn(component, 'onMouseEnter').and.callThrough();
@@ -141,6 +150,8 @@ describe('ExpandableNavbarSectionComponent', () => {
});
describe('when spacebar is pressed on section header (while inactive)', () => {
let sidebarToggler: DebugElement;
beforeEach(() => {
spyOn(component, 'toggleSection').and.callThrough();
spyOn(menuService, 'toggleActiveSection');
@@ -149,15 +160,27 @@ describe('ExpandableNavbarSectionComponent', () => {
component.ngOnInit();
fixture.detectChanges();
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
// dispatch the (keyup.space) action used in our component HTML
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
});
it('should call toggleSection on the menuService', () => {
// dispatch the (keyup.space) action used in our component HTML
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space', key: ' ' }));
expect(component.toggleSection).toHaveBeenCalled();
expect(menuService.toggleActiveSection).toHaveBeenCalled();
});
// Should not do anything in order to work correctly with NVDA: https://www.nvaccess.org/
it('should not do anything on keydown space', () => {
const event: Event = new KeyboardEvent('keydown', { code: 'Space', key: ' ' });
spyOn(event, 'preventDefault').and.callThrough();
// dispatch the (keyup.space) action used in our component HTML
sidebarToggler.nativeElement.dispatchEvent(event);
expect(event.preventDefault).toHaveBeenCalled();
});
});
describe('when spacebar is pressed on section header (while active)', () => {
@@ -179,12 +202,116 @@ describe('ExpandableNavbarSectionComponent', () => {
expect(menuService.toggleActiveSection).toHaveBeenCalled();
});
});
describe('when enter is pressed on section header (while inactive)', () => {
let sidebarToggler: DebugElement;
beforeEach(() => {
spyOn(component, 'toggleSection').and.callThrough();
spyOn(menuService, 'toggleActiveSection');
// Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property.
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false));
component.ngOnInit();
fixture.detectChanges();
sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
});
// Should not do anything in order to work correctly with NVDA: https://www.nvaccess.org/
it('should not do anything on keydown space', () => {
const event: Event = new KeyboardEvent('keydown', { code: 'Enter' });
spyOn(event, 'preventDefault').and.callThrough();
// dispatch the (keyup.space) action used in our component HTML
sidebarToggler.nativeElement.dispatchEvent(event);
expect(event.preventDefault).toHaveBeenCalled();
});
});
describe('when arrow down is pressed on section header', () => {
it('should call activateSection', () => {
spyOn(component, 'activateSection').and.callThrough();
const sidebarToggler: DebugElement = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
// dispatch the (keydown.ArrowDown) action used in our component HTML
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowDown' }));
expect(component.focusOnFirstChildSection).toBe(true);
expect(component.activateSection).toHaveBeenCalled();
});
});
describe('when tab is pressed on section header', () => {
it('should call deactivateSection', () => {
spyOn(component, 'deactivateSection').and.callThrough();
const sidebarToggler: DebugElement = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
// dispatch the (keydown.ArrowDown) action used in our component HTML
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Tab' }));
expect(component.deactivateSection).toHaveBeenCalled();
});
});
describe('navigateDropdown', () => {
beforeEach(fakeAsync(() => {
jasmine.getEnv().allowRespy(true);
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([
Object.assign(new MenuSection(), {
id: 'subSection1',
model: Object.assign(new LinkMenuItemModel(), {
type: 'TEST_LINK',
}),
parentId: component.section.id,
}),
Object.assign(new MenuSection(), {
id: 'subSection2',
model: Object.assign(new LinkMenuItemModel(), {
type: 'TEST_LINK',
}),
parentId: component.section.id,
}),
]));
component.ngOnInit();
flush();
fixture.detectChanges();
component.focusOnFirstChildSection = true;
component.active$.next(true);
fixture.detectChanges();
}));
it('should close the modal on Tab', () => {
spyOn(menuService, 'deactivateSection').and.callThrough();
const firstSubsection: DebugElement = fixture.debugElement.queryAll(By.css('.dropdown-menu a[role="menuitem"]'))[0];
firstSubsection.nativeElement.focus();
firstSubsection.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Tab' }));
expect(menuService.deactivateSection).toHaveBeenCalled();
});
it('should close the modal on Escape', () => {
spyOn(menuService, 'deactivateSection').and.callThrough();
const firstSubsection: DebugElement = fixture.debugElement.queryAll(By.css('.dropdown-menu a[role="menuitem"]'))[0];
firstSubsection.nativeElement.focus();
firstSubsection.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape' }));
expect(menuService.deactivateSection).toHaveBeenCalled();
});
});
});
describe('on smaller, mobile screens', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, ExpandableNavbarSectionComponent, TestComponent, VarDirective],
imports: [
ExpandableNavbarSectionComponent,
HoverOutsideDirective,
NoopAnimationsModule,
TestComponent,
],
providers: [
{ provide: 'sectionDataProvider', useValue: {} },
{ provide: MenuService, useValue: menuService },
@@ -253,7 +380,9 @@ describe('ExpandableNavbarSectionComponent', () => {
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``,
template: `
<a role="menuitem">link</a>
`,
standalone: true,
})
class TestComponent {

View File

@@ -5,10 +5,12 @@ import {
NgIf,
} from '@angular/common';
import {
AfterViewChecked,
Component,
HostListener,
Inject,
Injector,
OnDestroy,
OnInit,
} from '@angular/core';
import { RouterLinkActive } from '@angular/router';
@@ -19,7 +21,8 @@ import { slide } from '../../shared/animations/slide';
import { HostWindowService } from '../../shared/host-window.service';
import { MenuService } from '../../shared/menu/menu.service';
import { MenuID } from '../../shared/menu/menu-id.model';
import { VarDirective } from '../../shared/utils/var.directive';
import { MenuSection } from '../../shared/menu/menu-section.model';
import { HoverOutsideDirective } from '../../shared/utils/hover-outside.directive';
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
/**
@@ -31,9 +34,17 @@ import { NavbarSectionComponent } from '../navbar-section/navbar-section.compone
styleUrls: ['./expandable-navbar-section.component.scss'],
animations: [slide],
standalone: true,
imports: [VarDirective, RouterLinkActive, NgComponentOutlet, NgIf, NgFor, AsyncPipe],
imports: [
AsyncPipe,
HoverOutsideDirective,
NgComponentOutlet,
NgFor,
NgIf,
RouterLinkActive,
],
})
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit {
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements AfterViewChecked, OnInit, OnDestroy {
/**
* This section resides in the Public Navbar
*/
@@ -44,6 +55,11 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
*/
mouseEntered = false;
/**
* Whether the section was expanded
*/
focusOnFirstChildSection = false;
/**
* True if screen size was small before a resize event
*/
@@ -54,6 +70,18 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
*/
isMobile$: Observable<boolean>;
/**
* Boolean used to add the event listeners to the items in the expandable menu when expanded. This is done for
* performance reasons, there is currently an *ngIf on the menu to prevent the {@link HoverOutsideDirective} to tank
* performance when not expanded.
*/
addArrowEventListeners = false;
/**
* List of current dropdown items who have event listeners
*/
private dropdownItems: NodeListOf<HTMLElement>;
@HostListener('window:resize', ['$event'])
onResize() {
this.isMobile$.pipe(
@@ -68,29 +96,80 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
});
}
constructor(@Inject('sectionDataProvider') menuSection,
constructor(
@Inject('sectionDataProvider') public section: MenuSection,
protected menuService: MenuService,
protected injector: Injector,
private windowService: HostWindowService,
protected windowService: HostWindowService,
) {
super(menuSection, menuService, injector);
super(section, menuService, injector);
this.isMobile$ = this.windowService.isMobile();
}
ngOnInit() {
super.ngOnInit();
this.subs.push(this.active$.subscribe((active: boolean) => {
if (active === true) {
this.addArrowEventListeners = true;
} else {
this.focusOnFirstChildSection = undefined;
this.unsubscribeFromEventListeners();
}
}));
}
ngAfterViewChecked(): void {
if (this.addArrowEventListeners) {
this.dropdownItems = document.querySelectorAll(`#${this.expandableNavbarSectionId()} *[role="menuitem"]`);
this.dropdownItems.forEach((item: HTMLElement) => {
item.addEventListener('keydown', this.navigateDropdown.bind(this));
});
if (this.focusOnFirstChildSection && this.dropdownItems.length > 0) {
this.dropdownItems.item(0).focus();
}
this.addArrowEventListeners = false;
}
}
ngOnDestroy(): void {
super.ngOnDestroy();
this.unsubscribeFromEventListeners();
}
/**
* Activate this section if it's currently inactive, deactivate it when it's currently active.
* Also saves whether this toggle was performed by a keyboard event (non-click event) in order to know if thi first
* item should be focussed when activating a section.
*
* @param {Event} event The user event that triggered this method
*/
override toggleSection(event: Event): void {
this.focusOnFirstChildSection = event.type !== 'click';
super.toggleSection(event);
}
/**
* Removes all the current event listeners on the dropdown items (called when the menu is closed & on component
* destruction)
*/
unsubscribeFromEventListeners(): void {
if (this.dropdownItems) {
this.dropdownItems.forEach((item: HTMLElement) => {
item.removeEventListener('keydown', this.navigateDropdown.bind(this));
});
this.dropdownItems = undefined;
}
}
/**
* When the mouse enters the section toggler activate the menu section
* @param $event
* @param isActive
*/
onMouseEnter($event: Event, isActive: boolean) {
onMouseEnter($event: Event): void {
this.isMobile$.pipe(
first(),
).subscribe((isMobile) => {
if (!isMobile && !isActive && !this.mouseEntered) {
if (!isMobile && !this.active$.value && !this.mouseEntered) {
this.activateSection($event);
}
this.mouseEntered = true;
@@ -100,13 +179,12 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
/**
* When the mouse leaves the section toggler deactivate the menu section
* @param $event
* @param isActive
*/
onMouseLeave($event: Event, isActive: boolean) {
onMouseLeave($event: Event): void {
this.isMobile$.pipe(
first(),
).subscribe((isMobile) => {
if (!isMobile && isActive && this.mouseEntered) {
if (!isMobile && this.active$.value && this.mouseEntered) {
this.deactivateSection($event);
}
this.mouseEntered = false;
@@ -115,9 +193,60 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
/**
* returns the ID of the DOM element representing the navbar section
* @param sectionId
*/
expandableNavbarSectionId(sectionId: string) {
return `expandable-navbar-section-${sectionId}-dropdown`;
expandableNavbarSectionId(): string {
return `expandable-navbar-section-${this.section.id}-dropdown`;
}
/**
* Handles the navigation between the menu items
*
* @param event
*/
navigateDropdown(event: KeyboardEvent): void {
if (event.code === 'Tab') {
this.deactivateSection(event, false);
return;
} else if (event.code === 'Escape') {
this.deactivateSection(event, false);
(document.querySelector(`a[aria-controls="${this.expandableNavbarSectionId()}"]`) as HTMLElement)?.focus();
return;
}
event.preventDefault();
event.stopPropagation();
const items: NodeListOf<Element> = document.querySelectorAll(`#${this.expandableNavbarSectionId()} *[role="menuitem"]`);
if (items.length === 0) {
return;
}
const currentIndex: number = Array.from(items).findIndex((item: Element) => item === event.target);
if (event.key === 'ArrowDown') {
(items[(currentIndex + 1) % items.length] as HTMLElement).focus();
} else if (event.key === 'ArrowUp') {
(items[(currentIndex - 1 + items.length) % items.length] as HTMLElement).focus();
}
}
/**
* Handles all the keydown events on the dropdown toggle
*
* @param event
*/
keyDown(event: KeyboardEvent): void {
switch (event.code) {
// Works for both Tab & Shift Tab
case 'Tab':
this.deactivateSection(event, false);
break;
case 'ArrowDown':
this.focusOnFirstChildSection = true;
this.activateSection(event);
break;
case 'Space':
case 'Enter':
event.preventDefault();
break;
}
}
}

View File

@@ -138,7 +138,7 @@
<div class="btn-group">
<button *ngIf="!eventElement.hasProject"
class="btn btn-outline-primary btn-sm"
[disabled]="eventElement.isRunning"
[dsBtnDisabled]="eventElement.isRunning"
(click)="openModalLookup(eventElement); $event.stopPropagation();"
[attr.aria-label]="'quality-assurance.event.modal.project.select' | translate"
>
@@ -146,7 +146,7 @@
</button>
<button *ngIf="eventElement.hasProject"
class="btn btn-outline-danger btn-sm"
[disabled]="eventElement.isRunning"
[dsBtnDisabled]="eventElement.isRunning"
(click)="removeProject(eventElement)"
[attr.aria-label]="'quality-assurance.event.modal.project.remove' | translate"
>
@@ -161,7 +161,7 @@
class="btn btn-outline-success btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.import' | translate}}"
container="body"
[disabled]="eventElement.isRunning"
[dsBtnDisabled]="eventElement.isRunning"
(click)="modalChoice('ACCEPTED', eventElement, acceptModal)"
[attr.aria-label]="'quality-assurance.event.action.import' | translate"
>
@@ -171,7 +171,7 @@
class="btn btn-outline-success btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.accept' | translate}}"
container="body"
[disabled]="eventElement.isRunning"
[dsBtnDisabled]="eventElement.isRunning"
(click)="executeAction('ACCEPTED', eventElement)"
[attr.aria-label]="'quality-assurance.event.action.accept' | translate"
>
@@ -180,7 +180,7 @@
<button class="btn btn-outline-dark btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.ignore' | translate}}"
container="body"
[disabled]="eventElement.isRunning"
[dsBtnDisabled]="eventElement.isRunning"
(click)="openModal('DISCARDED', eventElement, ignoreModal)"
[attr.aria-label]="'quality-assurance.event.action.ignore' | translate"
>
@@ -190,7 +190,7 @@
*ngIf="(isAdmin$ | async)"
ngbTooltip="{{'quality-assurance.event.action.reject' | translate}}"
container="body"
[disabled]="eventElement.isRunning"
[dsBtnDisabled]="eventElement.isRunning"
(click)="openModal('REJECTED', eventElement, rejectModal)"
[attr.aria-label]="'quality-assurance.event.action.reject' | translate"
>
@@ -200,7 +200,7 @@
*ngIf="(isAdmin$ | async) === false"
ngbTooltip="{{'quality-assurance.event.action.undo' | translate }}"
container="body"
[disabled]="eventElement.isRunning"
[dsBtnDisabled]="eventElement.isRunning"
[attr.aria-label]="'quality-assurance.event.action.undo' | translate"
(click)="openModal('UNDO', eventElement, undoModal)">
<i class="fas fa-trash-alt"></i>
@@ -210,7 +210,7 @@
<button class="btn btn-outline-danger btn-sm button-width"
ngbTooltip="{{'quality-assurance.event.action.undo' | translate}}"
container="body"
[disabled]="eventElement.isRunning"
[dsBtnDisabled]="eventElement.isRunning"
[attr.aria-label]="'quality-assurance.event.action.undo' | translate"
(click)="openModal('UNDO', eventElement, undoModal)">
<i class="fas fa-trash-alt"></i>

View File

@@ -65,6 +65,7 @@ import {
} from '../../../core/shared/operators';
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
import { AlertComponent } from '../../../shared/alert/alert.component';
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';
@@ -86,7 +87,7 @@ import { EPersonDataComponent } from './ePerson-data/ePerson-data.component';
templateUrl: './quality-assurance-events.component.html',
styleUrls: ['./quality-assurance-events.component.scss'],
standalone: true,
imports: [AlertComponent, NgIf, ThemedLoadingComponent, PaginationComponent, NgFor, RouterLink, NgbTooltipModule, AsyncPipe, TranslateModule, EPersonDataComponent],
imports: [AlertComponent, NgIf, ThemedLoadingComponent, PaginationComponent, NgFor, RouterLink, NgbTooltipModule, AsyncPipe, TranslateModule, EPersonDataComponent, BtnDisabledDirective],
})
export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
/**

View File

@@ -32,8 +32,8 @@
<div id="project-search" class="input-group mb-3">
<input type="text" class="form-control" (keyup.enter)="search(projectTitle)" [(ngModel)]="projectTitle" placeholder="{{labelPrefix + label + '.placeholder' |translate}}" aria-label="" aria-describedby="">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" [disabled]="projectTitle === ''" (click)="projectTitle = ''">{{(labelPrefix + label + '.clear'|translate)}}</button>
<button type="button" class="btn btn-primary" [disabled]="projectTitle === ''" (click)="search(projectTitle)">{{(labelPrefix + label + '.search'|translate)}}</button>
<button type="button" class="btn btn-outline-secondary" [dsBtnDisabled]="projectTitle === ''" (click)="projectTitle = ''">{{(labelPrefix + label + '.clear'|translate)}}</button>
<button type="button" class="btn btn-primary" [dsBtnDisabled]="projectTitle === ''" (click)="search(projectTitle)">{{(labelPrefix + label + '.search'|translate)}}</button>
</div>
</div>
@@ -66,6 +66,6 @@
<button type="button" class="btn btn-outline-secondary" (click)="close()">{{ (labelPrefix + label + '.cancel' | translate) }}</button>
</div>
<div>
<button type="button" class="btn btn-primary" [disabled]="selectedImportType === importType.None" (click)="bound()">{{ (labelPrefix + label + '.bound' | translate) }}</button>
<button type="button" class="btn btn-primary" [dsBtnDisabled]="selectedImportType === importType.None" (click)="bound()">{{ (labelPrefix + label + '.bound' | translate) }}</button>
</div>
</div>

View File

@@ -30,6 +30,7 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { Item } from '../../../core/shared/item.model';
import { SearchService } from '../../../core/shared/search/search.service';
import { AlertComponent } from '../../../shared/alert/alert.component';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import {
hasValue,
isNotEmpty,
@@ -105,7 +106,7 @@ export interface QualityAssuranceEventData {
styleUrls: ['./project-entry-import-modal.component.scss'],
templateUrl: './project-entry-import-modal.component.html',
standalone: true,
imports: [RouterLink, NgIf, FormsModule, ThemedLoadingComponent, ThemedSearchResultsComponent, AlertComponent, AsyncPipe, TranslateModule],
imports: [RouterLink, NgIf, FormsModule, ThemedLoadingComponent, ThemedSearchResultsComponent, AlertComponent, AsyncPipe, TranslateModule, BtnDisabledDirective],
})
/**
* Component to display a modal window for linking a project to an Quality Assurance event

View File

@@ -21,7 +21,7 @@
</div>
<button (click)="ignoreSuggestion()" class="btn btn-danger ml-2"><i class="fa fa-ban"></i>
{{ ignoreSuggestionLabel() | translate}}</button>
<button *ngIf="!isBulk" (click)="toggleSeeEvidences()" [disabled]="!hasEvidence" class="btn btn-info ml-2">
<button *ngIf="!isBulk" (click)="toggleSeeEvidences()" [dsBtnDisabled]="!hasEvidence" class="btn btn-info ml-2">
<i class="fa fa-eye"></i>
<ng-container *ngIf="!seeEvidence"> {{ 'suggestion.seeEvidence' | translate}}</ng-container>
<ng-container *ngIf="seeEvidence"> {{ 'suggestion.hideEvidence' | translate}}</ng-container>

View File

@@ -15,6 +15,7 @@ import { take } from 'rxjs/operators';
import { Suggestion } from '../../core/notifications/suggestions/models/suggestion.model';
import { Collection } from '../../core/shared/collection.model';
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { ThemedCreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
import { EntityDropdownComponent } from '../../shared/entity-dropdown/entity-dropdown.component';
import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestion-approve-and-import';
@@ -31,6 +32,7 @@ import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestio
TranslateModule,
NgIf,
NgbDropdownModule,
BtnDisabledDirective,
],
standalone: true,
})

View File

@@ -64,10 +64,10 @@
<span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span>
</div>
<div class="mt-4">
<button class="btn btn-primary mr-2" [disabled]="processBulkDeleteService.isProcessing$() |async"
<button class="btn btn-primary mr-2" [dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async"
(click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
<button id="delete-confirm" class="btn btn-danger"
[disabled]="processBulkDeleteService.isProcessing$() |async"
[dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async"
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
</button>
</div>

View File

@@ -16,6 +16,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { hasValue } from '../../shared/empty.util';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { VarDirective } from '../../shared/utils/var.directive';
@@ -31,7 +32,7 @@ import { ProcessOverviewTableComponent } from './table/process-overview-table.co
selector: 'ds-process-overview',
templateUrl: './process-overview.component.html',
standalone: true,
imports: [NgIf, RouterLink, PaginationComponent, NgFor, VarDirective, AsyncPipe, DatePipe, TranslateModule, NgTemplateOutlet, ProcessOverviewTableComponent],
imports: [NgIf, RouterLink, PaginationComponent, NgFor, VarDirective, AsyncPipe, DatePipe, TranslateModule, NgTemplateOutlet, ProcessOverviewTableComponent, BtnDisabledDirective],
})
/**
* Component displaying a list of all processes in a paginated table

View File

@@ -29,7 +29,7 @@
<input type="checkbox" [checked]="checked" (change)="toggleCheckbox()"/>
{{ 'dso-selector.claim.item.not-mine-label' | translate }}
</div>
<button type="submit" class="btn btn-primary ml-5 mr-2" (click)="createFromScratch()" [disabled]="!checked">
<button type="submit" class="btn btn-primary ml-5 mr-2" (click)="createFromScratch()" [dsBtnDisabled]="!checked">
<i class="fas fa-plus"></i>
{{ 'dso-selector.claim.item.create-from-scratch' | translate }}
</button>

View File

@@ -25,6 +25,7 @@ import { Item } from '../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { ViewMode } from '../../core/shared/view-mode.model';
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { DSOSelectorModalWrapperComponent } from '../../shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component';
import { CollectionElementLinkType } from '../../shared/object-collection/collection-element-link.type';
import { ListableObjectComponentLoaderComponent } from '../../shared/object-collection/shared/listable-object/listable-object-component-loader.component';
@@ -42,6 +43,7 @@ import { ProfileClaimService } from '../profile-claim/profile-claim.service';
AsyncPipe,
TranslateModule,
NgForOf,
BtnDisabledDirective,
],
standalone: true,
})

View File

@@ -13,7 +13,7 @@
<p>{{'researcher.profile.not.associated' | translate}}</p>
</div>
<button *ngIf="!researcherProfile" class="btn btn-primary mr-2"
[disabled]="(isProcessingCreate() | async)"
[dsBtnDisabled]="(isProcessingCreate() | async)"
(click)="createProfile()">
<span *ngIf="(isProcessingCreate() | async)">
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
@@ -23,10 +23,10 @@
</span>
</button>
<ng-container *ngIf="researcherProfile">
<button class="btn btn-primary mr-2" [disabled]="!researcherProfile" (click)="viewProfile(researcherProfile)">
<button class="btn btn-primary mr-2" [dsBtnDisabled]="!researcherProfile" (click)="viewProfile(researcherProfile)">
<i class="fas fa-info-circle"></i> {{'researcher.profile.view' | translate}}
</button>
<button class="btn btn-danger" [disabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)">
<button class="btn btn-danger" [dsBtnDisabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)">
<span *ngIf="(isProcessingDelete() | async)">
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
</span>

View File

@@ -36,6 +36,7 @@ import {
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload,
} from '../../core/shared/operators';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component';
import { isNotEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -53,6 +54,7 @@ import { ProfileClaimItemModalComponent } from '../profile-claim-item-modal/prof
TranslateModule,
UiSwitchModule,
VarDirective,
BtnDisabledDirective,
],
standalone: true,
})

View File

@@ -220,7 +220,8 @@ export class ProfilePageComponent implements OnInit {
*/
updateSecurity() {
const passEntered = isNotEmpty(this.password);
if (this.invalidSecurity) {
const validCurrentPassword = isNotEmpty(this.currentPassword);
if (validCurrentPassword && !passEntered) {
this.notificationsService.error(this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.general'));
}
if (!this.invalidSecurity && passEntered) {

Some files were not shown because too many files have changed in this diff Show More