Skip to content

Commit

Permalink
Showing 10 changed files with 80 additions and 58 deletions.
1 change: 1 addition & 0 deletions backend/src/main/resources/i18n/messages.properties
@@ -468,6 +468,7 @@ message.unsaved-source-2=icon on the dashboard.
message.service-resolver-name-required=Service Provider Name is required
message.entity-id-required=Entity ID is required
message.entity-id-must-be-unique=Entity ID must be unique
message.target-required=Entity ID to copy is required
message.file-upload-alert=Note: You can only import a file with a single entityID (EntityDescriptor element) in it. Anything more in that file will result in an error.
message.add-new-md-resolver=Add a new metadata source
message.wizard-status=Step { index } of { length }
1 change: 1 addition & 0 deletions backend/src/main/resources/i18n/messages_en.properties
@@ -432,6 +432,7 @@ message.unsaved-source-2=icon on the dashboard.
message.service-resolver-name-required=Service Provider Name is required
message.entity-id-required=Entity ID is required
message.entity-id-must-be-unique=Entity ID must be unique
message.target-required=Entity ID to copy is required
message.file-upload-alert=Note: You can only import a file with a single entityID (EntityDescriptor element) in it. Anything more in that file will result in an error.
message.add-new-md-resolver=Add a new metadata source
message.wizard-status=Step { index } of { length }
2 changes: 1 addition & 1 deletion backend/src/main/resources/metadata-sources-ui-schema.json
@@ -269,7 +269,7 @@
"title": "label.contact-email-address",
"description": "tooltip.contact-email",
"type": "string",
"pattern": "^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$",
"pattern": "^(mailto:)?(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$",
"minLength": 1,
"maxLength": 255
}
@@ -21,8 +21,8 @@ <h2 class="mb-4" [ngSwitch]="type$ | async">
</button>
</div>
<div *ngIf="hasXml$ | async">
<div class="btn-group">
<a class="btn" routerLink="../options" routerLinkActive="btn-primary">
<div class="btn-group" role="group" aria-label="Options selected">
<a class="btn" routerLink="../options" routerLinkActive="btn-primary" aria-pressed="true">
<span class="sr-only" translate="action.toggle-view">Toggle view:</span>
Options
</a>
@@ -5,12 +5,12 @@ <h2 class="mb-4" [ngSwitch]="type$ | async">
</h2>
<div class="container-fluid">
<div class="px-3 my-3 d-flex justify-content-end">
<div class="btn-group">
<div class="btn-group" role="group" aria-label="XML selected">
<a class="btn" routerLink="../options" routerLinkActive="btn-primary">
<span class="sr-only" translate="action.toggle-view">Toggle view:</span>
Options
</a>
<a class="btn" routerLink="../xml" routerLinkActive="btn-primary">
<a class="btn" routerLink="../xml" routerLinkActive="btn-primary" aria-pressed="true">
<span class="sr-only" translate="action.toggle-view">Toggle view:</span>
XML
</a>
47 changes: 28 additions & 19 deletions ui/src/app/metadata/resolver/container/copy-resolver.component.html
@@ -38,39 +38,48 @@ <h3 class="tag tag-primary">
[required]="true"
role="textbox"
i18n-aria-label="@@label.select-entity-id-to-copy"
aria-label="Select the Entity ID to copy">
aria-label="Select the Entity ID to copy"
aria-describedby="target-help">
</auto-complete>
<ng-container *ngIf="providerForm.get('serviceProviderName').touched && providerForm.get('serviceProviderName').invalid">
<small class="form-text text-danger" *ngIf="providerForm.get('serviceProviderName').hasError('required')">
<translate-i18n key="message.service-resolver-name-required">Service Resolver Name is Required</translate-i18n>
</small>
</ng-container>
<small id="target-help"
class="form-text text-danger"
[ngClass]="{'sr-only': !(providerForm.get('target').touched && providerForm.get('target').invalid)}"
*ngIf="providerForm.get('target').hasError('required')">
<translate-i18n key="message.target-required">Entity ID to copy is Required</translate-i18n>
</small>
</div>
<div class="form-group">
<label for="serviceProviderName">
<translate-i18n key="label.metadata-source-name-dashboard-display-only">Metadata Source Name (Dashboard Display Only)</translate-i18n>
<i class="fa fa-fw fa-asterisk text-danger" aria-hidden="true"></i>
</label>
<input id="serviceProviderName" type="text" class="form-control" placeholder="" formControlName="serviceProviderName" />
<ng-container *ngIf="providerForm.get('serviceProviderName').touched && providerForm.get('serviceProviderName').invalid">
<small class="form-text text-danger" *ngIf="providerForm.get('serviceProviderName').hasError('required')">
<translate-i18n key="message.service-resolver-name-required">Service Resolver Name is required</translate-i18n>
</small>
</ng-container>
<input id="serviceProviderName" type="text" class="form-control" placeholder="" formControlName="serviceProviderName" aria-describedby="serviceProviderName-help" />
<small class="form-text text-danger"
[ngClass]="{'sr-only': !(providerForm.get('serviceProviderName').touched && providerForm.get('serviceProviderName').invalid)}"
*ngIf="providerForm.get('serviceProviderName').hasError('required')"
id="serviceProviderName-help">
<translate-i18n key="message.service-resolver-name-required">Service Resolver Name is required</translate-i18n>
</small>
</div>
<div class="form-group">
<label for="entityId">
<translate-i18n key="label.service-resolver-entity-id">New Entity ID</translate-i18n>
<i class="fa fa-fw fa-asterisk text-danger" aria-hidden="true"></i>
</label>
<input id="entityId" type="text" class="form-control" placeholder="" formControlName="entityId" />
<ng-container *ngIf="providerForm.get('entityId').touched && providerForm.get('entityId').invalid">
<small class="form-text text-danger" *ngIf="providerForm.get('entityId').hasError('required')">
<input id="entityId" type="text"
class="form-control"
placeholder=""
formControlName="entityId"
aria-describedby="entityId-help"/>
<small class="form-text text-danger"
id="entityId-help"
[ngClass]="{'sr-only': !(providerForm.get('entityId').touched && providerForm.get('entityId').invalid)}">
<ng-container *ngIf="providerForm.get('entityId').hasError('required')">
<translate-i18n key="message.entity-id-required">Entity ID is required</translate-i18n>
</small>
</ng-container>
<small class="form-text text-danger" *ngIf="providerForm.get('entityId').hasError('unique')">
<translate-i18n key="message.entity-id-must-be-unique">Entity ID must be unique</translate-i18n>
</ng-container>
<ng-container *ngIf="providerForm.get('entityId').hasError('unique')">
<translate-i18n key="message.entity-id-must-be-unique">Entity ID must be unique</translate-i18n>
</ng-container>
</small>
</div>
<button type="button"
@@ -52,7 +52,8 @@
class="component-control"
limit="10"
[formControl]="search"
[matches]="ids"
[matches]="ids$ | async"
[count]="idCount$ | async"
(keydown.enter)="onSelectValue(search.value)">
</auto-complete>
<small [class.text-danger]="search.invalid && search.touched" translate="message.entity-id-min-unique">
@@ -3,7 +3,7 @@ import { FormControl, Validators, AbstractControl, ValidatorFn } from '@angular/
import { ObjectWidget } from 'ngx-schema-form';
import { Store } from '@ngrx/store';
import { Observable, Subject } from 'rxjs';
import { distinctUntilChanged, skipWhile, takeUntil, map } from 'rxjs/operators';
import { distinctUntilChanged, skipWhile, takeUntil, map, withLatestFrom, filter, switchMap, startWith } from 'rxjs/operators';

import * as fromRoot from '../../../app.reducer';
import * as fromFilters from '../../../metadata/filter/reducer';
@@ -19,7 +19,7 @@ import { QueryEntityIds, ClearSearch } from '../../../metadata/filter/action/sea
export class FilterTargetComponent extends ObjectWidget implements OnDestroy, AfterViewInit {
private ngUnsubscribe: Subject<null> = new Subject<null>();
ids$: Observable<string[]>;
ids: string[];
idCount$: Observable<number>;

search: FormControl = new FormControl(
'',
@@ -40,17 +40,22 @@ export class FilterTargetComponent extends ObjectWidget implements OnDestroy, Af
) {
super();
this.ids$ = this.store.select(fromFilters.getEntityCollection);
this.ids$.subscribe(ids => {
this.ids = [...ids];
});

this.idCount$ = this.ids$.pipe(map(list => list.length));

this.search
.valueChanges
.pipe(
takeUntil(this.ngUnsubscribe),
distinctUntilChanged()
distinctUntilChanged(),
withLatestFrom(this.ids$),
filter(([term, ids]) => term && term.length >= 4 && ids.indexOf(term) < 0),
map(([term]) => new QueryEntityIds({
term,
limit: 10
}))
)
.subscribe(query => this.searchEntityIds(query));
.subscribe(action => this.store.dispatch(action));

this.script
.valueChanges
@@ -101,15 +106,6 @@ export class FilterTargetComponent extends ObjectWidget implements OnDestroy, Af
};
}

searchEntityIds(term: string): void {
if (term && term.length >= 4 && this.ids.indexOf(term) < 0) {
this.store.dispatch(new QueryEntityIds({
term,
limit: 10
}));
}
}

getButtonConfig(id: string): any {
let buttons = this.formProperty.getProperty('value').schema.buttons;
return (buttons || []).map(btn => ({
26 changes: 17 additions & 9 deletions ui/src/app/shared/autocomplete/autocomplete.component.html
@@ -6,32 +6,38 @@
[id]="fieldId"
class="form-control"
autocomplete="off"
autocorrect="off"
aria-autocomplete="list"
role="combobox"
type="text"
[placeholder]="placeholder"
[attr.aria-activedescendant]="activeDescendant"
aria-autocomplete="both"
[attr.aria-owns]="fieldId + '__listbox'"
[attr.aria-expanded]="(menuIsVisible$ | async) ? 'true' : 'false'"
[attr.aria-haspopup]="fieldId + '__listbox'"
(focus)="handleInputFocus()"
(blur)="handleInputBlur()"
/>
<div class="input-group-append" *ngIf="dropdown">
<button class="btn btn-outline-secondary"
type="button"
role="button"
aria-haspopup="true"
[attr.aria-expanded]="state.currentState.menuOpen"
(click)="state.setState({menuOpen: !state.currentState.menuOpen})">
[attr.aria-activedescendant]="activeDescendant"
[attr.aria-owns]="fieldId + '__listbox'"
[attr.aria-expanded]="(menuIsVisible$ | async) ? 'true' : 'false'"
(click)="handleDropdown($event)">
<i class="fa fa-caret-down"></i>
<span class="sr-only">Toggle Dropdown</span>
</button>
</div>
</div>
<ul class="dropdown-menu"
<div class="dropdown-menu"
[id]="fieldId + '__listbox'"
role="listbox"
[class.show]="(menuIsVisible$ | async) && !processing && !input.disabled">
<li *ngFor="let option of matches; let i = index;"
<button *ngFor="let option of matches; let i = index;"
#matchElement
class="dropdown-item"
[class.active]="state.currentState.selected === i"
@@ -42,12 +48,14 @@
(mousedown)="handleOptionMouseDown($event)"
(mouseenter)="handleOptionMouseEnter(i)"
(mouseout)="handleOptionMouseOut()"
tabindex="0"
role="option"
[attr.aria-selected]="state.currentState.focused === i"
[innerHTML]="option | highlight:this.input.value"></li>
<li class="dropdown-item dropdown-item-noresults"
role="option"
[attr.aria-label]="this.input.value"
[innerHTML]="option | highlight:this.input.value">
</button>
<p class="dropdown-item dropdown-item-noresults m-0"
*ngIf="matches && matches.length === 0 && !processing">
{{ noneFoundText }}
</li>
</ul>
</p>
</div>
22 changes: 14 additions & 8 deletions ui/src/app/shared/autocomplete/autocomplete.component.ts
@@ -51,6 +51,7 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte
@Input() processing = false;
@Input() dropdown = false;
@Input() placeholder = '';
@Input() count = null;

@Output() more: EventEmitter<any> = new EventEmitter<any>();
@Output() onChange: EventEmitter<string> = new EventEmitter<string>();
@@ -118,14 +119,16 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte
}

ngOnChanges(changes: SimpleChanges): void {
if (changes.matches && this.matches) {
if (changes.matches && this.matches && this.state.currentState.menuOpen) {
this.announceResults();
}
}

announceResults(): void {
const count = this.matches.length;
this.live.announce(count === 0 ? 'No results available' : `${count} result${count === 1 ? '' : 's'} available`);
this.live.announce(count === 0 ?
`${this.noneFoundText}` :
`${count} result${count === 1 ? '' : 's'} available`, 'polite', 5000);
}

writeValue(value: any): void {
@@ -157,6 +160,12 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte
});
}

handleDropdown($event: MouseEvent | KeyboardEvent | Event): void {
const open = this.state.currentState.menuOpen;
this.state.setState({menuOpen: !open});
this.handleOptionFocus(0);
}

handleViewMore($event: MouseEvent | KeyboardEvent | Event): void {
$event.preventDefault();
$event.stopPropagation();
@@ -216,7 +225,6 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte

handleInputChange(query: string): void {
query = query || '';

const queryEmpty = query.length === 0;
const autoselect = this.hasAutoselect;
const optionsAvailable = this.matches.length > 0;
@@ -227,8 +235,6 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte
selected: searchForOptions ? ((autoselect && optionsAvailable) ? 0 : -1) : null
});
this.propagateChange(query);

setTimeout(() => this.announceResults(), 250);
}

handleInputFocus(): void {
@@ -284,7 +290,6 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte
}

handleDownArrow(event: KeyboardEvent): void {
event.preventDefault();
let isNotAtBottom = this.state.currentState.selected !== this.matches.length - 1;
if (this.showMoreAvailable) {
isNotAtBottom = this.state.currentState.selected !== this.matches.length;
@@ -293,6 +298,7 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte
if (allowMoveDown) {
this.handleOptionFocus(this.state.currentState.selected + 1);
}
event.preventDefault();
}

handleSpace(event: KeyboardEvent): void {
@@ -349,8 +355,8 @@ export class AutoCompleteComponent implements OnInit, OnDestroy, OnChanges, Afte
return !!(agent.match(/(iPod|iPhone|iPad)/g) && agent.match(/AppleWebKit/g));
}

getOptionId(index): string {
return `${this.fieldId}__option--${index}`;
getOptionId(index: string | number): string {
return `${this.fieldId}__option--${index}`.replace('/', '');
}

get hasAutoselect(): boolean {

0 comments on commit cb49853

Please sign in to comment.