fix(console): LDAP UI optimization for better required field recognition, improve onboarding all done visibility (#5659)

* fix: onboarding all done styles

* ldap UI change

* rm log

* set required marker to formfield, max width of string-list comp

* seperate formfields

* formarray

* clear action

* validator

* hide pwd field

* rm dead code

* lint
This commit is contained in:
Max Peintner 2023-04-18 17:37:26 +02:00 committed by GitHub
parent 0ed2906b5d
commit c420de1533
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 277 additions and 197 deletions

View File

@ -1,5 +1,14 @@
<ng-template #labelTemplate>
<label class="cnsl-label-wrapper" [attr.for]="_control.id" [attr.aria-owns]="_control.id">
<ng-content select="cnsl-label"></ng-content>
<span *ngIf="_control.required && !hideRequiredMarker" aria-hidden="true" class="cnsl-form-field-required-marker"
>*</span
>
</label>
</ng-template>
<div class="cnsl-form-field-wrapper" (click)="_control.onContainerClick && _control.onContainerClick($event)">
<ng-content select="cnsl-label"></ng-content>
<ng-template [ngTemplateOutlet]="labelTemplate"></ng-template>
<div class="cnsl-rel" #inputContainer>
<ng-content></ng-content>
<ng-content select="cnslSuffix"></ng-content>

View File

@ -51,6 +51,7 @@ interface ValidationError {
'[class.ng-valid]': '_shouldForward("valid")',
'[class.ng-invalid]': '_shouldForward("invalid")',
'[class.ng-pending]': '_shouldForward("pending")',
'[class.ng-required]': '_control.required',
'[class.cnsl-form-field-disabled]': '_control.disabled',
'[class.cnsl-form-field-autofilled]': '_control.autofilled',
'[class.cnsl-focused]': '_control.focused',
@ -69,6 +70,7 @@ export class CnslFormFieldComponent extends CnslFormFieldBase implements OnDestr
@ContentChild(MatFormFieldControl) _controlNonStatic!: MatFormFieldControl<any>;
@ContentChild(MatFormFieldControl, { static: true }) _controlStatic!: MatFormFieldControl<any>;
@Input() public disableValidationErrors = false;
@Input() public hideRequiredMarker = false;
get _control(): MatFormFieldControl<any> {
return this._explicitFormFieldControl || this._controlNonStatic || this._controlStatic;

View File

@ -24,6 +24,12 @@ export function requiredValidator(c: AbstractControl): ValidationErrors | null {
return i18nErr(Validators.required(c), 'ERRORS.REQUIRED');
}
export function minArrayLengthValidator(minArrLength: number): ValidatorFn {
return (c: AbstractControl): ValidationErrors | null => {
return arrayLengthValidator(c, minArrLength, 'ERRORS.ATLEASTONE');
};
}
export function emailValidator(c: AbstractControl): ValidationErrors | null {
return i18nErr(Validators.email(c), 'ERRORS.NOTANEMAIL');
}
@ -56,6 +62,12 @@ function regexpValidator(c: AbstractControl, regexp: RegExp, i18nKey: string): V
return !c.value || regexp.test(c.value) ? null : i18nErr({ invalid: true }, i18nKey, { regexp: regexp });
}
function arrayLengthValidator(c: AbstractControl, length: number, i18nKey: string): ValidationErrors | null {
const arr: string[] = c.value;
const invalidStrings: string[] = arr.filter((val: string) => val.trim() === '');
return arr && invalidStrings.length === 0 && arr.length >= length ? null : i18nErr({ invalid: true }, i18nKey);
}
function i18nErr(err: ValidationErrors | null | undefined, i18nKey: string, params?: any): ValidationErrors | null {
if (err === null) {
return null;

View File

@ -9,23 +9,31 @@
$foreground: map-get($theme, foreground);
$secondary-text: map-get($foreground, secondary-text);
.cnsl-label {
display: block;
.cnsl-label-wrapper {
display: flex;
font-size: 12px;
color: $secondary-text;
transition: color 0.2s ease;
margin-bottom: 4px;
font-weight: 400;
.cnsl-label {
display: block;
}
.cnsl-form-field-required-marker {
margin-left: 1px;
}
}
.cnsl-form-field-disabled {
.cnsl-label {
.cnsl-label-wrapper {
color: if($is-dark-theme, #ffffff80, #00000061);
}
}
.cnsl-form-field-invalid {
.cnsl-label {
.cnsl-label-wrapper {
color: $warn-color;
}
}

View File

@ -198,6 +198,12 @@
.state-circle {
display: none;
}
.action-card {
.action-content {
opacity: 1;
}
}
}
}
}

View File

@ -1,54 +1,65 @@
<form [formGroup]="form" class="attribute-form">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.LDAPIDATTRIBUTE' | translate }}*</cnsl-label>
<input cnslInput formControlName="idAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.AVATARURLATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="avatarUrlAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.DISPLAYNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="displayNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.EMAILATTRIBUTEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="emailAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.EMAILVERIFIEDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="emailVerifiedAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.FIRSTNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="firstNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.LASTNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="lastNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NICKNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="nickNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PHONEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="phoneAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PHONEVERIFIEDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="phoneVerifiedAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PREFERREDLANGUAGEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="preferredLanguageAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PREFERREDUSERNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="preferredUsernameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PROFILEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="profileAttribute" />
<cnsl-label>{{ 'IDP.LDAPIDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="idAttribute" required />
</cnsl-form-field>
<div class="attribute-more-row">
<span>{{ 'ACTIONS.MORE' | translate }}</span>
<button (click)="showMore = !showMore" type="button" mat-icon-button>
<mat-icon *ngIf="showMore">keyboard_arrow_up</mat-icon>
<mat-icon *ngIf="!showMore">keyboard_arrow_down</mat-icon>
</button>
</div>
<ng-container *ngIf="showMore">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.AVATARURLATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="avatarUrlAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.DISPLAYNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="displayNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.EMAILATTRIBUTEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="emailAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.EMAILVERIFIEDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="emailVerifiedAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.FIRSTNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="firstNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.LASTNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="lastNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NICKNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="nickNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PHONEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="phoneAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PHONEVERIFIEDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="phoneVerifiedAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PREFERREDLANGUAGEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="preferredLanguageAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PREFERREDUSERNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="preferredUsernameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PROFILEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="profileAttribute" />
</cnsl-form-field>
</ng-container>
</form>

View File

@ -4,3 +4,8 @@
max-width: 400px;
padding-bottom: 1rem;
}
.attribute-more-row {
display: flex;
align-items: center;
}

View File

@ -29,6 +29,7 @@ export class LDAPAttributesComponent implements OnChanges, OnDestroy {
profileAttribute: new FormControl('', []),
});
public showMore: boolean = false;
constructor() {
this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
if (value) {

View File

@ -17,12 +17,13 @@
<div class="identity-provider-content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
<input cnslInput formControlName="name" required />
</cnsl-form-field>
<h2 class="subheader">{{ 'IDP.LDAPCONNECTION' | translate }}</h2>
<cnsl-string-list
class="string-list-component-wrapper"
title="{{ 'IDP.SERVERS' | translate }}"
formControlName="serversList"
[required]="true"
@ -30,13 +31,13 @@
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.BASEDN' | translate }}</cnsl-label>
<input cnslInput formControlName="baseDn" />
<input cnslInput formControlName="baseDn" required />
</cnsl-form-field>
<div [ngClass]="{ 'identity-provider-2-col': !provider }">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.BINDDN' | translate }}</cnsl-label>
<input cnslInput formControlName="bindDn" />
<input cnslInput formControlName="bindDn" required />
</cnsl-form-field>
<mat-checkbox
@ -46,14 +47,14 @@
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.UPDATEBINDPASSWORD' | translate }}</mat-checkbox
>
<cnsl-form-field *ngIf="!provider || (provider && updateBindPassword)" class="formfield">
<cnsl-form-field class="formfield pwd" [ngClass]="{ show: !provider || (provider && updateBindPassword) }">
<cnsl-label>{{ 'IDP.BINDPASSWORD' | translate }}</cnsl-label>
<input
cnslInput
name="bindpassword"
formControlName="bindPassword"
type="password"
autocomplete="new-password"
[required]="!provider"
/>
</cnsl-form-field>
</div>
@ -62,16 +63,18 @@
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.USERBASE' | translate }}</cnsl-label>
<input cnslInput formControlName="userBase" />
<input cnslInput formControlName="userBase" required />
</cnsl-form-field>
<cnsl-string-list
class="string-list-component-wrapper"
title="{{ 'IDP.USERFILTERS' | translate }}"
formControlName="userFiltersList"
[required]="true"
></cnsl-string-list>
<cnsl-string-list
class="string-list-component-wrapper"
title="{{ 'IDP.USEROBJECTCLASSES' | translate }}"
formControlName="userObjectClassesList"
[required]="true"
@ -79,22 +82,11 @@
<div class="identity-provider-optional-h-wrapper">
<h2>{{ 'IDP.LDAPATTRIBUTES' | translate }}</h2>
<button (click)="showAttributes = !showAttributes" type="button" mat-icon-button>
<mat-icon *ngIf="showAttributes">keyboard_arrow_up</mat-icon>
<mat-icon *ngIf="!showAttributes">keyboard_arrow_down</mat-icon>
</button>
<span *ngIf="!provider?.config?.ldap?.attributes?.idAttribute" class="state error">{{
'IDP.REQUIRED' | translate
}}</span>
</div>
<div *ngIf="showAttributes">
<cnsl-ldap-attributes
[initialAttributes]="provider?.config?.ldap?.attributes"
(attributesChanged)="attributes = $event"
></cnsl-ldap-attributes>
</div>
<cnsl-ldap-attributes
[initialAttributes]="provider?.config?.ldap?.attributes"
(attributesChanged)="attributes = $event"
></cnsl-ldap-attributes>
<div class="identity-provider-optional-h-wrapper">
<h2>{{ 'IDP.OPTIONAL' | translate }}</h2>
@ -123,7 +115,7 @@
color="primary"
mat-raised-button
class="continue-button"
[disabled]="form.invalid || attributes.toObject().idAttribute === '' || form.disabled"
[disabled]="!form.valid || !attributes.toObject().idAttribute || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>

View File

@ -20,7 +20,7 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { minArrayLengthValidator, requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
@ -30,7 +30,6 @@ import { PolicyComponentServiceType } from '../../policies/policy-component-type
})
export class ProviderLDAPComponent {
public updateBindPassword: boolean = false;
public showAttributes: boolean = false;
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
public attributes: LDAPAttributes = new LDAPAttributes();
@ -54,15 +53,15 @@ export class ProviderLDAPComponent {
) {
this.form = new FormGroup({
name: new FormControl('', [requiredValidator]),
serversList: new FormControl('', [requiredValidator]),
serversList: new FormControl<string[]>([''], [minArrayLengthValidator(1)]),
baseDn: new FormControl('', [requiredValidator]),
bindDn: new FormControl('', [requiredValidator]),
bindPassword: new FormControl('', [requiredValidator]),
userBase: new FormControl('', [requiredValidator]),
userFiltersList: new FormControl('', [requiredValidator]),
userObjectClassesList: new FormControl('', [requiredValidator]),
userFiltersList: new FormControl<string[]>([''], [minArrayLengthValidator(1)]),
userObjectClassesList: new FormControl<string[]>([''], [minArrayLengthValidator(1)]),
timeout: new FormControl<number>(0),
startTls: new FormControl(false),
startTls: new FormControl<boolean>(false),
});
this.authService
@ -112,6 +111,7 @@ export class ProviderLDAPComponent {
if (this.id) {
this.getData(this.id);
this.bindPassword?.setValidators([]);
this.bindPassword?.updateValueAndValidity();
}
});
}
@ -125,12 +125,25 @@ export class ProviderLDAPComponent {
this.service
.getProviderByID(req)
.then((resp) => {
this.provider = resp.idp;
this.loading = false;
if (this.provider?.config?.ldap) {
this.form.patchValue(this.provider.config.ldap);
if (resp.idp) {
this.provider = resp.idp;
this.loading = false;
this.name?.setValue(this.provider.name);
this.timeout?.setValue(this.provider.config.ldap.timeout?.seconds);
const config = this.provider?.config?.ldap;
if (config) {
this.serversList?.setValue(config.serversList);
this.startTls?.setValue(config.startTls);
this.baseDn?.setValue(config.baseDn);
this.bindDn?.setValue(config.bindDn);
this.userBase?.setValue(config.userBase);
this.userObjectClassesList?.setValue(config.userObjectClassesList);
this.userFiltersList?.setValue(config.userFiltersList);
if (this.provider?.config?.ldap?.timeout?.seconds) {
this.timeout?.setValue(this.provider?.config?.ldap?.timeout?.seconds);
}
}
}
})
.catch((error) => {

View File

@ -1,5 +1,8 @@
@use '@angular/material' as mat;
@mixin identity-provider-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background);
.identity-provider-desc {
font-size: 14px;
@ -44,6 +47,14 @@
display: block;
max-width: 400px;
&.pwd {
display: none;
}
&.pwd.show {
display: block;
}
.name-hint {
font-size: 12px;
}
@ -63,6 +74,10 @@
}
}
.string-list-component-wrapper {
max-width: 400px;
}
.identity-provider-content {
display: flex;
flex-direction: column;

View File

@ -1,30 +1,43 @@
<form class="string-list-form" (ngSubmit)="add(redInput)">
<cnsl-form-field class="formfield">
<cnsl-label>{{ title }}</cnsl-label>
<input #redInput cnslInput [formControl]="control" />
</cnsl-form-field>
<button
matTooltip="{{ 'ACTIONS.ADD' | translate }}"
type="submit"
mat-icon-button
[disabled]="control.invalid || control.disabled"
>
<mat-icon>add</mat-icon>
</button>
</form>
<div class="form-array-list">
<div class="form-field-list">
<div class="list-header-wrapper">
<p class="list-header cnsl-secondary-text">{{ title }}*</p>
<button
class="add-element-btn"
matTooltip="{{ 'ACTIONS.ADD' | translate }}"
type="button"
mat-icon-button
(click)="addArrayEntry()"
>
<mat-icon>add</mat-icon>
</button>
</div>
<ng-container *ngFor="let formControl of formArray.controls; index as i">
<div class="element-row">
<cnsl-form-field class="formfield" [hideRequiredMarker]="true">
<input cnslInput title="{{ 'IDP.SERVERS' | translate }}" [formControl]="$any(formControl)" required />
</cnsl-form-field>
<div class="string-list">
<div *ngFor="let str of value" class="value-line">
<span>{{ str }}</span>
<span class="fill-space"></span>
<button
type="button"
matTooltip="{{ 'ACTIONS.DELETE' | translate }}"
mat-icon-button
(click)="remove(str)"
class="icon-button"
>
<mat-icon class="icon">cancel</mat-icon>
</button>
<button
class="add-element-btn"
[disabled]="i === 0 && formArray.controls.length === 1 && formControl.value === ''"
[matTooltip]="
i === 0 && formArray.controls.length === 1 ? ('ACTIONS.CLEAR' | translate) : ('ACTIONS.REMOVE' | translate)
"
type="button"
mat-icon-button
color="warn"
(click)="i === 0 && formArray.controls.length === 1 ? clearEntryAtIndex(i) : removeEntryAtIndex(i)"
>
<i *ngIf="i === 0 && formArray.controls.length === 1; else removeIcon" class="las la-times-circle"></i>
<ng-template #removeIcon>
<i class="las la-minus-circle"></i>
</ng-template>
</button>
</div>
</ng-container>
<span class="control-error" *ngIf="control.touched && control.errors && control.errors['errorsatleastone']">{{
control.errors['errorsatleastone'].i18nKey | translate
}}</span>
</div>
</div>

View File

@ -5,56 +5,50 @@
$background: map-get($theme, background);
$is-dark-theme: map-get($theme, is-dark);
$warn: map-get($theme, warn);
$warn-color: map-get($warn, 500);
$button-text-color: map-get($foreground, text);
$button-disabled-text-color: map-get($foreground, disabled-button);
$divider-color: map-get($foreground, dividers);
$secondary-text: map-get($foreground, secondary-text);
$warncolor: map-get($warn, 500);
.string-list {
width: 100%;
.form-array-list {
display: flex;
flex-direction: row;
max-width: 400px;
background: if($is-dark-theme, #00000020, mat.get-color-from-palette($background, cards));
margin-left: -1rem;
margin-right: -1rem;
padding: 0 1rem 0.5rem 1rem;
margin-top: 0.5rem;
.value-line {
.list-header-wrapper {
display: flex;
align-items: center;
margin: 0.5rem 0;
padding: 0 0 0 0.75rem;
border-radius: 4px;
background: map-get($background, infosection);
margin: 0.5rem -0.5rem 0 0;
.fill-space {
flex: 1;
.list-header {
font-size: 12px;
}
}
.icon-button {
height: 30px;
line-height: 30px;
.form-field-list {
flex: 1;
display: flex;
flex-direction: column;
.icon {
font-size: 1rem;
margin-bottom: 3px;
}
.element-row {
display: flex;
align-items: center;
&:not(:hover) {
color: $secondary-text;
.formfield {
flex: 1;
}
}
.control-error {
font-size: 12px;
color: $warncolor;
}
}
.add-element-btn {
margin-bottom: 0rem;
}
}
}
.string-list-form {
display: flex;
align-items: flex-end;
min-width: 320px;
.formfield {
width: 500px;
}
button {
margin-bottom: 0.9rem;
margin-right: -0.5rem;
}
}

View File

@ -1,7 +1,7 @@
import { Component, forwardRef, Input, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, Subject, takeUntil } from 'rxjs';
import { requiredValidator } from '../form-field/validators/validators';
import { Component, forwardRef, Input, OnDestroy, ViewChildren, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormArray, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { distinctUntilChanged, Subject, takeUntil } from 'rxjs';
import { minArrayLengthValidator, requiredValidator } from '../form-field/validators/validators';
@Component({
selector: 'cnsl-string-list',
@ -16,22 +16,23 @@ import { requiredValidator } from '../form-field/validators/validators';
},
],
})
export class StringListComponent implements ControlValueAccessor, OnInit, OnDestroy {
export class StringListComponent implements ControlValueAccessor, OnDestroy {
@Input() title: string = '';
@Input() required: boolean = false;
@Input() public getValues: Observable<void> = new Observable(); // adds formfieldinput to array on emission
@Input() public control: FormControl = new FormControl<string>({ value: '', disabled: true });
@Input() public control: FormControl = new FormControl<string[]>({ value: [], disabled: true });
private destroy$: Subject<void> = new Subject();
@ViewChild('redInput') input!: any;
private val: string[] = [];
@ViewChildren('stringInput') input!: any[];
public val: string[] = [];
ngOnInit(): void {
this.getValues.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.add(this.input.nativeElement);
public formArray: FormArray = new FormArray([new FormControl('', [requiredValidator])]);
constructor() {
this.control.setValidators([minArrayLengthValidator(1)]);
this.formArray.valueChanges.pipe(takeUntil(this.destroy$), distinctUntilChanged()).subscribe((value) => {
this.value = value;
});
this.required ? this.control.setValidators([requiredValidator]) : this.control.setValidators([]);
}
ngOnDestroy(): void {
@ -50,12 +51,24 @@ export class StringListComponent implements ControlValueAccessor, OnInit, OnDest
}
}
addArrayEntry() {
this.formArray.push(new FormControl('', [requiredValidator]));
}
removeEntryAtIndex(index: number) {
this.formArray.removeAt(index);
}
clearEntryAtIndex(index: number) {
this.formArray.controls[index].setValue('');
}
get value() {
return this.val;
}
writeValue(value: string[]) {
this.value = value;
value.map((v, i) => this.formArray.setControl(i, new FormControl(v, [requiredValidator])));
}
registerOnChange(fn: any) {
@ -73,28 +86,4 @@ export class StringListComponent implements ControlValueAccessor, OnInit, OnDest
this.control.enable();
}
}
public add(input: any): void {
if (this.control.valid) {
const trimmed = input.value.trim();
if (trimmed) {
this.val ? this.val.push(input.value) : (this.val = [input.value]);
this.onChange(this.val);
this.onTouch(this.val);
}
if (input) {
input.value = '';
}
}
}
public remove(str: string): void {
const index = this.value.indexOf(str);
if (index >= 0) {
this.value.splice(index, 1);
this.onChange(this.value);
this.onTouch(this.value);
}
}
}

View File

@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyButtonModule } from '@angular/material/legacy-button';
import { MatLegacyChipsModule } from '@angular/material/legacy-chips';
import { MatLegacyTooltipModule } from '@angular/material/legacy-tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { InputModule } from '../input/input.module';
@ -15,6 +16,7 @@ import { StringListComponent } from './string-list.component';
InputModule,
FormsModule,
ReactiveFormsModule,
MatLegacyChipsModule,
TranslateModule,
MatIconModule,
MatLegacyTooltipModule,

View File

@ -10,11 +10,11 @@
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="machine-create-form">
<div class="machine-create-content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="userName" required />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" required />
</cnsl-form-field>
<cnsl-form-field class="formfield">

View File

@ -13,11 +13,11 @@
<div class="user-create-grid">
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}</cnsl-label>
<input cnslInput matRipple formControlName="email" required />
</cnsl-form-field>
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
<input
cnslInput
formControlName="userName"
@ -28,11 +28,11 @@
</cnsl-form-field>
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="firstName" required />
</cnsl-form-field>
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="lastName" required />
</cnsl-form-field>
<cnsl-form-field>

View File

@ -247,6 +247,7 @@
},
"ERRORS": {
"REQUIRED": "Bitte fülle dieses Feld aus.",
"ATLEASTONE": "Geben Sie mindestens einen Wert an.",
"TOKENINVALID": {
"TITLE": "Du bist abgemeldet",
"DESCRIPTION": "Klicke auf \"Einloggen\", um Dich erneut anzumelden."

View File

@ -248,6 +248,7 @@
},
"ERRORS": {
"REQUIRED": "Please fill in this field.",
"ATLEASTONE": "Provide at least one value.",
"TOKENINVALID": {
"TITLE": "Your authorization token has expired.",
"DESCRIPTION": "Click the button below to log in again."

View File

@ -248,6 +248,7 @@
},
"ERRORS": {
"REQUIRED": "Por favor rellena este campo.",
"ATLEASTONE": "Proporcione al menos un valor.",
"TOKENINVALID": {
"TITLE": "Tu token de autorización token ha caducado.",
"DESCRIPTION": "Haz clic en el botón más abajo para iniciar sesión otra vez."

View File

@ -247,6 +247,7 @@
},
"ERRORS": {
"REQUIRED": "Remplis ce champ s'il te plaît.",
"ATLEASTONE": "Indiquez au moins une valeur.",
"TOKENINVALID": {
"TITLE": "Votre jeton d'autorisation a expiré.",
"DESCRIPTION": "Cliquez sur le bouton ci-dessous pour vous reconnecter."

View File

@ -247,6 +247,7 @@
},
"ERRORS": {
"REQUIRED": "Compilare questo campo.",
"ATLEASTONE": "Inserisci almeno un valore.",
"TOKENINVALID": {
"TITLE": "Il tuo Access Token \u00e8 scaduto.",
"DESCRIPTION": "Clicca il pulsante per richiedere una nuova sessione."

View File

@ -248,6 +248,7 @@
},
"ERRORS": {
"REQUIRED": "一部の必須項目が不足しています。",
"ATLEASTONE": "少なくとも 1 つの値を指定してください。",
"TOKENINVALID": {
"TITLE": "トークンが期限切れになりました。",
"DESCRIPTION": "下のボタンをクリックして、もう一度ログインする。"

View File

@ -247,6 +247,7 @@
},
"ERRORS": {
"REQUIRED": "Proszę wypełnić to pole.",
"ATLEASTONE": "Podaj co najmniej jedną wartość.",
"TOKENINVALID": {
"TITLE": "Twój token autoryzacji wygasł.",
"DESCRIPTION": "Kliknij przycisk poniżej, aby ponownie się zalogować."

View File

@ -247,6 +247,7 @@
},
"ERRORS": {
"REQUIRED": "请填写此栏",
"ATLEASTONE": "P至少提供一个值。",
"TOKENINVALID": {
"TITLE": "您的授权令牌已过期。",
"DESCRIPTION": "点击下方按钮再次登录。"