Merge branch 'main' of github.com:4science/dspace-angular into CST-4499

This commit is contained in:
Davide Negretti
2021-10-19 16:18:12 +02:00
42 changed files with 6240 additions and 6203 deletions

View File

@@ -0,0 +1,15 @@
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
import { testA11y } from 'cypress/support/utils';
describe('Breadcrumbs', () => {
it('should pass accessibility tests', () => {
// Visit an Item, as those have more breadcrumbs
cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION);
// Wait for breadcrumbs to be visible
cy.get('ds-breadcrumbs').should('be.visible');
// Analyze <ds-breadcrumbs> for accessibility
testA11y('ds-breadcrumbs');
});
});

View File

@@ -0,0 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Browse By Author', () => {
it('should pass accessibility tests', () => {
cy.visit('/browse/author');
// Wait for <ds-browse-by-metadata-page> to be visible
cy.get('ds-browse-by-metadata-page').should('be.visible');
// Analyze <ds-browse-by-metadata-page> for accessibility
testA11y('ds-browse-by-metadata-page');
});
});

View File

@@ -0,0 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Browse By Date Issued', () => {
it('should pass accessibility tests', () => {
cy.visit('/browse/dateissued');
// Wait for <ds-browse-by-date-page> to be visible
cy.get('ds-browse-by-date-page').should('be.visible');
// Analyze <ds-browse-by-date-page> for accessibility
testA11y('ds-browse-by-date-page');
});
});

View File

@@ -0,0 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Browse By Subject', () => {
it('should pass accessibility tests', () => {
cy.visit('/browse/subject');
// Wait for <ds-browse-by-metadata-page> to be visible
cy.get('ds-browse-by-metadata-page').should('be.visible');
// Analyze <ds-browse-by-metadata-page> for accessibility
testA11y('ds-browse-by-metadata-page');
});
});

View File

@@ -0,0 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Browse By Title', () => {
it('should pass accessibility tests', () => {
cy.visit('/browse/title');
// Wait for <ds-browse-by-title-page> to be visible
cy.get('ds-browse-by-title-page').should('be.visible');
// Analyze <ds-browse-by-title-page> for accessibility
testA11y('ds-browse-by-title-page');
});
});

View File

@@ -0,0 +1,15 @@
import { TEST_COLLECTION } from 'cypress/support';
import { testA11y } from 'cypress/support/utils';
describe('Collection Page', () => {
it('should pass accessibility tests', () => {
cy.visit('/collections/' + TEST_COLLECTION);
// <ds-collection-page> tag must be loaded
cy.get('ds-collection-page').should('exist');
// Analyze <ds-collection-page> for accessibility issues
testA11y('ds-collection-page');
});
});

View File

@@ -0,0 +1,32 @@
import { TEST_COLLECTION } from 'cypress/support';
import { testA11y } from 'cypress/support/utils';
describe('Collection Statistics Page', () => {
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/' + TEST_COLLECTION;
it('should load if you click on "Statistics" from a Collection page', () => {
cy.visit('/collections/' + TEST_COLLECTION);
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
});
it('should contain a "Total visits" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
cy.get('.' + TEST_COLLECTION + '_TotalVisits').should('exist');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
cy.get('.' + TEST_COLLECTION + '_TotalVisitsPerMonth').should('exist');
});
it('should pass accessibility tests', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
// <ds-collection-statistics-page> tag must be loaded
cy.get('ds-collection-statistics-page').should('exist');
// Analyze <ds-collection-statistics-page> for accessibility issues
testA11y('ds-collection-statistics-page');
});
});

View File

@@ -0,0 +1,25 @@
import { Options } from 'cypress-axe';
import { testA11y } from 'cypress/support/utils';
describe('Community List Page', () => {
it('should pass accessibility tests', () => {
cy.visit('/community-list');
// <ds-community-list-page> tag must be loaded
cy.get('ds-community-list-page').should('exist');
// Open first Community (to show Collections)...that way we scan sub-elements as well
cy.get('ds-community-list :nth-child(1) > .btn-group > .btn').click();
// Analyze <ds-community-list-page> for accessibility issues
// Disable heading-order checks until it is fixed
testA11y('ds-community-list-page',
{
rules: {
'heading-order': { enabled: false }
}
} as Options
);
});
});

View File

@@ -0,0 +1,15 @@
import { TEST_COMMUNITY } from 'cypress/support';
import { testA11y } from 'cypress/support/utils';
describe('Community Page', () => {
it('should pass accessibility tests', () => {
cy.visit('/communities/' + TEST_COMMUNITY);
// <ds-community-page> tag must be loaded
cy.get('ds-community-page').should('exist');
// Analyze <ds-community-page> for accessibility issues
testA11y('ds-community-page',);
});
});

View File

@@ -0,0 +1,32 @@
import { TEST_COMMUNITY } from 'cypress/support';
import { testA11y } from 'cypress/support/utils';
describe('Community Statistics Page', () => {
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/' + TEST_COMMUNITY;
it('should load if you click on "Statistics" from a Community page', () => {
cy.visit('/communities/' + TEST_COMMUNITY);
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
});
it('should contain a "Total visits" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
cy.get('.' + TEST_COMMUNITY + '_TotalVisits').should('exist');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
cy.get('.' + TEST_COMMUNITY + '_TotalVisitsPerMonth').should('exist');
});
it('should pass accessibility tests', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
// <ds-community-statistics-page> tag must be loaded
cy.get('ds-community-statistics-page').should('exist');
// Analyze <ds-community-statistics-page> for accessibility issues
testA11y('ds-community-statistics-page');
});
});

View File

@@ -0,0 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Footer', () => {
it('should pass accessibility tests', () => {
cy.visit('/');
// Footer must first be visible
cy.get('ds-footer').should('be.visible');
// Analyze <ds-footer> for accessibility
testA11y('ds-footer');
});
});

View File

@@ -0,0 +1,19 @@
import { testA11y } from 'cypress/support/utils';
describe('Header', () => {
it('should pass accessibility tests', () => {
cy.visit('/');
// Header must first be visible
cy.get('ds-header').should('be.visible');
// Analyze <ds-header> for accessibility
testA11y({
include: ['ds-header'],
exclude: [
['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174
['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149
],
});
});
});

View File

@@ -0,0 +1,19 @@
import { testA11y } from 'cypress/support/utils';
describe('Site Statistics Page', () => {
it('should load if you click on "Statistics" from homepage', () => {
cy.visit('/');
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
cy.location('pathname').should('eq', '/statistics');
});
it('should pass accessibility tests', () => {
cy.visit('/statistics');
// <ds-site-statistics-page> tag must be loaded
cy.get('ds-site-statistics-page').should('exist');
// Analyze <ds-site-statistics-page> for accessibility issues
testA11y('ds-site-statistics-page');
});
});

View File

@@ -1,3 +1,5 @@
import { testA11y } from 'cypress/support/utils';
describe('Homepage', () => {
beforeEach(() => {
// All tests start with visiting homepage
@@ -20,18 +22,11 @@ describe('Homepage', () => {
cy.url().should('include', 'query=' + encodeURI(queryString));
});
// it('should pass accessibility tests', () => {
// // first must inject Axe into current page
// cy.injectAxe();
it('should pass accessibility tests', () => {
// Wait for homepage tag to appear
cy.get('ds-home-page').should('be.visible');
// // Analyze entire page for accessibility issues
// // NOTE: this test checks accessibility of header/footer as well
// cy.checkA11y({
// exclude: [
// ['#klaro'], // Klaro plugin (privacy policy popup) has color contrast issues
// ['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174
// ['.dropdownLogin'] // "Log in" link in header has color contrast issues
// ],
// });
// });
// Analyze <ds-home-page> for accessibility issues
testA11y('ds-home-page');
});
});

View File

@@ -1,15 +1,31 @@
describe('Item Page', () => {
const ITEMPAGE = '/items/e98b0f27-5c19-49a0-960d-eb6ad5287067';
const ENTITYPAGE = '/entities/publication/e98b0f27-5c19-49a0-960d-eb6ad5287067';
import { Options } from 'cypress-axe';
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
import { testA11y } from 'cypress/support/utils';
it('should contain element ds-item-page when navigating to an item page', () => {
cy.visit(ENTITYPAGE);
cy.get('ds-item-page').should('exist');
});
describe('Item Page', () => {
const ITEMPAGE = '/items/' + TEST_ENTITY_PUBLICATION;
const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION;
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
it('should redirect to the entity page when navigating to an item page', () => {
cy.visit(ITEMPAGE);
cy.location('pathname').should('eq', ENTITYPAGE);
});
it('should pass accessibility tests', () => {
cy.visit(ENTITYPAGE);
// <ds-item-page> tag must be loaded
cy.get('ds-item-page').should('exist');
// Analyze <ds-item-page> for accessibility issues
// Disable heading-order checks until it is fixed
testA11y('ds-item-page',
{
rules: {
'heading-order': { enabled: false }
}
} as Options
);
});
});

View File

@@ -1,6 +1,14 @@
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
import { testA11y } from 'cypress/support/utils';
describe('Item Statistics Page', () => {
const ITEMUUID = 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
const ITEMSTATISTICSPAGE = '/statistics/items/' + ITEMUUID;
const ITEMSTATISTICSPAGE = '/statistics/items/' + TEST_ENTITY_PUBLICATION;
it('should load if you click on "Statistics" from an Item/Entity page', () => {
cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION);
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
});
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
cy.visit(ITEMSTATISTICSPAGE);
@@ -8,18 +16,23 @@ describe('Item Statistics Page', () => {
cy.get('ds-item-page').should('not.exist');
});
it('should contain the item statistics page url when navigating to an item statistics page', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
});
it('should contain a "Total visits" section', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('.' + ITEMUUID + '_TotalVisits').should('exist');
cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisits').should('exist');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('.' + ITEMUUID + '_TotalVisitsPerMonth').should('exist');
cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisitsPerMonth').should('exist');
});
it('should pass accessibility tests', () => {
cy.visit(ITEMSTATISTICSPAGE);
// <ds-item-statistics-page> tag must be loaded
cy.get('ds-item-statistics-page').should('exist');
// Analyze <ds-item-statistics-page> for accessibility issues
testA11y('ds-item-statistics-page');
});
});

View File

@@ -1,3 +1,6 @@
import { Options } from 'cypress-axe';
import { testA11y } from 'cypress/support/utils';
describe('Search Page', () => {
// unique ID of the search form (for selecting specific elements below)
const SEARCHFORM_ID = '#search-form';
@@ -16,4 +19,54 @@ describe('Search Page', () => {
cy.get(SEARCHFORM_ID + ' button.search-button').click();
cy.url().should('include', 'query=' + encodeURI(queryString));
});
it('should pass accessibility tests', () => {
cy.visit('/search');
// <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('exist');
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
cy.get('.filter-toggle').click({ multiple: true });
// Analyze <ds-search-page> for accessibility issues
testA11y(
{
include: ['ds-search-page'],
exclude: [
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
],
},
{
rules: {
// Search filters fail these two "moderate" impact rules
'heading-order': { enabled: false },
'landmark-unique': { enabled: false }
}
} as Options
);
});
it('should pass accessibility tests in Grid view', () => {
cy.visit('/search');
// Click to display grid view
// TODO: These buttons should likely have an easier way to uniquely select
cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?spc.sf=score&spc.sd=DESC&view=grid"] > .fas').click();
// <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('exist');
// Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page',
{
rules: {
// Search filters fail these two "moderate" impact rules
'heading-order': { enabled: false },
'landmark-unique': { enabled: false }
}
} as Options
);
});
});

View File

@@ -1,5 +1,16 @@
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
// For more info, visit https://on.cypress.io/plugins-api
/* tslint:disable:no-empty */
module.exports = (on, config) => { };
/* tslint:enable:no-empty */
module.exports = (on, config) => {
// Define "log" and "table" tasks, used for logging accessibility errors during CI
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
on('task', {
log(message: string) {
console.log(message);
return null;
},
table(message: string) {
console.table(message);
return null;
}
});
};

View File

@@ -19,3 +19,8 @@
// Import Cypress Axe tools for all tests
// https://github.com/component-driven/cypress-axe
import 'cypress-axe';
// Global constants used in tests
export const TEST_COLLECTION = '282164f5-d325-4740-8dd1-fa4d6d3e7200';
export const TEST_COMMUNITY = '0958c910-2037-42a9-81c7-dca80e3892b4';
export const TEST_ENTITY_PUBLICATION = 'e98b0f27-5c19-49a0-960d-eb6ad5287067';

44
cypress/support/utils.ts Normal file
View File

@@ -0,0 +1,44 @@
import { Result } from 'axe-core';
import { Options } from 'cypress-axe';
// Log violations to terminal/commandline in a table format.
// Uses 'log' and 'table' tasks defined in ../plugins/index.ts
// Borrowed from https://github.com/component-driven/cypress-axe#in-your-spec-file
function terminalLog(violations: Result[]) {
cy.task(
'log',
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`
);
// pluck specific keys to keep the table readable
const violationData = violations.map(
({ id, impact, description, helpUrl, nodes }) => ({
id,
impact,
description,
helpUrl,
nodes: nodes.length,
html: nodes.map(node => node.html)
})
);
// Print violations as an array, since 'node.html' above often breaks table alignment
cy.task('log', violationData);
// Optionally, uncomment to print as a table
// cy.task('table', violationData);
}
// Custom "testA11y()" method which checks accessibility using cypress-axe
// while also ensuring any violations are logged to the terminal (see terminalLog above)
// This method MUST be called after cy.visit(), as cy.injectAxe() must be called after page load
export const testA11y = (context?: any, options?: Options) => {
cy.injectAxe();
cy.configureAxe({
rules: [
// Disable color contrast checks as they are inaccurate / result in a lot of false positives
// See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast
{ id: 'color-contrast', enabled: false },
]
});
cy.checkA11y(context, options, terminalLog);
};

View File

@@ -6,7 +6,8 @@
"compilerOptions": {
"types": [
"cypress",
"cypress-axe"
"cypress-axe",
"node"
]
}
}

View File

@@ -152,7 +152,7 @@
"copy-webpack-plugin": "^6.4.1",
"css-loader": "3.4.0",
"cssnano": "^4.1.10",
"cypress": "8.3.1",
"cypress": "8.6.0",
"cypress-axe": "^0.13.0",
"deep-freeze": "0.0.1",
"dotenv": "^8.2.0",

View File

@@ -1,79 +1,80 @@
<div [class.form-group]="(model.type !== 'GROUP' && asBootstrapFormGroup) || getClass('element', 'container').includes('form-group')"
[formGroup]="group"
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
<label *ngIf="!isCheckbox && hasLabel"
[id]="'label_' + model.id"
[for]="id"
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: model"></ng-container>
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->
<div [ngClass]="{'form-row': model.hasLanguages || isRelationship,
<label *ngIf="!isCheckbox && hasLabel"
[id]="'label_' + model.id"
[for]="id"
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: model"></ng-container>
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->
<div [ngClass]="{'form-row': model.hasLanguages || isRelationship,
'd-none': value?.isVirtual && (model.hasSelectableMetadata || context?.index > 0)}">
<div [ngClass]="getClass('grid', 'control')">
<ng-container #componentViewContainer></ng-container>
<small *ngIf="hasHint && ((model.repeatable === false && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<!-- In case of repeatable fields show empty space for all elements except the first -->
<div *ngIf="context?.index !== null
<div [ngClass]="getClass('grid', 'control')">
<div>
<ng-container #componentViewContainer></ng-container>
</div>
<small *ngIf="hasHint && ((model.repeatable === false && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<!-- In case of repeatable fields show empty space for all elements except the first -->
<div *ngIf="context?.index !== null
&& (!showErrorMessages || errorMessages.length === 0)" class="clearfix w-100 mb-2"></div>
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
</div>
<div *ngIf="showErrorMessages" [id]="id + '_errors'"
[ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
</div>
</div>
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2" >
<select
#language="ngModel"
[disabled]="model.readOnly"
[(ngModel)]="model.language"
class="form-control"
(blur)="onBlur($event)"
(change)="onChangeLanguage($event)"
[ngModelOptions]="{standalone: true}"
required>
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
</select>
</div>
<div *ngIf="isRelationship" class="col-auto text-center">
<button class="btn btn-secondary"
type="button"
ngbTooltip="{{'form.lookup-help' | translate}}"
placement="top"
(click)="openLookup(); $event.stopPropagation();"><i class="fa fa-search"></i>
</button>
</div>
</div>
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: model"></ng-container>
<ng-container *ngIf="value?.isVirtual">
<ds-existing-metadata-list-element
*ngIf="model.hasSelectableMetadata"
[reoRel]="relationshipValue$ | async"
[submissionItem]="item$ | async"
[listId]="listId"
[metadataFields]="model.metadataFields"
[submissionId]="model.submissionId"
[relationshipOptions]="model.relationship"
(remove)="onRemove()"
>
</ds-existing-metadata-list-element>
<ds-existing-relation-list-element
*ngIf="!model.hasSelectableMetadata"
[ngClass]="{'d-block pb-2 pt-2': !context?.index}"
[reoRel]="relationshipValue$ | async"
[submissionItem]="item$ | async"
[listId]="listId"
[metadataFields]="model.metadataFields"
[submissionId]="model.submissionId"
[relationshipOptions]="model.relationship"
>
</ds-existing-relation-list-element>
<small *ngIf="hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<div class="clearfix w-100 mb-2"></div>
</ng-container>
<ng-content></ng-content>
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2" >
<select
#language="ngModel"
[disabled]="model.readOnly"
[(ngModel)]="model.language"
class="form-control"
(blur)="onBlur($event)"
(change)="onChangeLanguage($event)"
[ngModelOptions]="{standalone: true}"
required>
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
</select>
</div>
<div *ngIf="isRelationship" class="col-auto text-center">
<button class="btn btn-secondary"
type="button"
ngbTooltip="{{'form.lookup-help' | translate}}"
placement="top"
(click)="openLookup(); $event.stopPropagation();"><i class="fa fa-search"></i>
</button>
</div>
</div>
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: model"></ng-container>
<ng-container *ngIf="value?.isVirtual">
<ds-existing-metadata-list-element
*ngIf="model.hasSelectableMetadata"
[reoRel]="relationshipValue$ | async"
[submissionItem]="item$ | async"
[listId]="listId"
[metadataFields]="model.metadataFields"
[submissionId]="model.submissionId"
[relationshipOptions]="model.relationship"
(remove)="onRemove()"
>
</ds-existing-metadata-list-element>
<ds-existing-relation-list-element
*ngIf="!model.hasSelectableMetadata"
[ngClass]="{'d-block pb-2 pt-2': !context?.index}"
[reoRel]="relationshipValue$ | async"
[submissionItem]="item$ | async"
[listId]="listId"
[metadataFields]="model.metadataFields"
[submissionId]="model.submissionId"
[relationshipOptions]="model.relationship"
>
</ds-existing-relation-list-element>
<small *ngIf="hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<div class="clearfix w-100 mb-2"></div>
</ng-container>
<ng-content></ng-content>
</div>

View File

@@ -1,50 +1,54 @@
<div class="d-flex">
<ds-number-picker
tabindex="1"
[disabled]="model.disabled"
[min]="minYear"
[max]="maxYear"
[name]="'year'"
[size]="4"
[(ngModel)]="initialYear"
[value]="year"
[invalid]="showErrorMessages"
[placeholder]='yearPlaceholder'
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<div>
<fieldset class="d-flex">
<legend [id]="'legend_' + model.id" [ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]">
{{model.placeholder}} <span *ngIf="model.required">*</span>
</legend>
<ds-number-picker
tabindex="1"
[disabled]="model.disabled"
[min]="minYear"
[max]="maxYear"
[name]="'year'"
[size]="4"
[(ngModel)]="initialYear"
[value]="year"
[invalid]="showErrorMessages"
[placeholder]='yearPlaceholder'
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<ds-number-picker
tabindex="2"
[min]="minMonth"
[max]="maxMonth"
[name]="'month'"
[size]="6"
[(ngModel)]="initialMonth"
[value]="month"
[placeholder]="monthPlaceholder"
[disabled]="!year || model.disabled"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<ds-number-picker
tabindex="3"
[min]="minDay"
[max]="maxDay"
[name]="'day'"
[size]="2"
[(ngModel)]="initialDay"
[value]="day"
[placeholder]="dayPlaceholder"
[disabled]="!month || model.disabled"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<ds-number-picker
tabindex="2"
[min]="minMonth"
[max]="maxMonth"
[name]="'month'"
[size]="6"
[(ngModel)]="initialMonth"
[value]="month"
[placeholder]="monthPlaceholder"
[disabled]="!year || model.disabled"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<ds-number-picker
tabindex="3"
[min]="minDay"
[max]="maxDay"
[name]="'day'"
[size]="2"
[(ngModel)]="initialDay"
[value]="day"
[placeholder]="dayPlaceholder"
[disabled]="!month || model.disabled"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
</fieldset>
</div>
<div class="clearfix"></div>

View File

@@ -1,3 +1,7 @@
.col-lg-1 {
width: auto;
}
legend {
font-size: initial;
}

View File

@@ -69,6 +69,7 @@ describe('DsDatePickerComponent test suite', () => {
[bindId]='bindId'
[group]='group'
[model]='model'
[legend]='legend'
(blur)='onBlur($event)'
(change)='onValueChange($event)'
(focus)='onFocus($event)'></ds-date-picker>`;

View File

@@ -20,6 +20,7 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicDsDatePickerModel;
@Input() legend: string;
@Output() selected = new EventEmitter<number>();
@Output() remove = new EventEmitter<number>();

View File

@@ -1,24 +1,30 @@
import {
DynamicDateControlModel,
DynamicDateControlModelConfig,
DynamicDatePickerModelConfig,
DynamicFormControlLayout,
serializable
} from '@ng-dynamic-forms/core';
export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DATE';
export interface DynamicDsDateControlModelConfig extends DynamicDatePickerModelConfig {
legend?: string;
}
/**
* Dynamic Date Picker Model class
*/
export class DynamicDsDatePickerModel extends DynamicDateControlModel {
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER;
malformedDate: boolean;
legend: string;
hasLanguages = false;
repeatable = false;
constructor(config: DynamicDateControlModelConfig, layout?: DynamicFormControlLayout) {
constructor(config: DynamicDsDateControlModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.malformedDate = false;
this.legend = config.legend;
}
}

View File

@@ -1,8 +1,13 @@
<div #sdRef="ngbDropdown" ngbDropdown display="dynamic" placement="bottom-right" class="w-100">
<div class="position-relative right-addon">
<div class="position-relative right-addon"
role="combobox"
[attr.aria-label]="model.label"
[attr.aria-owns]="'combobox_' + id + '_listbox'">
<i ngbDropdownToggle class="position-absolute scrollable-dropdown-toggle"
aria-hidden="true"></i>
<input class="form-control"
[attr.aria-controls]="'combobox_' + id + '_listbox'"
[attr.aria-activedescendant]="'combobox_' + id + '_selected'"
[attr.aria-label]="model.placeholder"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
@@ -24,6 +29,8 @@
aria-expanded="false"
[attr.aria-label]="model.placeholder">
<div class="scrollable-menu"
role="listbox"
[id]="'combobox_' + id + '_listbox'"
[attr.aria-label]="model.placeholder"
infiniteScroll
[infiniteScrollDistance]="2"
@@ -32,7 +39,10 @@
[scrollWindow]="false">
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList" (click)="onSelect(listEntry); sdRef.close()" title="{{ listEntry.display }}">
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList"
(click)="onSelect(listEntry); sdRef.close()"
title="{{ listEntry.display }}" role="option"
[attr.id]="listEntry.display == (currentValue|async) ? ('combobox_' + id + '_selected') : null">
{{inputFormatter(listEntry)}}
</button>
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
@@ -40,5 +50,3 @@
</div>
</div>

View File

@@ -1,6 +1,8 @@
import { FieldParser } from './field-parser';
import { DynamicDatePickerModelConfig } from '@ng-dynamic-forms/core';
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
import {
DynamicDsDateControlModelConfig,
DynamicDsDatePickerModel
} from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
import { isNotEmpty } from '../../../empty.util';
import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/date-picker/date-picker.component';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
@@ -9,7 +11,8 @@ export class DateFieldParser extends FieldParser {
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
let malformedDate = false;
const inputDateModelConfig: DynamicDatePickerModelConfig = this.initModel(null, label);
const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, false, true);
inputDateModelConfig.legend = this.configData.label;
inputDateModelConfig.toggleIcon = 'fas fa-calendar';
this.setValues(inputDateModelConfig as any, fieldValue);

View File

@@ -1,6 +1,6 @@
<div class="d-flex flex-column align-items-center justify-content-around mr-3">
<button
class="btn btn-link"
class="btn btn-link-focus"
type="button"
tabindex="0"
[disabled]="disabled"
@@ -25,7 +25,7 @@
aria-label="name"
>
<button
class="btn btn-link"
class="btn btn-link-focus"
type="button"
tabindex="0"
[disabled]="disabled"

View File

@@ -23,3 +23,24 @@
input {
max-width: 80px !important;
}
.btn-link-focus {
// behave as btn-link but does not override box-shadow of btn-link:focus
font-weight: $font-weight-normal;
color: $link-color;
text-decoration: $link-decoration;
@include hover {
color: $link-hover-color;
text-decoration: $link-hover-decoration;
}
&:disabled,
&.disabled {
color: $btn-link-disabled-color;
pointer-events: none;
}
&:focus,
&.focus {
text-decoration: $link-hover-decoration;
}
}

View File

@@ -1,5 +1,6 @@
export const mockDynamicFormLayoutService = jasmine.createSpyObj('DynamicFormLayoutService', {
getElementId: jasmine.createSpy('getElementId')
getElementId: jasmine.createSpy('getElementId'),
getClass: 'class',
});
export const mockDynamicFormValidationService = jasmine.createSpyObj('DynamicFormValidationService', {

View File

@@ -19,11 +19,14 @@
(fileOver)="fileOverBase($event)"
class="well ds-base-drop-zone mt-1 mb-3 text-muted">
<div class="text-center m-0 p-2 d-flex justify-content-center align-items-center" *ngIf="uploader?.queue?.length === 0">
<span><i class="fas fa-upload" aria-hidden="true"></i> {{dropMsg | translate}} {{'uploader.or' | translate}}</span>
<label for="inputFileUploader" class="btn btn-link m-0 p-0 ml-1" tabindex="0" (keyup.enter)="$event.stopImmediatePropagation(); fileInput.click()">
<input #fileInput id="inputFileUploader" class="d-none" type="file" role="button" ng2FileSelect [uploader]="uploader" multiple tabindex="0" />
{{'uploader.browse' | translate}}
</label>
<span>
<i class="fas fa-upload" aria-hidden="true"></i>
{{dropMsg | translate}}{{'uploader.or' | translate}}
<label for="inputFileUploader" class="btn btn-link m-0 p-0 ml-1" tabindex="0" (keyup.enter)="$event.stopImmediatePropagation(); fileInput.click()">
<span role="button" [attr.aria-label]="'uploader.browse' | translate">{{'uploader.browse' | translate}}</span>
</label>
<input #fileInput id="inputFileUploader" class="d-none" type="file" ng2FileSelect [uploader]="uploader" multiple tabindex="0" />
</span>
</div>
<div *ngIf="(isOverBaseDropZone | async) || uploader?.queue?.length !== 0">
<div class="m-1">

View File

@@ -17,3 +17,23 @@
z-index: var(--ds-submission-footer-z-index);
}
.btn-link-focus {
// behave as btn-link but does not override box-shadow of btn-link:focus
font-weight: $font-weight-normal;
color: $link-color;
text-decoration: $link-decoration;
@include hover {
color: $link-hover-color;
text-decoration: $link-hover-decoration;
}
&:disabled,
&.disabled {
color: $btn-link-disabled-color;
pointer-events: none;
}
&:focus,
&.focus {
text-decoration: $link-hover-decoration;
}
}

View File

@@ -15,15 +15,15 @@
<span class="float-left section-title" tabindex="0">{{ 'submission.sections.'+sectionData.header | translate }}</span>
<div class="d-inline-block float-right">
<i *ngIf="!(sectionRef.isValid() | async) && !(sectionRef.hasErrors())" class="fas fa-exclamation-circle text-warning mr-3"
aria-hidden="true" title="{{'submission.sections.status.warnings.title' | translate}}"></i>
title="{{'submission.sections.status.warnings.title' | translate}}" role="img" [attr.aria-label]="'submission.sections.status.warnings.aria' | translate"></i>
<i *ngIf="(sectionRef.hasErrors())" class="fas fa-exclamation-circle text-danger mr-3"
aria-hidden="true" title="{{'submission.sections.status.errors.title' | translate}}"></i>
title="{{'submission.sections.status.errors.title' | translate}}" role="img" [attr.aria-label]="'submission.sections.status.errors.aria' | translate"></i>
<i *ngIf="(sectionRef.isValid() | async) && !(sectionRef.hasErrors())" class="fas fa-check-circle text-success mr-3"
aria-hidden="true" title="{{'submission.sections.status.valid.title' | translate}}"></i>
title="{{'submission.sections.status.valid.title' | translate}}" role="img" [attr.aria-label]="'submission.sections.status.valid.aria' | translate"></i>
<a class="close"
tabindex="0"
role="button"
[attr.aria-label]="(sectionRef.isOpen() ? 'submission.sections.toggle.close' : 'submission.sections.toggle.open') | translate"
[attr.aria-label]="(sectionRef.isOpen() ? 'submission.sections.toggle.aria.close' : 'submission.sections.toggle.aria.open') | translate: {sectionHeader: ('submission.sections.'+sectionData.header | translate)}"
[title]="(sectionRef.isOpen() ? 'submission.sections.toggle.close' : 'submission.sections.toggle.open') | translate">
<span *ngIf="sectionRef.isOpen()" class="fas fa-chevron-up fa-fw"></span>
<span *ngIf="!sectionRef.isOpen()" class="fas fa-chevron-down fa-fw"></span>

View File

@@ -74,7 +74,7 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePicke
required: null
},
errorMessages: {
required: 'submission.sections.upload.form.date-required'
required: 'submission.sections.upload.form.date-required-from'
}
};
export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = {
@@ -104,7 +104,7 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerM
required: null
},
errorMessages: {
required: 'submission.sections.upload.form.date-required'
required: 'submission.sections.upload.form.date-required-until'
}
};
export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = {

View File

@@ -10,16 +10,16 @@
</div>
<div class="float-right w-15" [class.sticky-buttons]="!readMode">
<ng-container *ngIf="readMode">
<ds-file-download-link [cssClasses]="'btn btn-link'" [isBlank]="true" [bitstream]="getBitstream()">
<ds-file-download-link [cssClasses]="'btn btn-link-focus'" [isBlank]="true" [bitstream]="getBitstream()">
<i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i>
</ds-file-download-link>
<button class="btn btn-link"
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.edit.title' | translate"
title="{{ 'submission.sections.upload.edit.title' | translate }}"
(click)="$event.preventDefault();switchMode();">
<i class="fa fa-edit fa-2x text-normal"></i>
</button>
<button class="btn btn-link"
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.delete.confirm.title' | translate"
title="{{ 'submission.sections.upload.delete.confirm.title' | translate }}"
[disabled]="(processingDelete$ | async)"
@@ -29,17 +29,17 @@
</button>
</ng-container>
<ng-container *ngIf="!readMode">
<button class="btn btn-link"
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.save-metadata' | translate"
title="{{ 'submission.sections.upload.save-metadata' | translate }}"
(click)="saveBitstreamData($event);">
<i class="fa fa-save fa-2x text-success"></i>
</button>
<button class="btn btn-link"
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.undo' | translate"
title="{{ 'submission.sections.upload.undo' | translate }}"
(click)="$event.preventDefault();switchMode();"><i class="fa fa-ban fa-2x text-warning"></i></button>
<button class="btn btn-link"
<button class="btn btn-link-focus"
[attr.aria-label]="'submission.sections.upload.delete.confirm.title' | translate"
title="{{ 'submission.sections.upload.delete.confirm.title' | translate }}"
[disabled]="(processingDelete$ | async)"

View File

@@ -3678,10 +3678,20 @@
"submission.sections.status.warnings.title": "Warnings",
"submission.sections.status.errors.aria": "has errors",
"submission.sections.status.valid.aria": "is valid",
"submission.sections.status.warnings.aria": "has warnings",
"submission.sections.toggle.open": "Open section",
"submission.sections.toggle.close": "Close section",
"submission.sections.toggle.aria.open": "Expand {{sectionHeader}} section",
"submission.sections.toggle.aria.close": "Collapse {{sectionHeader}} section",
"submission.sections.upload.delete.confirm.cancel": "Cancel",
"submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?",
@@ -3702,6 +3712,10 @@
"submission.sections.upload.form.date-required": "Date is required.",
"submission.sections.upload.form.date-required-from": "Grant access from date is required.",
"submission.sections.upload.form.date-required-until": "Grant access until date is required.",
"submission.sections.upload.form.from-label": "Grant access from",
"submission.sections.upload.form.from-placeholder": "From",

File diff suppressed because it is too large Load Diff

View File

@@ -158,25 +158,17 @@ export const environment: GlobalConfig = {
code: 'en',
label: 'English',
active: true,
}, {
code: 'de',
label: 'Deutsch',
active: true,
}, {
code: 'cs',
label: 'Čeština',
active: true,
}, {
code: 'nl',
label: 'Nederlands',
code: 'de',
label: 'Deutsch',
active: true,
},{
code: 'pt-BR',
label: 'Português do Brasil',
active: true,
},{
code: 'pt-PT',
label: 'Português',
}, {
code: 'es',
label: 'Español',
active: true,
}, {
code: 'fr',
@@ -186,14 +178,26 @@ export const environment: GlobalConfig = {
code: 'lv',
label: 'Latviešu',
active: true,
}, {
code: 'hu',
label: 'Magyar',
active: true,
}, {
code: 'nl',
label: 'Nederlands',
active: true,
}, {
code: 'pt-PT',
label: 'Português',
active: true,
},{
code: 'pt-BR',
label: 'Português do Brasil',
active: true,
},{
code: 'fi',
label: 'Suomi',
active: true,
},{
code: 'hu',
label: 'magyar',
active: true,
}],
// Browse-By Pages
browseBy: {

View File

@@ -4153,10 +4153,10 @@ cypress-axe@^0.13.0:
resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-0.13.0.tgz#3234e1a79a27701f2451fcf2f333eb74204c7966"
integrity sha512-fCIy7RiDCm7t30U3C99gGwQrUO307EYE1QqXNaf9ToK4DVqW8y5on+0a/kUHMrHdlls2rENF6TN9ZPpPpwLrnw==
cypress@8.3.1:
version "8.3.1"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.3.1.tgz#c6760dbb907df2570b0e1ac235fa31c30f9260a6"
integrity sha512-1v6pfx+/5cXhaT5T6QKOvnkawmEHWHLiVzm3MYMoQN1fkX2Ma1C32STd3jBStE9qT5qPSTILjGzypVRxCBi40g==
cypress@8.6.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.6.0.tgz#8d02fa58878b37cfc45bbfce393aa974fa8a8e22"
integrity sha512-F7qEK/6Go5FsqTueR+0wEw2vOVKNgk5847Mys8vsWkzPoEKdxs+7N9Y1dit+zhaZCLtMPyrMwjfA53ZFy+lSww==
dependencies:
"@cypress/request" "^2.88.6"
"@cypress/xvfb" "^1.2.4"
@@ -4192,6 +4192,7 @@ cypress@8.3.1:
minimist "^1.2.5"
ospath "^1.2.2"
pretty-bytes "^5.6.0"
proxy-from-env "1.0.0"
ramda "~0.27.1"
request-progress "^3.0.0"
supports-color "^8.1.1"
@@ -9839,6 +9840,11 @@ proxy-addr@~2.0.5:
forwarded "~0.1.2"
ipaddr.js "1.9.1"
proxy-from-env@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"