[AAE-10450] Stories for components in login module (#7909)

Co-authored-by: Bartosz Sekula <Bartosz.Sekula@hyland.com>
This commit is contained in:
Robert Duda 2022-10-20 13:14:30 +02:00 committed by GitHub
parent b64d1583e2
commit 47d95fc7c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 519 additions and 114 deletions

View File

@ -309,7 +309,8 @@
},
"DIALOG": {
"CANCEL": "Cancel",
"CHOOSE": "Choose"
"CHOOSE": "Choose",
"LOGIN": "Sign in"
}
},
"ADF-DATATABLE": {

View File

@ -1,25 +1,27 @@
<header
mat-dialog-title
data-automation-id="login-dialog-title">{{data?.title}}
data-automation-id="login-dialog-title">
{{data?.title}}
</header>
<mat-dialog-content class="adf-login-dialog-content">
<adf-login-dialog-panel #adfLoginPanel
(success)="onLoginSuccess($event)">
<adf-login-dialog-panel #adfLoginPanel (success)="onLoginSuccess($event)">
</adf-login-dialog-panel>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button
mat-button
(click)="close()"
data-automation-id="login-dialog-actions-cancel">{{ 'LOGIN.DIALOG.CANCEL' | translate }}
mat-button (click)="close()"
data-automation-id="login-dialog-actions-cancel">
{{ 'LOGIN.DIALOG.CANCEL' | translate }}
</button>
<button mat-button
<button
mat-button
class="choose-action"
data-automation-id="login-dialog-actions-perform"
[disabled]="!isFormValid()"
(click)="submitForm()">{{ buttonActionName | translate}}
(click)="submitForm()">
{{ buttonActionName | translate}}
</button>
</mat-dialog-actions>

View File

@ -0,0 +1,102 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Meta, moduleMetadata, Story } from '@storybook/angular';
import { CoreStoryModule } from '../../testing/core.story.module';
import { RouterTestingModule } from '@angular/router/testing';
import { LoginModule } from './../login.module';
import { LoginDialogStorybookComponent } from './login-dialog.stories.component';
import { MatButtonModule } from '@angular/material/button';
import { AuthenticationService } from './../../services/authentication.service';
import { AuthenticationMock } from './../../mock/authentication.service.mock';
export default {
component: LoginDialogStorybookComponent,
title: 'Core/Login/Login Dialog',
decorators: [
moduleMetadata({
imports: [CoreStoryModule, LoginModule, RouterTestingModule, MatButtonModule],
providers: [
{ provide: AuthenticationService, useClass: AuthenticationMock }
]
})
],
parameters: {
docs: {
description: {
component: `Allows a user to perform a login via a dialog.`
}
}
},
argTypes: {
correct: {
control: 'none',
name: 'To test correct functionality:',
description: 'Use `fake-username` and `fake-password`.',
table: { category: 'Storybook Info' }
},
corsError: {
control: 'none',
name: 'To test CORS error:',
description: 'Use `fake-username-CORS-error` and `fake-password`.',
table: { category: 'Storybook Info' }
},
csrfError: {
control: 'none',
name: 'To test CSRF error:',
description: 'Use `fake-username-CSRF-error` and `fake-password`.',
table: { category: 'Storybook Info' }
},
ecmAccessError: {
control: 'none',
name: 'To test ECM access error:',
description: 'Use `fake-username-ECM-access-error` and `fake-password`.',
table: { category: 'Storybook Info' }
},
closed: {
action: 'closed',
description: 'Emitted when the dialog is closed.',
table: {
type: { summary: 'EventEmitter <any>' },
category: 'Actions'
}
},
error: {
action: 'error',
description: 'Emitted when the login fails.',
table: {
type: { summary: 'EventEmitter <any>' },
category: 'Actions'
}
},
executeSubmit: {
action: 'executeSubmit',
description: 'Emitted when the login form is submitted.',
table: {
type: { summary: 'EventEmitter <any>' },
category: 'Actions'
}
}
}
} as Meta;
const template: Story<LoginDialogStorybookComponent> = (args: LoginDialogStorybookComponent) => ({
props: args
});
export const loginDialog = template.bind({});
loginDialog.parameters = { layout: 'centered' };

View File

@ -0,0 +1,67 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Output, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Subject } from 'rxjs';
import { LoginDialogComponent } from './login-dialog.component';
import { LoginDialogComponentData } from './login-dialog-component-data.interface';
@Component({
selector: 'adf-login-dialog-storybook',
template: `<button mat-raised-button (click)="openLoginDialog()">
Open dialog
</button>`
})
export class LoginDialogStorybookComponent {
@Output() executeSubmit = new EventEmitter<string>();
@Output() error = new EventEmitter<string>();
@Output() closed = new EventEmitter<string>();
constructor(private dialog: MatDialog) { }
openLoginDialog() {
const data: LoginDialogComponentData = {
title: 'Perform a Login',
actionName: 'LOGIN',
logged: new Subject<any>()
};
this.dialog.open(
LoginDialogComponent,
{
data,
panelClass: 'adf-login-dialog',
width: '630px'
}
);
data.logged.subscribe(
() => {
this.executeSubmit.emit('executeSubmit');
},
(error) => {
this.error.emit(error);
},
() => {
this.closed.emit('closed');
this.dialog.closeAll();
}
);
}
}

View File

@ -3,17 +3,27 @@
<div class="adf-ie11FixerChild">
<mat-card class="adf-login-card-wide">
<form id="adf-login-form" [formGroup]="form" (submit)="onSubmit(form.value)" autocomplete="off">
<form
id="adf-login-form"
[formGroup]="form"
autocomplete="off"
(submit)="onSubmit(form.value)">
<mat-card-header>
<mat-card-title>
<div class="adf-alfresco-logo">
<!--HEADER TEMPLATE-->
<ng-template *ngIf="headerTemplate"
ngFor [ngForOf]="[data]"
[ngForTemplate]="headerTemplate">
<ng-template
*ngIf="headerTemplate"
ngFor
[ngForOf]="[data]"
[ngForTemplate]="headerTemplate">
</ng-template>
<img *ngIf="!headerTemplate" id="adf-login-img-logo" class="adf-img-logo" [src]="logoImageUrl"
alt="{{'LOGIN.LOGO' | translate }}">
<img
*ngIf="!headerTemplate"
id="adf-login-img-logo"
class="adf-img-logo"
[src]="logoImageUrl"
alt="{{'LOGIN.LOGO' | translate }}">
</div>
</mat-card-title>
</mat-card-header>
@ -22,122 +32,166 @@
<!--ERRORS AREA-->
<div class="adf-error-container">
<div *ngIf="isError" id="login-error" data-automation-id="login-error"
class="adf-error adf-error-message">
<div
*ngIf="isError"
id="login-error"
data-automation-id="login-error"
class="adf-error adf-error-message">
<mat-icon class="adf-error-icon">warning</mat-icon>
<span class="adf-login-error-message">{{errorMsg | translate }}</span>
<span class="adf-login-error-message">
{{errorMsg | translate }}
</span>
</div>
</div>
<div *ngIf="!implicitFlow">
<!--USERNAME FIELD-->
<div class="adf-login__field"
[ngClass]="{'adf-is-invalid': isErrorStyle(form.controls.username)}">
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
<input matInput placeholder="{{'LOGIN.LABEL.USERNAME' | translate }}"
type="text"
class="adf-full-width"
formControlName="username"
autocapitalize="none"
id="username"
data-automation-id="username"
(blur)="trimUsername($event)">
<div
class="adf-login__field"
[ngClass]="{'adf-is-invalid': isErrorStyle(form.controls.username)}">
<mat-form-field
class="adf-full-width"
floatPlaceholder="never"
color="primary">
<input
matInput
type="text"
class="adf-full-width"
formControlName="username"
id="username"
data-automation-id="username"
placeholder="{{'LOGIN.LABEL.USERNAME' | translate }}"
autocapitalize="none"
(blur)="trimUsername($event)">
</mat-form-field>
<span class="adf-login-validation" for="username" *ngIf="formError['username']">
<span id="username-error" class="adf-login-error" data-automation-id="username-error">{{formError['username'] | translate }}</span>
</span>
<span
*ngIf="formError['username']"
class="adf-login-validation"
for="username">
<span
id="username-error"
class="adf-login-error"
data-automation-id="username-error">
{{formError['username'] | translate}}
</span>
</span>
</div>
<!--PASSWORD FIELD-->
<div class="adf-login__field">
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
<input matInput placeholder="{{'LOGIN.LABEL.PASSWORD' | translate }}"
[type]="isPasswordShow ? 'text' : 'password'"
formControlName="password"
id="password"
data-automation-id="password">
<mat-form-field
class="adf-full-width"
floatPlaceholder="never"
color="primary">
<input
matInput
placeholder="{{'LOGIN.LABEL.PASSWORD' | translate }}"
[type]="isPasswordShow ? 'text' : 'password'"
formControlName="password"
id="password"
data-automation-id="password">
<button
matSuffix
mat-icon-button
type="button"
[attr.aria-label]="(isPasswordShow ?
'LOGIN.ARIA-LABEL.HIDE-PASSWORD':
'LOGIN.ARIA-LABEL.SHOW-PASSWORD'
) | translate"
[attr.aria-label]="(isPasswordShow ? 'LOGIN.ARIA-LABEL.HIDE-PASSWORD' : 'LOGIN.ARIA-LABEL.SHOW-PASSWORD') | translate"
(click)="toggleShowPassword($event)"
(keyup.enter)="toggleShowPassword($event)"
[attr.data-automation-id]="isPasswordShow ? 'hide_password':'show_password'">
<mat-icon class="adf-login-password-icon">
{{ isPasswordShow ? 'visibility':'visibility_off' }}
</mat-icon>
</button>
[attr.data-automation-id]="isPasswordShow ? 'hide_password' : 'show_password'">
<mat-icon class="adf-login-password-icon">
{{ isPasswordShow ? 'visibility' : 'visibility_off' }}
</mat-icon>
</button>
</mat-form-field>
<span class="adf-login-validation" for="password" *ngIf="formError['password']">
<span id="password-required" class="adf-login-error"
data-automation-id="password-required">{{formError['password'] | translate }}</span>
</span>
<span
class="adf-login-validation"
for="password"
*ngIf="formError['password']">
<span
id="password-required"
class="adf-login-error"
data-automation-id="password-required">
{{formError['password'] | translate}}
</span>
</span>
</div>
<!--CUSTOM CONTENT-->
<ng-content></ng-content>
<br>
<button type="submit" id="login-button"
class="adf-login-button"
mat-raised-button color="accent"
[class.adf-isChecking]="actualLoginStep === LoginSteps.Checking"
[class.adf-isWelcome]="actualLoginStep === LoginSteps.Welcome"
data-automation-id="login-button" [disabled]="!form.valid"
[attr.aria-label]="'LOGIN.BUTTON.LOGIN' | translate">
<span *ngIf="actualLoginStep === LoginSteps.Landing" class="adf-login-button-label">{{ 'LOGIN.BUTTON.LOGIN' | translate }}</span>
<div *ngIf="actualLoginStep === LoginSteps.Checking"
class="adf-interactive-login-label">
<button
type="submit"
id="login-button"
class="adf-login-button"
mat-raised-button
color="accent"
[class.adf-isChecking]="actualLoginStep === LoginSteps.Checking"
[class.adf-isWelcome]="actualLoginStep === LoginSteps.Welcome"
data-automation-id="login-button"
[disabled]="!form.valid"
[attr.aria-label]="'LOGIN.BUTTON.LOGIN' | translate">
<span
class="adf-login-button-label">{{ 'LOGIN.BUTTON.CHECKING' | translate }}</span>
<div class="adf-login-spinner-container">
<mat-spinner id="checking-spinner" class="adf-login-checking-spinner"
[diameter]="25"></mat-spinner>
*ngIf="actualLoginStep === LoginSteps.Landing"
class="adf-login-button-label">
{{'LOGIN.BUTTON.LOGIN' | translate }}
</span>
<div
*ngIf="actualLoginStep === LoginSteps.Checking"
class="adf-interactive-login-label">
<span class="adf-login-button-label">
{{ 'LOGIN.BUTTON.CHECKING' | translate}}
</span>
<div class="adf-login-spinner-container">
<mat-spinner
id="checking-spinner"
class="adf-login-checking-spinner"
[diameter]="25">
</mat-spinner>
</div>
</div>
<div
*ngIf="actualLoginStep === LoginSteps.Welcome"
class="adf-interactive-login-label">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.WELCOME' | translate }}</span>
<mat-icon class="adf-welcome-icon">done</mat-icon>
</div>
</div>
<div *ngIf="actualLoginStep === LoginSteps.Welcome" class="adf-interactive-login-label">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.WELCOME' | translate }}</span>
<mat-icon class="adf-welcome-icon">done</mat-icon>
</div>
</button>
<div *ngIf="showRememberMe" class="adf-login__remember-me">
<mat-checkbox id="adf-login-remember" color="primary" class="adf-login-remember-me"
[checked]="rememberMe"
(change)="rememberMe = !rememberMe">{{ 'LOGIN.LABEL.REMEMBER' | translate }}
<mat-checkbox
id="adf-login-remember"
color="primary"
class="adf-login-remember-me"
[checked]="rememberMe"
(change)="rememberMe = !rememberMe">
{{ 'LOGIN.LABEL.REMEMBER' | translate }}
</mat-checkbox>
</div>
</div>
<div *ngIf="implicitFlow">
<button type="button" (click)="implicitLogin()" id="login-button-sso"
[attr.aria-label]="'LOGIN.BUTTON.SSO' | translate"
class="adf-login-button"
mat-raised-button color="primary"
data-automation-id="login-button-sso">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.SSO' | translate }}</span>
<button
type="button"
(click)="implicitLogin()"
id="login-button-sso"
[attr.aria-label]="'LOGIN.BUTTON.SSO' | translate"
class="adf-login-button"
mat-raised-button color="primary"
data-automation-id="login-button-sso">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.SSO' | translate }}</span>
</button>
</div>
</mat-card-content>
<mat-card-actions *ngIf="footerTemplate || showLoginActions">
<div class="adf-login-action-container">
<!--FOOTER TEMPLATE-->
<ng-template *ngIf="footerTemplate"
ngFor [ngForOf]="[data]"
[ngForTemplate]="footerTemplate">
<ng-template
*ngIf="footerTemplate"
ngFor
[ngForOf]="[data]"
[ngForTemplate]="footerTemplate">
</ng-template>
<div class="adf-login-action" *ngIf="!footerTemplate && showLoginActions">
<div id="adf-login-action-left" class="adf-login-action-left">
@ -149,7 +203,6 @@
</div>
</div>
</mat-card-actions>
</form>
</mat-card>

View File

@ -0,0 +1,180 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Meta, moduleMetadata, Story } from '@storybook/angular';
import { CoreStoryModule } from '../../testing/core.story.module';
import { LoginModule } from '../login.module';
import { LoginComponent } from './login.component';
import { RouterModule } from '@angular/router';
import { AuthenticationService } from './../../services/authentication.service';
import { AuthenticationMock } from './../../mock/authentication.service.mock';
export default {
component: LoginComponent,
title: 'Core/Login/Login',
decorators: [
moduleMetadata({
imports: [CoreStoryModule, LoginModule, RouterModule.forRoot([], { useHash: true })],
providers: [
{ provide: AuthenticationService, useClass: AuthenticationMock }
]
})
],
parameters: {
docs: {
description: {
component: `Authenticates to Alfresco Content Services and Alfresco Process Services.`
}
}
},
argTypes: {
correct: {
control: 'none',
name: 'To test correct functionality:',
description: 'Use `fake-username` and `fake-password`.',
table: { category: 'Storybook Info' }
},
corsError: {
control: 'none',
name: 'To test CORS error:',
description: 'Use `fake-username-CORS-error` and `fake-password`.',
table: { category: 'Storybook Info' }
},
csrfError: {
control: 'none',
name: 'To test CSRF error:',
description: 'Use `fake-username-CSRF-error` and `fake-password`.',
table: { category: 'Storybook Info' }
},
ecmAccessError: {
control: 'none',
name: 'To test ECM access error:',
description: 'Use `fake-username-ECM-access-error` and `fake-password`.',
table: { category: 'Storybook Info' }
},
showRememberMe: {
control: 'boolean',
description: 'Should the `Remember me` checkbox be shown? When selected, this option will remember the logged-in user after the browser is closed to avoid logging in repeatedly.',
defaultValue: true,
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'true' }
}
},
showLoginActions: {
control: 'boolean',
description: 'Should the extra actions (`Need Help`, `Register`, etc) be shown?',
defaultValue: true,
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'true' }
}
},
needHelpLink: {
control: 'text',
description: 'Sets the URL of the NEED HELP link in the footer.',
defaultValue: '/?path=/story/core-login-login--login',
table: {
type: { summary: 'string' },
defaultValue: { summary: '' }
}
},
registerLink: {
control: 'text',
description: 'Sets the URL of the REGISTER link in the footer.',
defaultValue: '/?path=/story/core-login-login--login',
table: {
type: { summary: 'string' },
defaultValue: { summary: '' }
}
},
logoImageUrl: {
control: 'text',
description: 'Path to a custom logo image.',
defaultValue: './assets/images/alfresco-logo.svg',
table: {
type: { summary: 'string' },
defaultValue: { summary: './assets/images/alfresco-logo.svg' }
}
},
backgroundImageUrl: {
control: 'text',
description: 'Path to a custom background image.',
defaultValue: './assets/images/background.svg',
table: {
type: { summary: 'string' },
defaultValue: { summary: './assets/images/background.svg' }
}
},
copyrightText: {
control: 'text',
description: 'The copyright text below the login box.',
defaultValue: '\u00A9 2016 Alfresco Software, Inc. All Rights Reserved.',
table: {
type: { summary: 'string' },
defaultValue: { summary: '\u00A9 2016 Alfresco Software, Inc. All Rights Reserved.' }
}
},
fieldsValidation: {
control: 'object',
description: 'Custom validation rules for the login form.',
table: {
type: { summary: 'any' },
defaultValue: { summary: 'undefined' }
}
},
successRoute: {
control: 'text',
description: 'Route to redirect to on successful login.',
defaultValue: '.',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'null' }
}
},
success: {
action: 'success',
description: 'Emitted when the login is successful.',
table: {
type: { summary: 'EventEmitter <LoginSuccessEvent>' },
category: 'Actions'
}
},
error: {
action: 'error',
description: 'Emitted when the login fails.',
table: {
type: { summary: 'EventEmitter <LoginErrorEvent>' },
category: 'Actions'
}
},
executeSubmit: {
action: 'executeSubmit',
description: 'Emitted when the login form is submitted.',
table: {
type: { summary: 'EventEmitter <LoginSubmitEvent>' },
category: 'Actions'
}
}
}
} as Meta;
const template: Story<LoginComponent> = (args: LoginComponent) => ({
props: args
});
export const login = template.bind({});

View File

@ -16,28 +16,28 @@
*/
import { Observable, of, throwError } from 'rxjs';
import { RedirectionModel } from '../models/redirection.model';
import { Injectable } from '@angular/core';
import { AuthenticationService } from '../services/authentication.service';
import { AlfrescoApiService } from '../services/alfresco-api.service';
import { CookieService } from '../services/cookie.service';
import { LogService } from '../services/log.service';
import { StorageService } from '../services/storage.service';
import { AppConfigService } from '../app-config/app-config.service';
export class AuthenticationMock {
private redirectUrl: RedirectionModel = null;
setRedirectUrl(url: RedirectionModel) {
this.redirectUrl = url;
@Injectable({
providedIn: 'root'
})
export class AuthenticationMock extends AuthenticationService {
constructor(
appConfig: AppConfigService,
storageService: StorageService,
alfrescoApi: AlfrescoApiService,
cookie: CookieService,
logService: LogService
) {
super(appConfig, storageService, alfrescoApi, cookie, logService);
}
isEcmLoggedIn(): boolean {
return true;
}
isBpmLoggedIn(): boolean {
return true;
}
getRedirectUrl(): string | null {
return this.redirectUrl ? this.redirectUrl.url : null;
}
// TODO: real auth service returns Observable<string>
login(username: string, password: string): Observable<{ type: string; ticket: any }> {
if (username === 'fake-username' && password === 'fake-password') {
return of({ type: 'type', ticket: 'ticket' });

View File

@ -107,7 +107,7 @@ export class AlfrescoApiService {
contextRoot: this.appConfig.get<string>(AppConfigValues.CONTEXTROOTECM),
disableCsrf: this.appConfig.get<boolean>(AppConfigValues.DISABLECSRF),
withCredentials: this.appConfig.get<boolean>(AppConfigValues.AUTH_WITH_CREDENTIALS, false),
domainPrefix : this.appConfig.get<string>(AppConfigValues.STORAGE_PREFIX),
domainPrefix: this.appConfig.get<string>(AppConfigValues.STORAGE_PREFIX),
oauth2: oauth
});
}

View File

@ -49,9 +49,9 @@ export class UserPreferencesService {
onChange: Observable<any>;
constructor(public translate: TranslateService,
private appConfig: AppConfigService,
private storage: StorageService,
private alfrescoApiService: AlfrescoApiService) {
private appConfig: AppConfigService,
private storage: StorageService,
private alfrescoApiService: AlfrescoApiService) {
this.alfrescoApiService.alfrescoApiInitialized.pipe(filter(status => status)).subscribe(this.initUserPreferenceStatus.bind(this));
this.onChangeSubject = new BehaviorSubject(this.userPreferenceStatus);
this.onChange = this.onChangeSubject.asObservable();