diff --git a/lib/core/src/lib/i18n/en.json b/lib/core/src/lib/i18n/en.json index f2ac097fad..947bee586c 100644 --- a/lib/core/src/lib/i18n/en.json +++ b/lib/core/src/lib/i18n/en.json @@ -309,7 +309,8 @@ }, "DIALOG": { "CANCEL": "Cancel", - "CHOOSE": "Choose" + "CHOOSE": "Choose", + "LOGIN": "Sign in" } }, "ADF-DATATABLE": { diff --git a/lib/core/src/lib/login/components/login-dialog.component.html b/lib/core/src/lib/login/components/login-dialog.component.html index 35463e7f9b..5f0661e526 100644 --- a/lib/core/src/lib/login/components/login-dialog.component.html +++ b/lib/core/src/lib/login/components/login-dialog.component.html @@ -1,25 +1,27 @@ {{data?.title}} + data-automation-id="login-dialog-title"> + {{data?.title}} - + {{ 'LOGIN.DIALOG.CANCEL' | translate }} + mat-button (click)="close()" + data-automation-id="login-dialog-actions-cancel"> + {{ 'LOGIN.DIALOG.CANCEL' | translate }} - {{ buttonActionName | translate}} + (click)="submitForm()"> + {{ buttonActionName | translate}} diff --git a/lib/core/src/lib/login/components/login-dialog.component.stories.ts b/lib/core/src/lib/login/components/login-dialog.component.stories.ts new file mode 100644 index 0000000000..2c557ed4d4 --- /dev/null +++ b/lib/core/src/lib/login/components/login-dialog.component.stories.ts @@ -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 ' }, + category: 'Actions' + } + }, + error: { + action: 'error', + description: 'Emitted when the login fails.', + table: { + type: { summary: 'EventEmitter ' }, + category: 'Actions' + } + }, + executeSubmit: { + action: 'executeSubmit', + description: 'Emitted when the login form is submitted.', + table: { + type: { summary: 'EventEmitter ' }, + category: 'Actions' + } + } + } +} as Meta; + +const template: Story = (args: LoginDialogStorybookComponent) => ({ + props: args +}); + +export const loginDialog = template.bind({}); +loginDialog.parameters = { layout: 'centered' }; diff --git a/lib/core/src/lib/login/components/login-dialog.stories.component.ts b/lib/core/src/lib/login/components/login-dialog.stories.component.ts new file mode 100644 index 0000000000..3fb7413d5f --- /dev/null +++ b/lib/core/src/lib/login/components/login-dialog.stories.component.ts @@ -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: ` + Open dialog + ` +}) +export class LoginDialogStorybookComponent { + + @Output() executeSubmit = new EventEmitter(); + @Output() error = new EventEmitter(); + @Output() closed = new EventEmitter(); + + constructor(private dialog: MatDialog) { } + + openLoginDialog() { + const data: LoginDialogComponentData = { + title: 'Perform a Login', + actionName: 'LOGIN', + logged: new Subject() + }; + + 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(); + } + ); + } +} diff --git a/lib/core/src/lib/login/components/login.component.html b/lib/core/src/lib/login/components/login.component.html index 6b2ab39b3e..18144998b7 100644 --- a/lib/core/src/lib/login/components/login.component.html +++ b/lib/core/src/lib/login/components/login.component.html @@ -3,17 +3,27 @@ - + - + - + @@ -22,122 +32,166 @@ - + warning - {{errorMsg | translate }} + + {{errorMsg | translate }} + - - - + + + - - - {{formError['username'] | translate }} - + + + {{formError['username'] | translate}} + + - - + + - - {{ isPasswordShow ? 'visibility':'visibility_off' }} - - + [attr.data-automation-id]="isPasswordShow ? 'hide_password' : 'show_password'"> + + {{ isPasswordShow ? 'visibility' : 'visibility_off' }} + + - - {{formError['password'] | translate }} - + + + {{formError['password'] | translate}} + + - - - {{ 'LOGIN.BUTTON.LOGIN' | translate }} - - + {{ 'LOGIN.BUTTON.CHECKING' | translate }} - - + *ngIf="actualLoginStep === LoginSteps.Landing" + class="adf-login-button-label"> + {{'LOGIN.BUTTON.LOGIN' | translate }} + + + + {{ 'LOGIN.BUTTON.CHECKING' | translate}} + + + + + + + + {{ 'LOGIN.BUTTON.WELCOME' | translate }} + done - - - - - {{ 'LOGIN.BUTTON.WELCOME' | translate }} - done - - - {{ 'LOGIN.LABEL.REMEMBER' | translate }} + + {{ 'LOGIN.LABEL.REMEMBER' | translate }} - - - {{ 'LOGIN.BUTTON.SSO' | translate }} + + {{ 'LOGIN.BUTTON.SSO' | translate }} - - + @@ -149,7 +203,6 @@ - diff --git a/lib/core/src/lib/login/components/login.component.stories.ts b/lib/core/src/lib/login/components/login.component.stories.ts new file mode 100644 index 0000000000..5a362ef6de --- /dev/null +++ b/lib/core/src/lib/login/components/login.component.stories.ts @@ -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 ' }, + category: 'Actions' + } + }, + error: { + action: 'error', + description: 'Emitted when the login fails.', + table: { + type: { summary: 'EventEmitter ' }, + category: 'Actions' + } + }, + executeSubmit: { + action: 'executeSubmit', + description: 'Emitted when the login form is submitted.', + table: { + type: { summary: 'EventEmitter ' }, + category: 'Actions' + } + } + } +} as Meta; + +const template: Story = (args: LoginComponent) => ({ + props: args +}); + +export const login = template.bind({}); diff --git a/lib/core/src/lib/mock/authentication.service.mock.ts b/lib/core/src/lib/mock/authentication.service.mock.ts index ccb3dc18be..e5317c3672 100644 --- a/lib/core/src/lib/mock/authentication.service.mock.ts +++ b/lib/core/src/lib/mock/authentication.service.mock.ts @@ -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 login(username: string, password: string): Observable<{ type: string; ticket: any }> { if (username === 'fake-username' && password === 'fake-password') { return of({ type: 'type', ticket: 'ticket' }); diff --git a/lib/core/src/lib/services/alfresco-api.service.ts b/lib/core/src/lib/services/alfresco-api.service.ts index 00f39a33f1..4c02de5585 100644 --- a/lib/core/src/lib/services/alfresco-api.service.ts +++ b/lib/core/src/lib/services/alfresco-api.service.ts @@ -107,7 +107,7 @@ export class AlfrescoApiService { contextRoot: this.appConfig.get(AppConfigValues.CONTEXTROOTECM), disableCsrf: this.appConfig.get(AppConfigValues.DISABLECSRF), withCredentials: this.appConfig.get(AppConfigValues.AUTH_WITH_CREDENTIALS, false), - domainPrefix : this.appConfig.get(AppConfigValues.STORAGE_PREFIX), + domainPrefix: this.appConfig.get(AppConfigValues.STORAGE_PREFIX), oauth2: oauth }); } diff --git a/lib/core/src/lib/services/user-preferences.service.ts b/lib/core/src/lib/services/user-preferences.service.ts index 63e0f31599..e88be14dc0 100644 --- a/lib/core/src/lib/services/user-preferences.service.ts +++ b/lib/core/src/lib/services/user-preferences.service.ts @@ -49,9 +49,9 @@ export class UserPreferencesService { onChange: Observable; 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();