[DEV] initial commit (that not work)

This commit is contained in:
Edouard DUPIN 2024-03-29 18:35:55 +01:00
parent 21072b2e71
commit 0e609cb755
111 changed files with 13217 additions and 5661 deletions

View File

@ -1,27 +1,46 @@
# KarCw
KarCw
=====
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.1.
Karideo-And-Rabbit Common Web is a generic library of the KAR application group
## Development server
Develop:
========
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
Install dependency
## Code scaffolding
```bash
pnpm install
```
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
Build for debug:
```bash
pnpm run dev
```
## Build
Build the environment
```bash
pnpm run build
```
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
deploy the environment
```bash
cd dist/kar-cw
pnpm publish --no-git-checks
```
## Running unit tests
Use development mode for the library:
=====================================
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
In a terminal run:
```bash
pnpm run dev
```
## Running end-to-end tests
On your client library run
```bash
cd <<CLIENT_PATH>>
pnpm link <<PATH_HERE>>/dist/kar-cw/
# ex: pnpm link ../kar-cw/dist/kar-cw/
```
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
Execute your library build as usual.

View File

@ -3,5 +3,37 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"kar-cw": {
"projectType": "library",
"root": "projects/kar-cw",
"sourceRoot": "projects/kar-cw/src",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/kar-cw/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/kar-cw/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/kar-cw/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "projects/kar-cw/tsconfig.spec.json",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
}
}
}

5633
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,16 @@
{
"name": "kar-cw",
"name": "kar-common",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"dev": "ng build kar-cw --watch --configuration development",
"build": "ng build kar-cw",
"deploy": "cd dist/kar-cw && pnpm publish --no-git-checks",
"link": "cd dist/kar-cw && pnpm link # TODO this in sudo ...",
"test": "ng test",
"lint": "ng lint",
"style": "prettier --write .",
"update_packages": "ncu --upgrade",
"install_dependency": "pnpm install"
},
"private": true,
"dependencies": {
@ -23,6 +27,7 @@
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.3.1",
"@angular/cli": "^17.3.1",
"@angular/compiler-cli": "^17.3.0",
"@types/jasmine": "~5.1.0",
@ -32,6 +37,13 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"ng-packagr": "^17.3.0",
"typescript": "~5.4.2"
}
}
},
"maintainers": [
{
"name": "Edouard DUPIN",
"email": "yui.heero@gmail.com"
}
]
}

7723
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

24
projects/kar-cw/README.md Normal file
View File

@ -0,0 +1,24 @@
# KarCw
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0.
## Code scaffolding
Run `ng generate component component-name --project kar-cw` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project kar-cw`.
> Note: Don't forget to add `--project kar-cw` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build kar-cw` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build kar-cw`, go to the dist folder `cd dist/kar-cw` and run `npm publish`.
## Running unit tests
Run `ng test kar-cw` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/kar-cw",
"lib": {
"entryFile": "src/index.ts"
}
}

View File

@ -0,0 +1,12 @@
{
"name": "@kangaroo-and-rabbit/kar-cw",
"version": "0.1.5",
"peerDependencies": {
"@angular/common": "^17.3.0",
"@angular/core": "^17.3.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"sideEffects": false
}

View File

@ -0,0 +1,3 @@
<div>
<img height="50px" [src]="getImage()"/>
</div>

View File

@ -0,0 +1,68 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { PasswordEntryComponent } from '../password-entry/password-entry';
describe('PasswordEntryComponent global test', () => {
let component: PasswordEntryComponent;
let fixture: ComponentFixture<PasswordEntryComponent>;
let input: HTMLInputElement;
let button: HTMLButtonElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PasswordEntryComponent],
}).compileComponents();
fixture = TestBed.createComponent(PasswordEntryComponent);
component = fixture.componentInstance;
input = fixture.nativeElement.querySelector('div').querySelector('input');
button = fixture.nativeElement.querySelector('div').querySelector('button');
});
it('Test mode password (default)', () => {
fixture.detectChanges();
expect(input.textContent).toEqual('');
expect(button.textContent).toEqual('visibility_off');
expect(input.type).toEqual('password');
});
it('Test mode text', () => {
component.passwordVisibility = true;
fixture.detectChanges();
expect(input.textContent).toEqual('');
expect(button.textContent).toEqual('visibility');
expect(input.type).toEqual('text');
});
it('test click on hide button', fakeAsync(() => {
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(false);
button.click();
tick();
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(true);
expect(button.textContent).toEqual('visibility');
expect(input.type).toEqual('text');
button.click();
tick();
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(false);
expect(button.textContent).toEqual('visibility_off');
expect(input.type).toEqual('password');
}));
it('Set password', fakeAsync(() => {
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(false);
let tmpData = 'My beautifull Password';
input.value = tmpData;
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(input.textContent).toEqual(tmpData);
expect(component.value).toEqual(tmpData);
tmpData = '';
input.value = tmpData;
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(input.textContent).toEqual(tmpData);
expect(component.value).toEqual(tmpData);
}));
});

View File

@ -0,0 +1,37 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, Input } from '@angular/core';
export enum AsyncActionState {
IDLE = "idle",
LOADING = "loading",
DONE = "done",
FAIL = "fail",
}
@Component({
selector: 'app-async-status-component',
templateUrl: 'async-status-component.html',
styleUrls: ['async-status-component.less'],
})
export class AsyncActionStatusComponent {
/// Value of the password
@Input() value: AsyncActionState = AsyncActionState.IDLE;
public getImage(): string {
switch(this.value) {
case AsyncActionState.IDLE:
return '';
case AsyncActionState.LOADING:
return 'assets/images/load.svg';
case AsyncActionState.DONE:
return 'assets/images/validate.svg';
case AsyncActionState.FAIL:
return 'assets/images/validate-not.svg';
}
}
}

View File

@ -0,0 +1,13 @@
<div>
<div class="bmp-title">
<div class="title"><ng-content select="name"></ng-content></div>
<div class="description"><ng-content select="description"></ng-content></div>
</div>
<div class="bmp-elements">
<ng-content select="body"></ng-content>
</div>
<div class="bmp-validation">
<ng-content select="footer"></ng-content>
</div>
</div>

View File

@ -0,0 +1,78 @@
.bmp-title {
width: 80%;
border: solid;
border-width: 1px;
margin: auto;
padding: 10px 20px;
border-color: black;
border-radius: 10px 10px 0px 0px;
background-color: gray;
.title {
font-size: 24px;
font-weight: bold;
line-height: 24px;
vertical-align: middle;
margin: 10px 0 10px 0;
text-align: Left;
}
.description {
font-size: 16px;
line-height: 16px;
vertical-align: middle;
margin: 10px 0 10px 0;
text-align: Left;
}
}
.bmp-elements {
width: 80%;
border: solid;
border-width: 0px 1px;
margin: auto;
padding: 10px 20px;
border-color: black;
border-radius: 0px;
background-color: lightgray;
vertical-align: middle;
text-align: Left;
/*
.elem-hr {
width: 100%;
border-color: black;
border: dashed;
border-width: 1px 0 0 0;
margin: 10px auto;
}
.elem-title {
font-size: 18px;
font-weight: bold;
margin: 0 10px 0 36px;
}
.elem-description {
font-size: 14px;
margin: 0 20px 10px 50px;
font-style: italic;
}
.elem-input {
font-size: 14px;
margin: 0 20px 10px 36px;
input {
width: 100%;
}
}*/
}
.bmp-validation {
width: 80%;
border: solid;
border-width: 1px;
margin: auto;
padding: 10px 20px;
border-color: black;
border-radius: 0px 0px 10px 10px;
background-color: gray;
}

View File

@ -0,0 +1,18 @@
/** @file
* @author Edouard DUPIN
* @copyright 2020, Edouard DUPIN, all right reserved
* @license MPL-2 (see license file)
*/
import { Component } from '@angular/core';
@Component({
// moduleId: module.id.toString(),
selector: 'burger-property',
templateUrl: './burger-property.html',
styleUrls: ['./burger-property.less'],
})
export class BurgerPropertyComponent {
constructor() {}
}

View File

@ -0,0 +1,7 @@
<div class="elem-checkbox">
<input
type="checkbox"
[checked]="value"
(change)="onChange($event.target.checked)" />
{{comment}}
</div>

View File

@ -0,0 +1,6 @@
.elem-checkbox {
font-size: 18px;
font-weight: bold;
margin: 0 10px 0 10px;
}

View File

@ -0,0 +1,29 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-checkbox',
templateUrl: 'checkbox.html',
styleUrls: ['checkbox.less'],
})
export class CheckboxComponent {
/// Checkbox value
@Input() value: boolean = false;
/// Description of the checkbox
@Input() comment: string = "";
/// event when change the value of the password
@Output() changeValue: EventEmitter<boolean> = new EventEmitter();
/**
* When input value change, need update the display and change the internal value.
* @param newValue New value set on the password
*/
onChange(newValue: boolean): void {
this.value = newValue;
this.changeValue.emit(this.value);
}
}

View File

@ -0,0 +1,15 @@
<div class="top">
<input
type="text"
[placeholder]="placeholder===undefined?'':placeholder"
required=""
[style.border]="hasError||notANumber? '2px dashed red' : ''"
[(ngModel)]="value"
(input)="onChangeValue($event.target.value)" />
<button class="eye-button-2 unselectable" tabindex="-1" (click)="onDecrement()" type="submit">
<i class="material-icons">arrow_back_ios</i>
</button>
<button class="eye-button unselectable" tabindex="-1" (click)="onIncrement()" type="submit">
<i class="material-icons">arrow_forward_ios</i>
</button>
</div>

View File

@ -0,0 +1,39 @@
input {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
z-index: 5;
}
.eye-button {
margin-left: -44px;
margin-top: 15px;
float: right;
position: relative;
display: block;
border: none;
z-index: 15;
background: none;
padding: 4 1 13 1;
:hover {
background: none;
color: red;
}
}
.eye-button-2 {
margin-left: -70px;
margin-top: 12px;
float: right;
position: relative;
display: block;
border: none;
z-index: 15;
background: none;
padding: 4px 30px 4px 1px;
:hover {
background: none;
color: red;
}
}

View File

@ -0,0 +1,70 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { isNullOrUndefined, isNumeric } from '../../utils';
@Component({
selector: 'app-entry-number',
templateUrl: 'entry-number.html',
styleUrls: ['entry-number.less'],
})
export class EntryNumberComponent implements OnInit {
/// Value of the password
@Input() value: string | undefined = '';
/// Placeholder of the Value
@Input() placeholder: string | undefined = '';
/// The element has an error
@Input() hasError: boolean = false;
/// event when change the value of the password
@Output() changeValue: EventEmitter<number | undefined> = new EventEmitter();
public notANumber: boolean = false;
ngOnInit(): void {
//if (value)
}
/**
* When input value change, need update the display and change the internal value.
* @param newValue New value set on the password
*/
onChangeValue(newValue: string): void {
if (newValue === "") {
this.value = undefined;
this.notANumber = false;
this.changeValue.emit(undefined);
return;
}
this.value = newValue;
this.notANumber = false;
if (!isNumeric(this.value)) {
this.notANumber = true;
}
const numValue = Number(this.value);
this.changeValue.emit(numValue);
}
onIncrement() {
this.notANumber = false;
let newValue = undefined;
if (isNullOrUndefined(this.value) || !isNumeric(this.value)) {
newValue = 0;
} else {
newValue = Number(this.value) + 1;
}
this.value = "" + newValue;
this.changeValue.emit(newValue);
}
onDecrement() {
this.notANumber = false;
let newValue = undefined;
if (isNullOrUndefined(this.value) || !isNumeric(this.value)) {
newValue = 0;
} else {
newValue = Number(this.value) - 1;
}
this.value = "" + newValue;
this.changeValue.emit(newValue);
}
}

View File

@ -0,0 +1,8 @@
<div>
<app-entry
[value]="value"
[placeholder]="placeholder"
[hasError]="state !== true"
(changeValue)="check($event)"></app-entry>
<app-error-message-state [value]="state"></app-error-message-state>
</div>

View File

@ -0,0 +1,9 @@
input[type='text'] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
z-index: 5;
}

View File

@ -0,0 +1,59 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { isNullOrUndefined } from '../../utils';
export type ReturnFunction = (a: boolean | string) => void;
export type CheckerParameter = {
value: string,
result: ReturnFunction
}
@Component({
selector: 'app-entry-validator',
templateUrl: 'entry-validator.html',
styleUrls: ['entry-validator.less'],
})
export class EntryValidatorComponent implements OnInit {
/// Value of the password
@Input() value: string = '';
/// Placeholder of the Value
@Input() placeholder: string = '';
/// The element has an error
@Output() checker: EventEmitter<CheckerParameter> = new EventEmitter();
/// event when change the value of the password
@Output() changeValue: EventEmitter<string> = new EventEmitter();
public state: boolean | string = false;
ngOnInit(): void {
//if (value)
}
updateStatus(value: boolean | string): void {
this.state = value
if (this.state === true) {
this.changeValue.emit(this.value);
}
}
check(newValue: string): void {
let self = this;
let lambdaCallBack = (value: boolean | string) => { self.updateStatus(value) };
this.value = newValue;
if (isNullOrUndefined(this.checker)) {
this.changeValue.emit(this.value);
return;
}
this.checker.emit({
value: newValue,
result: lambdaCallBack,
});
}
}

View File

@ -0,0 +1,9 @@
<div class="top">
<input
type="text"
[placeholder]="placeholder===undefined?'':placeholder"
required=""
[style.border]="hasError? '2px dashed red' : ''"
[value]="value===undefined?'':value"
(input)="onChangeValue($event.target.value)" />
</div>

View File

@ -0,0 +1,9 @@
input[type='text'] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
z-index: 5;
}

View File

@ -0,0 +1,68 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { PasswordEntryComponent } from '../password-entry/password-entry';
describe('PasswordEntryComponent global test', () => {
let component: PasswordEntryComponent;
let fixture: ComponentFixture<PasswordEntryComponent>;
let input: HTMLInputElement;
let button: HTMLButtonElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PasswordEntryComponent],
}).compileComponents();
fixture = TestBed.createComponent(PasswordEntryComponent);
component = fixture.componentInstance;
input = fixture.nativeElement.querySelector('div').querySelector('input');
button = fixture.nativeElement.querySelector('div').querySelector('button');
});
it('Test mode password (default)', () => {
fixture.detectChanges();
expect(input.textContent).toEqual('');
expect(button.textContent).toEqual('visibility_off');
expect(input.type).toEqual('password');
});
it('Test mode text', () => {
component.passwordVisibility = true;
fixture.detectChanges();
expect(input.textContent).toEqual('');
expect(button.textContent).toEqual('visibility');
expect(input.type).toEqual('text');
});
it('test click on hide button', fakeAsync(() => {
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(false);
button.click();
tick();
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(true);
expect(button.textContent).toEqual('visibility');
expect(input.type).toEqual('text');
button.click();
tick();
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(false);
expect(button.textContent).toEqual('visibility_off');
expect(input.type).toEqual('password');
}));
it('Set password', fakeAsync(() => {
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(false);
let tmpData = 'My beautifull Password';
input.value = tmpData;
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(input.textContent).toEqual(tmpData);
expect(component.value).toEqual(tmpData);
tmpData = '';
input.value = tmpData;
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(input.textContent).toEqual(tmpData);
expect(component.value).toEqual(tmpData);
}));
});

View File

@ -0,0 +1,35 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
@Component({
selector: 'app-entry',
templateUrl: 'entry.html',
styleUrls: ['entry.less'],
})
export class EntryComponent implements OnInit {
/// Value of the password
@Input() value: string|undefined = '';
/// Placeholder of the Value
@Input() placeholder: string|undefined = '';
/// The element has an error
@Input() hasError: boolean = false;
/// event when change the value of the password
@Output() changeValue: EventEmitter<string> = new EventEmitter();
ngOnInit(): void {
//if (value)
}
/**
* When input value change, need update the display and change the internal value.
* @param newValue New value set on the password
*/
onChangeValue(newValue: string): void {
this.value = newValue;
this.changeValue.emit(this.value);
}
}

View File

@ -0,0 +1,3 @@
@if(value !== true && value !== false && value !== undefined) {
<div class="error color-shadow-black">{{value}}</div>
}

View File

@ -0,0 +1,34 @@
.error {
background-color: #f44336;
position: absolute;
z-index: 10;
display: block;
max-width: 450px;
padding: 5px 8px;
margin: 2px 0 0;
font-size: 16px;
font-weight: 400;
border-style: solid;
border-width: 0px;
box-sizing: border-box;
&:after,
&:before {
bottom: 100%;
left: 25px;
border: solid transparent;
content: ' ';
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&:after {
border-bottom-color: #f44336;
border-width: 10px;
margin-left: -10px;
}
}

View File

@ -0,0 +1,17 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-error-message-state',
templateUrl: 'error-message-state.html',
styleUrls: ['error-message-state.less'],
})
export class ErrorMessageStateComponent {
/// Value of the password
@Input() value: boolean|string = false;
}

View File

@ -0,0 +1 @@
<p>error works!</p>

View File

@ -0,0 +1,18 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-error',
templateUrl: './error.html',
styleUrls: ['./error.less'],
})
export class ErrorComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@ -0,0 +1,17 @@
import { AsyncActionState, AsyncActionStatusComponent } from './async-action-status/async-status-component';
import { BurgerPropertyComponent } from './burger-property/burger-property';
import { CheckboxComponent } from './checkbox/checkbox';
import { EntryNumberComponent } from './entry-number/entry-number';
import { EntryValidatorComponent } from './entry-validator/entry-validator';
import { EntryComponent } from './entry/entry';
import { ErrorMessageStateComponent } from './error-message-state/error-message-state';
import { ErrorComponent } from './error/error';
import { PasswordEntryComponent } from './password-entry/password-entry';
import { PopInComponent } from './popin/popin';
import { RenderFormComponent } from './render-settings/render-form';
import { RenderSettingsComponent } from './render-settings/render-settings';
import { SpinnerComponent } from './spinner/spinner';
import { TopMenuComponent } from './top-menu/top-menu';
import { UploadFileComponent } from './upload-file/upload-file';
export { BurgerPropertyComponent, EntryNumberComponent, CheckboxComponent, RenderFormComponent, RenderSettingsComponent, ErrorMessageStateComponent, AsyncActionState, AsyncActionStatusComponent, EntryValidatorComponent, PopInComponent, TopMenuComponent, UploadFileComponent, ErrorComponent, SpinnerComponent, PasswordEntryComponent, EntryComponent };

View File

@ -0,0 +1,12 @@
<div class="top">
<input
[type]="passwordVisibility?'text':'password'"
[placeholder]="placeholder===undefined?'':placeholder"
required=""
[style.border]="hasError? '2px dashed red' : ''"
[value]="value"
(input)="onChangeValue($event.target.value)" />
<button class="eye-button unselectable" tabindex="-1" (click)="onVisibility()" type="submit">
<i class="material-icons">{{passwordVisibility?"visibility":"visibility_off"}}</i>
</button>
</div>

View File

@ -0,0 +1,25 @@
.eye-button {
margin-left: -44px;
margin-top: 15px;
float: right;
position: relative;
display: block;
border: none;
z-index: 15;
background: none;
padding: 4 1 13 1;
:hover {
background: none;
}
}
input[type='text'],
input[type='password'] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
z-index: 5;
}

View File

@ -0,0 +1,68 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { PasswordEntryComponent } from './password-entry';
describe('PasswordEntryComponent global test', () => {
let component: PasswordEntryComponent;
let fixture: ComponentFixture<PasswordEntryComponent>;
let input: HTMLInputElement;
let button: HTMLButtonElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PasswordEntryComponent],
}).compileComponents();
fixture = TestBed.createComponent(PasswordEntryComponent);
component = fixture.componentInstance;
input = fixture.nativeElement.querySelector('div').querySelector('input');
button = fixture.nativeElement.querySelector('div').querySelector('button');
});
it('Test mode password (default)', () => {
fixture.detectChanges();
expect(input.textContent).toEqual('');
expect(button.textContent).toEqual('visibility_off');
expect(input.type).toEqual('password');
});
it('Test mode text', () => {
component.passwordVisibility = true;
fixture.detectChanges();
expect(input.textContent).toEqual('');
expect(button.textContent).toEqual('visibility');
expect(input.type).toEqual('text');
});
it('test click on hide button', fakeAsync(() => {
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(false);
button.click();
tick();
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(true);
expect(button.textContent).toEqual('visibility');
expect(input.type).toEqual('text');
button.click();
tick();
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(false);
expect(button.textContent).toEqual('visibility_off');
expect(input.type).toEqual('password');
}));
it('Set password', fakeAsync(() => {
fixture.detectChanges();
expect(component.passwordVisibility).toEqual(false);
let tmpData = 'My beautifull Password';
input.value = tmpData;
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(input.textContent).toEqual(tmpData);
expect(component.value).toEqual(tmpData);
tmpData = '';
input.value = tmpData;
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(input.textContent).toEqual(tmpData);
expect(component.value).toEqual(tmpData);
}));
});

View File

@ -0,0 +1,39 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-password-entry',
templateUrl: 'password-entry.html',
styleUrls: ['password-entry.less'],
})
export class PasswordEntryComponent {
/// Value of the password
@Input() value: string = '';
/// Placeholder of the Value
@Input() placeholder: string = 'Write password.';
/// The element has an error
@Input() hasError: boolean = false;
/// event when change the value of the password
@Output() changeValue: EventEmitter<string> = new EventEmitter();
/// Local value of the password viwibility
public passwordVisibility: boolean = false;
/**
* Ov visibility request change (toggle the visibility)
*/
onVisibility(): void {
this.passwordVisibility = !this.passwordVisibility;
}
/**
* When input value change, need update the display and change the internal value.
* @param newValue New value set on the password
*/
onChangeValue(newValue: string): void {
this.value = newValue;
this.changeValue.emit(this.value);
}
}

View File

@ -0,0 +1,50 @@
@if(displayPopIn) {
<div class="popin">
<div class="element {{popSize}}">
<div class="header">
<label class="unselectable">{{popTitle}}</label>
@if(closeTopRight === true) {
<div class="close">
<button class="button-close color-shadow-black" (click)="onCloseTop()" type="submit">
<label class="unselectable"><i class="material-icons">close</i></label>
</button>
</div>
}
</div>
<div class="body">
<ng-content></ng-content>
</div>
<div class="footer">
@if(validateTitle !== null) {
<div class="action">
<button class="button color-shadow-black" (click)="onValidate()" type="submit">
<label class="unselectable"><i class="material-icons">done</i> {{validateTitle}}</label>
</button>
</div>
}
@if(saveTitle != null) {
<div class="action">
<button class="button color-shadow-black" (click)="onSave()" type="submit">
<label class="unselectable"><i class="material-icons">save_alt</i> {{saveTitle}}</label>
</button>
</div>
}
@if(otherTitle != null) {
<div class="action">
<button class="button color-shadow-black" (click)="onOther()" type="submit">
<label class="unselectable"><i class="material-icons">star</i> {{otherTitle}}</label>
</button>
</div>
}
@if(closeTitle != null) {
<div class="action">
<button class="button color-shadow-black" (click)="onClose()" type="submit">
<label class="unselectable"><i class="material-icons">close</i> {{closeTitle}}</label>
</button>
</div>
}
</div>
</div>
<div class="background"></div>
</div>
}

View File

@ -0,0 +1,87 @@
/* popin STYLES
-------------------------------*/
/* popins are hidden by default */
.popin {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 800;
.small {
top: 15%;
right: 35%;
bottom: 15%;
left: 35%;
}
.medium {
top: 15%;
right: 25%;
bottom: 15%;
left: 25%;
}
.big {
top: 15%;
right: 15%;
bottom: 15%;
left: 15%;
}
.element {
/* popin container fixed across whole screen */
position: fixed;
display: block;
/* z-index must be higher than .popin-background */
z-index: 1000;
/* enables scrolling for tall popins */
overflow: auto;
.header {
padding: 10px;
//display: block;
background: #88f;
//margin: 40px;
//margin: auto;
height: 40px;
line-height: 36px;
.close {
display: block;
float: right;
}
}
.body {
//display: block;
padding: 20px;
background: #fff;
/* margin exposes part of the popin background */
//margin: 40px;
//margin: auto;
}
.footer {
padding: 10px;
//display: block;
background: #888;
//margin: 40px;
//margin: auto;
height: 40px;
line-height: 36px;
.action {
padding: 0 10px;
display: block;
float: right;
}
}
}
// Pop in mush have a background over all the windows ...
.background {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: #000;
opacity: 0.85;
z-index: 900;
}
}

View File

@ -0,0 +1,82 @@
/** @file
* @author Edouard DUPIN
* @copyright 2020, Edouard DUPIN, all right reserved
* @license MPL-2 (see license file)
*/
import { Component, Input, Output, OnInit, OnDestroy, EventEmitter } from '@angular/core';
import { PopInService } from '../../service';
import { isNullOrUndefined } from '../../utils';
@Component({
// moduleId: module.id.toString(),
selector: 'app-popin',
templateUrl: './popin.html',
styleUrls: ['./popin.less'],
})
export class PopInComponent implements OnInit, OnDestroy {
@Input() id?: string;
@Input() popTitle: string = 'No title';
@Input() closeTopRight: any = 'false';
@Input() popSize: string = 'medium';
@Output() callback: EventEmitter<any> = new EventEmitter();
@Input() closeTitle: any = null;
@Input() validateTitle: any = null;
@Input() saveTitle: any = null;
@Input() otherTitle: any = null;
public displayPopIn: boolean = false;
constructor(private popInService: PopInService) { }
ngOnInit(): void {
// ensure id attribute exists
if (!this.id) {
console.error('popin must have an id');
return;
}
// move element to bottom of page (just before </body>) so it can be displayed above everything else
// this.element.appendTo('body');
this.popInService.add(this);
// this.element.hide();
}
// remove self from popIn service when directive is destroyed
ngOnDestroy(): void {
if (isNullOrUndefined(this.id)) {
return;
}
this.popInService.remove(this.id);
// this.element.remove();
}
// open popIn
open(): void {
//console.log(`open pop-in: ${this.id}`);
this.displayPopIn = true;
//this.element.show();
}
// close popin
close(): void {
this.displayPopIn = false;
// this.element.hide();
}
onCloseTop(): void {
this.callback.emit(['close-top']);
}
onValidate(): void {
this.callback.emit(['validate']);
}
onClose(): void {
this.callback.emit(['close']);
}
onOther(): void {
this.callback.emit(['other']);
}
onSave(): void {
this.callback.emit(['save']);
}
}

View File

@ -0,0 +1,64 @@
<table width="100%">
@for (elem of values; track elem.key;) {
<tr>
@if(elem.type === 'STRING' || elem.type === 'PASSWORD' || elem.type === 'NUMBER') {
<td width="15%"
[style.color]="getStyleRequireError(elem)"
[style.font-weight]="elem.require === true ? 'bold' : ''"
>{{elem.title}}</td>
}
@if(elem.type === 'BOOLEAN') {
<td width="15%"
[style.color]="getStyleRequireError(elem)"
[style.font-weight]="elem.require === true ? 'bold' : ''"
>{{elem.title}}</td>
}
@if(elem.type === 'VALUE') {
<td width="15%"><b>{{elem.title}}</b></td>
<td width="85%">{{elem.value}}</td>
}
@if(elem.type === 'STRING') {
<td width="85%">
<app-entry
[value]="elem.value"
[placeholder]="elem.placeholder"
[hasError]="checkHasError(elem)"
(changeValue)="checkParameter($event, elem)"></app-entry>
<app-error-message-state [value]="elem.state"></app-error-message-state>
</td>
}
@if(elem.type === 'NUMBER') {
<td width="85%">
<app-entry-number
[value]="elem.value"
[placeholder]="elem.placeholder"
[hasError]="checkHasError(elem)"
(changeValue)="checkParameter($event, elem)"></app-entry-number>
<app-error-message-state [value]="elem.state"></app-error-message-state>
</td>
}
@if(elem.type === 'PASSWORD') {
<td width="85%">
<app-password-entry
[value]="elem.value"
[placeholder]="elem.placeholder"
[hasError]="checkHasError(elem)"
(changeValue)="checkParameter($event, elem)"></app-password-entry>
<app-error-message-state [value]="elem.state"></app-error-message-state>
</td>
}
@if(elem.type === 'BOOLEAN') {
<td width="85%">
<app-checkbox
[value]="elem.value"
(changeValue)="checkParameter($event, elem)"></app-checkbox>
<app-error-message-state [value]="elem.state"></app-error-message-state>
</td>
}
@if(elem.type === 'LINE') {
<td width="85%"></td>
<td width="85%"></td>
}
</tr>
}
</table>

View File

@ -0,0 +1,17 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component } from '@angular/core';
import { RenderSettingsComponent } from './render-settings';
@Component({
selector: 'app-render-form',
templateUrl: 'render-form.html',
styleUrls: ['render-form.less'],
})
export class RenderFormComponent extends RenderSettingsComponent {
}

View File

@ -0,0 +1,49 @@
@for (elem of values; track elem.key;) {
<div class="settings-elements">
@if(elem.type === 'LINE'){
<div>
<div class="elem-hr"></div>
</div>
}
@else if(elem.type === 'BOOLEAN'){
<div>
<div class="elem-checkbox">
<app-checkbox
[value]="elem.value"
[comment]="elem.title"
(changeValue)="checkParameter($event, elem)"></app-checkbox>
</div>
<div class="elem-description">{{elem.description}}</div>
</div>
}
@else if(elem.type === 'STRING'){
<div>
<div class="elem-title">{{elem.title}}</div>
<div class="elem-description">{{elem.description}}</div>
<div class="elem-input">
<app-entry
[value]="elem.value"
[placeholder]="elem.placeholder"
[hasError]="elem.state !== undefined"
(changeValue)="checkParameter($event, elem)"></app-entry>
<app-error-message-state [value]="elem.state"></app-error-message-state>
</div>
</div>
}
@else if(elem.type === 'PASSWORD'){
<div>
<div class="elem-title">{{elem.title}}</div>
<div class="elem-description">{{elem.description}}</div>
<div class="elem-input">
<input [value]="elem.value" />
<app-password-entry
[value]="elem.value"
[placeholder]="elem.placeholder"
[hasError]="elem.state !== true"
(changeValue)="checkParameter($event, elem)"></app-password-entry>
<app-error-message-state [value]="elem.state"></app-error-message-state>
</div>
</div>
}
</div>
}

View File

@ -0,0 +1,32 @@
.settings-elements {
.elem-hr {
width: 100%;
border-color: black;
border: dashed;
border-width: 1px 0 0 0;
margin: 10px auto;
}
.elem-checkbox {
font-size: 18px;
font-weight: bold;
margin: 0 10px 0 10px;
}
.elem-title {
font-size: 18px;
font-weight: bold;
margin: 0 10px 0 36px;
}
.elem-description {
font-size: 14px;
margin: 0 20px 10px 50px;
font-style: italic;
}
.elem-input {
font-size: 14px;
margin: 0 20px 10px 36px;
input {
width: 100%;
}
}
}

View File

@ -0,0 +1,204 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { isInArray, isNullOrUndefined, isObject, isString, isOptionalOf, isBoolean, isNumber, isUndefined } from '../../utils';
export type ReturnFunction = (a: boolean | string) => void;
export type CheckerParameterType = string | number | boolean | undefined;
// if string ==> the error, if undefined, it is OK
export type CheckerParameter = (value: CheckerParameterType) => string | undefined;
export enum SettingType {
VALUE = 'VALUE',
LINE = 'LINE',
BOOLEAN = 'BOOLEAN',
NUMBER = 'NUMBER',
STRING = 'STRING',
PASSWORD = 'PASSWORD',
}
export function isSettingType(data: any): data is SettingType {
return isInArray(data, ['VALUE', 'LINE', 'NUMBER', 'BOOLEAN', 'STRING', 'PASSWORD']);
}
export interface SettingsItem {
// Type of the menu Node
type: SettingType;
// Unique key reference
key?: string;
// Displayed Title
title?: string;
// Description of the parameter
description?: string;
// placeholder of the parameter
placeholder?: string;
// Parameter key to SET/GET or the sub-menu
value?: boolean | string | Number;
// when data is change the value is set here undefined if not correct (must be set @ undefined):
newValue?: boolean | string | Number;
// checker to validate the data:
checker?: CheckerParameter
// result of the checker (must be set @ undefined):
state?: boolean | string;
// The element is require to have a valid form.
require?: boolean
}
export function isSettingsItem(data: any): data is SettingsItem {
if (isNullOrUndefined(data)) {
return false;
}
if (!isObject(data)) {
return false;
}
if (!isSettingType(data.type)) {
return false;
}
if (!isString(data.key)) {
return false;
}
if (!isOptionalOf(data.title, isString)) {
return false;
}
if (!isOptionalOf(data.description, isString)) {
return false;
}
if (!isOptionalOf(data.placeholder, isString)) {
return false;
}
if (
!isOptionalOf(data.value, isBoolean) &&
!isOptionalOf(data.value, isString) &&
!isOptionalOf(data.value, isNumber)
) {
return false;
}
if (!isUndefined(data.newValue)) {
return false;
}
if (!isOptionalOf(data.state, isString)) {
return false;
}
if (!isOptionalOf(data.require, isBoolean)) {
return false;
}
return true;
}
@Component({
selector: 'app-render-settings',
templateUrl: 'render-settings.html',
styleUrls: ['render-settings.less'],
})
export class RenderSettingsComponent {
/// Value of the password
@Input() values: SettingsItem[] = [];
/// Number of error detected (undefined: No change or not ready, else the number of error)
@Output() changeState: EventEmitter<Number> = new EventEmitter();
/// event with the changed values
@Output() deltaValues: EventEmitter<any> = new EventEmitter();
createOutputValues(): object {
let out = {};
//console.log(" Create values ... out ... ");
this.values.forEach((value) => {
if (isNullOrUndefined(value.key)) {
return;
}
//console.log(` key: ${value.key} : ${value.newValue}`);
if (!isNullOrUndefined(value.newValue)) {
//console.log(` ==> set`);
out[value.key] = value.newValue;
}
});
return out;
}
countErrors(): number | undefined {
let out = 0;
this.values.forEach((value) => {
if (!isUndefined(value.state)) {
out++;
}
});
return out;
}
checkMissing(): boolean | undefined {
let error = 0;
this.values.forEach((elem) => {
if (isNumber(elem.newValue)) {
if ((isUndefined(elem.newValue) && elem.value === undefined) || elem.newValue === undefined) {
error++;
}
} else {
if ((isUndefined(elem.newValue) && elem.value === "") || elem.newValue === "") {
error++;
}
}
});
return error !== 0;
}
getStyleRequireError(elem): string {
if (elem.require !== true) {
return "";
}
if (isNumber(elem.newValue)) {
//console.log(`>>>>>>>>>>>>>>>>>>>>>>>>>>>>><Is a number : ${elem.newValue} ${elem.value}`)
if ((isUndefined(elem.newValue) && elem.value === undefined) || elem.newValue === undefined) {
return "red";
}
} else {
if ((isUndefined(elem.newValue) && elem.value === "") || elem.newValue === "") {
return "red";
}
}
return "";
}
checkHasError(item: SettingsItem) {
if (!isUndefined(item.state)) {
return true;
}
if (item.require === true && item.value === "" && isUndefined(item.newValue)) {
return true;
}
return false;
}
checkParameter(newValue: string | number | boolean | undefined, item: SettingsItem): void {
if (isNullOrUndefined(newValue) || newValue === "") {
item.state = undefined
} else if (!isNullOrUndefined(item?.checker)) {
item.state = item?.checker(newValue);
}
if (item.value === newValue) {
item.newValue = undefined;
} else {
item.newValue = newValue;
}
const missing = this.checkMissing();
const outValue = this.createOutputValues();
const nbError = this.countErrors();
const nbValuesChanges = Object.keys(outValue).length;
//console.log(`outValue=${JSON.stringify(outValue, null, 2)}`);
//console.log(`nbError=${nbError} nbValuesChanges=${nbValuesChanges}`);
if (nbValuesChanges === 0) {
this.changeState.emit(undefined);
return;
}
if (missing === true) {
this.changeState.emit(undefined);
return;
}
this.changeState.emit(nbError);
if (nbError === 0) {
this.deltaValues.emit(outValue);
}
}
}

View File

@ -0,0 +1,14 @@
/** @file
* @author Edouard DUPIN
* @copyright 2023, Edouard DUPIN, all right reserved
* @license MPL-2 (see license file)
*/
import { Component } from '@angular/core';
@Component({
selector: 'spinner',
template: '<img height="50px" src="assets/images/load.svg"/>',
})
export class SpinnerComponent {
}

View File

@ -0,0 +1,92 @@
<div class="top">
<div id="main-menu" class="main-menu color-menu-background">
@for (data of menu; track data.title;) {
<div>
@if(isNotButton(data)){
<div
class="inert_element unselectable"
[ngStyle]="{'float': data.position}"
[ngClass]="getClassModel(data.model)">
@if( data.icon) {
<div class="xdesktop"><i class="material-icons">{{data.icon}}</i> {{data.title}}</div>
<div class="xmobile">
<i class="material-icons">{{data.icon}}</i>
</div>
}
@else {
<div class="xdesktop">{{data.title}}</div>
}
</div>
}
@else {
<button
class="item unselectable"
(click)="onGeneric(data, $event)"
(auxclick)="onGeneric(data, $event)"
[ngStyle]="{'float': data.position}"
[ngClass]="getClassModel(data.model)">
@if(data.image) {
<div class="avatar unselectable"><img class="avatar" src="{{data.image}}" /> {{data.title}}</div>
}
@else if(data.icon) {
<div class="xdesktop">
<i class="material-icons">{{data.icon}}</i> {{data.title}}
</div>
<div class="xmobile">
<i class="material-icons">{{data.icon}}</i>
</div>
}
@else {
<div class="xdesktop">{{data.title}}</div>
}
</button>
}
</div>
}
</div>
@if(subMenu) {
<div class="fill-all" (click)="onOutUserProperty()">
<div class="sub-menu color-menu-background unselectable" [ngClass]="getClassMenuPosition()">
@for (data of subMenu; track data.title;) {
<div>
@if( isNotButton(data)) {
<div
class="inert_element unselectable"
[ngStyle]="{'float': data.position}"
[ngClass]="getClassModel(data.model)">
@if(data.icon ) {
<div class="xdesktop" [ngStyle]="{'float': data.position}">
<i class="material-icons">{{data.icon}}</i> {{data.title}}
</div>
<div class="xmobile" [ngStyle]="{'float': data.position}">
<i class="material-icons">{{data.icon}}</i>
</div>
}
@else {
<div [ngStyle]="{'float': data.position}">{{data.title}}</div>
}
</div>
}
@else if (isEnable(data)) {
<button
class="item"
(click)="onGeneric(data, $event)"
(auxclick)="onGeneric(data, $event)"
[ngStyle]="{'float': data.position}"
[ngClass]="getClassModel(data.model)">
@if(data.icon ) {
<div [ngStyle]="{'float': data.position}">
<i class="material-icons">{{data.icon}}</i> {{data.title}}
</div>
}
@else {
<div [ngStyle]="{'float': data.position}">{{data.title}}</div>
}
</button>
}
</div>
}
</div>
</div>
}
</div>

View File

@ -0,0 +1,182 @@
.element-pos-left {
z-index: 5;
float: left;
}
.element-pos-right {
z-index: 5;
float: right;
}
.element-pos-center {
z-index: 5;
float: none;
}
.model_happy {
color: yellow;
}
.model_disable {
color: rgb(100, 100, 100);
}
.model_error {
color: darkred;
}
.top {
.sub-menu {
position: fixed;
min-width: 150px;
min-height: 70px;
display: block;
overflow: visible;
box-shadow: none;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-start;
box-sizing: border-box;
flex-shrink: 0;
margin: 0;
padding: 0 3px 0 3px;
border: none;
z-index: 300;
box-shadow: 0px 2px 4px 0 rgba(0, 0, 0, 0.6);
.item {
display: block;
float: top;
line-height: 56px;
z-index: 4;
margin: 3px 0px 3px 0px;
/*padding: 0px 3px 0px 3px;*/
border: 0px; /*0px 3px 0px 3px solid transparent;*/
/*text-transform: uppercase;*/
font-weight: bold;
font-size: 17px;
width: 100%;
}
.material-icons {
vertical-align: middle;
}
/* Create an Arraw on the top ob the box ... */
&:after,
&:before {
bottom: 100%;
right: 13px;
border: solid transparent;
content: ' ';
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&:after {
border-color: rgba(136, 183, 213, 0);
border-bottom-color: #263238;
border-width: 15px;
margin-left: -15px;
}
}
.menu-left {
top: 75px;
left: 15px;
}
.menu-right {
top: 75px;
right: 15px;
}
.fill-all {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
/*
background-color: #0F0;
*/
z-index: 400;
}
.main-menu {
position: fixed;
top: 0px;
left: 0px;
display: block;
overflow: visible;
box-shadow: none;
min-height: 56px;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-start;
box-sizing: border-box;
flex-shrink: 0;
width: 100%;
margin: 0;
padding: 0 12px 0 12px;
border: none;
max-height: 1000px;
z-index: 3;
box-shadow: 0px 2px 4px 0 rgba(0, 0, 0, 0.6);
.item {
display: block;
float: left;
line-height: 56px;
z-index: 4;
margin: 0 3px 0 3px;
border: 0;
text-transform: uppercase;
font-weight: bold;
font-size: 17px;
.comment {
visibility: 'hidden';
}
@media all and (min-width: 700px) {
.comment {
visibility: 'visible';
}
}
}
.inert_element {
display: block;
float: left;
line-height: 56px;
z-index: 4;
margin: 0 3px 0 3px;
border: 0;
//text-transform: uppercase;
font-weight: bold;
font-size: 17px;
}
.ariane {
display: block;
float: left;
line-height: 56px;
z-index: 4;
padding: 0 0 0 15px;
margin: 0 3px 0 3px;
border: 0;
text-transform: uppercase;
font-weight: bold;
font-size: 15px;
}
.material-icons {
vertical-align: middle;
}
.avatar {
height: 42px;
width: 42px;
border-radius: 50%;
vertical-align: middle;
}
}
}

View File

@ -0,0 +1,104 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
import { Router } from '@angular/router';
import { MenuItem } from '../../model/menu-item';
import { isNullOrUndefined } from '../../utils';
export interface EventOnMenu {
menu: MenuItem;
newWindows: boolean;
ctrl: boolean;
}
@Component({
selector: 'app-top-menu',
templateUrl: './top-menu.html',
styleUrls: ['./top-menu.less'],
})
export class TopMenuComponent implements OnInit {
@Input() menu?: MenuItem[];
subMenu?: MenuItem[] = undefined;
subMenuPosition?: String = undefined;
@Output() callback: EventEmitter<EventOnMenu> = new EventEmitter();
constructor(private router: Router) { }
isNotButton(data: MenuItem) {
return (
isNullOrUndefined(data.navigateTo) &&
(isNullOrUndefined(data.callback) || data.callback === false) &&
isNullOrUndefined(data.subMenu)
);
}
isEnable(data: MenuItem) {
if (!isNullOrUndefined(data) && !isNullOrUndefined(data.enable) && data.enable === false) {
return false;
}
return true;
}
getPosition(data: string): string {
return `float: ${data}`;
}
ngOnInit() { }
onOutUserProperty(): void {
//console.log('onOutUserProperty ==> event...');
this.subMenu = undefined;
}
getClassMenuPosition(): string {
if (isNullOrUndefined(this.subMenuPosition)) {
return 'menu-left';
}
return 'menu-' + this.subMenuPosition;
}
getClassModel(data?: string): string {
if (isNullOrUndefined(data)) {
return '';
}
return 'model_' + data;
}
onGeneric(data: MenuItem, event: any): void {
//console.log(`onGeneric()`);
// check if we need to navigate
if (!isNullOrUndefined(data.navigateTo)) {
// remove in every case the subMenu:
this.subMenu = undefined;
this.subMenuPosition = undefined;
this.router.navigate([data.navigateTo]);
return;
}
if (!isNullOrUndefined(data.callback) && data.callback === true) {
//console.log(`Emit message on ${JSON.stringify(data)}`);
this.callback.emit({
menu: data,
newWindows: event.which === 2,
ctrl: event.ctrlKey,
});
return;
}
// check if we need to display a submenu
if (isNullOrUndefined(data.subMenu)) {
//just a toggle mode:
data.subMenu = undefined;
this.subMenuPosition = undefined;
return;
}
if (this.subMenu === data.subMenu) {
//just a toggle mode:
this.subMenu = undefined;
this.subMenuPosition = undefined;
return;
}
//console.log(`Set Menu: ${JSON.stringify(data.subMenu)}`);
// set the requested menu
this.subMenu = data.subMenu;
this.subMenuPosition = data.position;
}
}

View File

@ -0,0 +1,11 @@
<div>
<div class="uploadfilecontainer" (click)="fileInput.click()" appDragDrop (onFileDropped)="uploadFile($event)">
<input hidden type="file" #fileInput (change)="uploadFile($event.target.files)" />
</div>
@for (file of files; track file.id; let i = $index) {
<div class="files-list">
<p>{{ file }}</p>
<button class="delete-file" (click)="deleteAttachment(i)">// DELETE //</button>
</div>
}
</div>

View File

@ -0,0 +1,39 @@
.uploadfilecontainer {
//background-image: url("../../../assets/cloud-2044823_960_720.png");
background-repeat: no-repeat;
background-size: 100px;
background-position: center;
height: 200px;
width: 80%;
margin: 20px auto;
border: 2px dashed #1c8adb;
border-radius: 10px;
}
.uploadfilecontainer:hover {
cursor: pointer;
background-color: #9ecbec !important;
opacity: 0.8;
}
.files-list {
display: flex;
justify-content: space-between;
width: 80%;
margin: 10px auto;
background: #ffffff;
border: 1px dashed;
border-radius: 12px;
padding: 5px;
color: #1c8adb;
}
.files-list .delete-file {
background: transparent;
border: none;
cursor: pointer;
}
.files-list .delete-file img {
width: 30px;
}

View File

@ -0,0 +1,25 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component } from '@angular/core';
@Component({
selector: 'app-upload-file',
templateUrl: './upload-file.html',
styleUrls: ['./upload-file.less'],
})
export class UploadFileComponent {
files: any = [];
uploadFile(event: any) {
for (let index = 0; index < event.length; index++) {
const element = event[index];
this.files.push(element.name);
}
}
deleteAttachment(index: any) {
this.files.splice(index, 1);
}
}

View File

@ -0,0 +1,9 @@
export * from './kar-cw.service';
export * from './kar-cw.module';
export * from './kar-cw.component';
export * from './utils';
export * from './service';
export * from './scene';
export * from './popin';
export * from './model';
export * from './component';

View File

@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import { PopInComponent, TopMenuComponent, UploadFileComponent, ErrorComponent, PasswordEntryComponent, EntryComponent, EntryValidatorComponent, SpinnerComponent, AsyncActionStatusComponent, ErrorMessageStateComponent, CheckboxComponent, BurgerPropertyComponent, RenderSettingsComponent, RenderFormComponent, EntryNumberComponent } from './component';
import { PopInUploadProgress, PopInDeleteConfirm } from './popin';
export const ALL_COMPONENTS = [
PopInComponent,
PopInUploadProgress,
PopInDeleteConfirm,
TopMenuComponent,
UploadFileComponent,
ErrorComponent,
PasswordEntryComponent,
EntryComponent,
EntryValidatorComponent,
SpinnerComponent,
AsyncActionStatusComponent,
ErrorMessageStateComponent,
CheckboxComponent,
BurgerPropertyComponent,
RenderSettingsComponent,
RenderFormComponent,
EntryNumberComponent,
];

View File

@ -0,0 +1,14 @@
/** @file
* @author Edouard DUPIN
* @copyright 2024, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { OnlyUsersGuard, OnlyAdminGuard, OnlyUsersGuardHome, OnlyUnregisteredGuardHome } from "./service";
export const ALL_GUARDS = [
OnlyUsersGuard,
OnlyAdminGuard,
OnlyUsersGuardHome,
OnlyUnregisteredGuardHome,
];

View File

@ -0,0 +1,39 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { CommonModule } from "@angular/common";
import { ALL_COMPONENTS } from "./kar-cw.component";
import { ALL_SCENES } from "./kar-cw.scene";
import { ALL_SERVICES } from "./kar-cw.service";
import { ALL_GUARDS } from "./kar-cw.guard";
@NgModule({
declarations: [
...ALL_COMPONENTS,
...ALL_SCENES
],
imports: [
FormsModule,
ReactiveFormsModule,
CommonModule,
],
providers: [
...ALL_SERVICES,
...ALL_GUARDS,
],
exports: [
...ALL_COMPONENTS,
...ALL_SCENES,
//...ALL_SERVICES,
//...ALL_GUARDS,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
})
export class KarCWModule { }

View File

@ -0,0 +1,6 @@
import { ForbiddenScene } from "./scene";
export const ALL_SCENES = [
ForbiddenScene,
];

View File

@ -0,0 +1,20 @@
/** @file
* @author Edouard DUPIN
* @copyright 2024, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { CookiesService, StorageService, PopInService, SessionService, UserService, SSOService, NotificationService } from "./service";
import { HttpWrapperService } from "./service/http-wrapper_kjdhqslkjf";
export const ALL_SERVICES = [
CookiesService,
HttpWrapperService,
StorageService,
PopInService,
SessionService,
UserService,
SSOService,
NotificationService,
];

View File

@ -0,0 +1,16 @@
export interface Environment {
production: boolean,
// URL of development API
applName: string,
defaultServer: string,
server: Map<string, string>,
ssoSite: string,
ssoSignIn: string,
ssoSignUp: string,
ssoSignOut: string,
tokenStoredInPermanentStorage: boolean,
};

View File

@ -0,0 +1,3 @@
import { isMenuItem, isMenuPosition, MenuItem, MenuPosition } from './menu-item';
export { MenuPosition, isMenuPosition, MenuItem, isMenuItem };

View File

@ -0,0 +1,67 @@
import { isObject, isOptionalOf, isString, isNullOrUndefined, isOptionalArrayOf, isInArray } from '../utils';
export enum MenuPosition {
LEFT = 'left',
RIGHT = 'right',
CENTER = 'none',
}
export function isMenuPosition(data: any): data is MenuPosition {
return isInArray(data, ['left', 'right', 'none']);
}
export interface MenuItem {
// Position of the menu element
position: MenuPosition;
// Hover help
hover?: string;
// Icon of the menu (need to be a material-icon name
icon?: string;
// Displayed Title
image?: string;
// Displayed Title
title: string;
// Model of the display:
model?: string;
// Jump Link (If undefined: it is considered as text and not a button)
navigateTo?: string;
// Menu model For a subList of elements
callback?: boolean;
// Other data that want to be set by the user
otherData?: any;
// Menu model For a subList of elements
enable?: boolean;
// Menu model For a subList of elements
subMenu?: MenuItem[];
}
export function isMenuItem(data: any): data is MenuItem {
if (isNullOrUndefined(data)) {
return false;
}
if (!isObject(data)) {
return false;
}
if (!isMenuPosition(data.position)) {
return false;
}
if (!isOptionalOf(data.hover, isString)) {
return false;
}
if (!isOptionalOf(data.icon, isString)) {
return false;
}
if (!isOptionalOf(data.image, isString)) {
return false;
}
if (!isString(data.title)) {
return false;
}
if (!isOptionalOf(data.navigateTo, isString)) {
return false;
}
if (!isOptionalArrayOf(data.subMenu, isMenuItem)) {
return false;
}
return true;
}

View File

@ -0,0 +1,19 @@
<div>
<app-popin
id="popin-delete-confirm"
popSize="small"
popTitle="Confirm Remove"
[closeTitle]="closeButtonTitle"
[validateTitle]="validateButtonTitle"
closeTopRight="true"
(callback)="eventPopUp($event[0])">
@if( imageUrl != null) {
<div class="expand">
<img src="{{imageUrl}}" class="cover" />
</div>
}
<p class="expand">
<label class="unselectable"><b>{{comment}}</b></label>
</p>
</app-popin>
</div>

View File

@ -0,0 +1,13 @@
.expand {
width: 100%;
input {
width: 100%;
}
div {
width: 100%;
}
textarea {
width: 100%;
}
text-align: center;
}

View File

@ -0,0 +1,36 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { PopInService } from '../../service';
@Component({
selector: 'delete-confirm',
templateUrl: './delete-confirm.html',
styleUrls: ['./delete-confirm.less'],
})
export class PopInDeleteConfirm implements OnInit {
@Input() comment?: string;
@Input() imageUrl?: string;
@Output() callback: EventEmitter<any> = new EventEmitter();
public closeButtonTitle: string = 'Cancel';
public validateButtonTitle: string = 'Validate';
constructor(private popInService: PopInService) { }
OnDestroy() { }
ngOnInit() { }
eventPopUp(_event: string): void {
console.log(`GET event: ${_event}`);
this.popInService.close('popin-delete-confirm');
if (_event === 'validate') {
this.callback.emit(null);
}
}
}

View File

@ -0,0 +1,4 @@
import { PopInDeleteConfirm } from './delete-confirm/delete-confirm';
import { PopInUploadProgress } from './upload-progress/upload-progress';
export { PopInDeleteConfirm, PopInUploadProgress };

View File

@ -0,0 +1,44 @@
<div>
<app-popin
id="popin-upload-progress"
popSize="medium"
popTitle="Upload Media File"
[closeTitle]="closeButtonTitle"
[otherTitle]="otherButtonTitle"
[validateTitle]="validateButtonTitle"
(callback)="eventPopUp($event[0])">
<p class="expand">
<label class="unselectable"><b>{{mediaTitle}}</b></label>
</p>
@if(progress != 100) {
<div class="progress-back">
<div class="progress-bar" style="width:{{progress}}%">&nbsp;&nbsp;&nbsp;{{progress}}%</div>
</div>
<div>
<label class="unselectable">Upload:</label><label style="text-align: right">{{uploadDisplay}}</label><br />
<label class="unselectable">Size:</label><label style="text-align: right">{{sizeDisplay}}</label>
</div>
}
@else if (error == null && result == null) {
<div>
<label class="unselectable">Upload done ... waiting server answer</label>
</div>
}
@else {
@if(error != null) {
<div>
<label class="unselectable"><b>Get an error From the server:</b></label
><br />
<label class="unselectable">{{error}}</label>
</div>
}
@if(result != null) {
<div>
<label class="unselectable"><b>Upload finished:</b></label
><br />
<label class="unselectable">{{result}}</label>
</div>
}
}
</app-popin>
</div>

View File

@ -0,0 +1,21 @@
.expand {
width: 100%;
input {
width: 100%;
}
textarea {
width: 100%;
}
}
.progress-back {
color: #000 !important;
background-color: #f1f1f1 !important;
border-radius: 3px;
}
.progress-bar {
color: #000000 !important;
background-color: #4caf50 !important;
border-radius: 3px;
}

View File

@ -0,0 +1,112 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, OnInit, Input, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router';
import { PopInService } from '../../service';
@Component({
selector: 'upload-progress',
templateUrl: './upload-progress.html',
styleUrls: ['./upload-progress.less'],
})
export class PopInUploadProgress implements OnInit {
@Input() mediaTitle: string = '';
@Input() mediaUploaded: number = 0;
@Input() mediaSize: number = 999999999999;
@Input() result?: string;
@Input() error?: string;
public closeButtonTitle?: string = 'Abort';
public otherButtonTitle?: string;
public validateButtonTitle?: string;
public uploadDisplay: string = '';
public sizeDisplay: string = '';
public progress: number = 0;
constructor(private router: Router, private popInService: PopInService) { }
OnDestroy() { }
ngOnInit() { }
eventPopUp(_event: string): void {
console.log(`GET event: ${_event}`);
this.popInService.close('popin-upload-progress');
}
updateNeedSend(): void { }
limit3(count: number): string {
if (count >= 1000) {
return `${count}`;
}
if (count >= 100) {
return ` ${count}`;
}
if (count >= 10) {
return ` ${count}`;
}
return ` ${count}`;
}
convertInHuman(countIn: number): string {
let count = countIn;
let tera = Math.trunc(count / (1024 * 1024 * 1024 * 1024));
count = count - tera * 1024 * 1024 * 1024 * 1024;
let giga = Math.trunc(count / (1024 * 1024 * 1024));
count = count - giga * 1024 * 1024 * 1024;
let mega = Math.trunc(count / (1024 * 1024));
count = count - mega * 1024 * 1024;
let kilo = Math.trunc(count / 1024);
count = count - kilo * 1024;
let out = '';
if (out.length !== 0 || tera !== 0) {
out = `${out} ${this.limit3(tera)}T`;
}
if (out.length !== 0 || giga !== 0) {
out = `${out} ${this.limit3(giga)}G`;
}
if (out.length !== 0 || mega !== 0) {
out = `${out} ${this.limit3(mega)}M`;
}
if (out.length !== 0 || kilo !== 0) {
out = `${out} ${this.limit3(kilo)}k`;
}
if (out.length !== 0 || count !== 0) {
out = `${out} ${this.limit3(count)}B`;
}
return out;
}
ngOnChanges(changes: SimpleChanges) {
//console.log(`Upload progress event : ${JSON.stringify(changes)}`);
this.progress = Math.trunc((this.mediaUploaded * 100) / this.mediaSize);
this.uploadDisplay = this.convertInHuman(this.mediaUploaded);
this.sizeDisplay = this.convertInHuman(this.mediaSize);
if (this.error === null && this.result === null) {
this.closeButtonTitle = 'Abort';
this.otherButtonTitle = undefined;
this.validateButtonTitle = undefined;
} else if (this.result === null) {
this.closeButtonTitle = undefined;
this.otherButtonTitle = 'Close';
this.validateButtonTitle = undefined;
} else {
this.closeButtonTitle = undefined;
this.otherButtonTitle = undefined;
this.validateButtonTitle = 'Ok';
}
}
}
export class UploadProgress {
labelMediaTitle: string = '';
mediaSendSize: number = 0;
mediaSize: number = 99999999999999;
result?: string;
error?: string;
clear() {
this.labelMediaTitle = '';
this.mediaSendSize = 0;
this.mediaSize = 99999999999999;
this.result = undefined;
this.error = undefined;
}
}

View File

@ -0,0 +1,15 @@
/*
* Public API Surface of kar-cw
*/
export * from './kar-cw.service';
export * from './kar-cw.module';
export * from './kar-cw.component';
export * from './kar-cw.scene';
export * from './kar-cw.guard';
export * from './utils';
export * from './service';
export * from './scene';
export * from './popin';
export * from './model';
export * from './component';

View File

@ -0,0 +1,8 @@
<div class="full-mode">
<div class="centered">
<div class="error">
<label class="unselectable"><i class="material-icons">report</i> 404 Not Found !</label>
</div>
<div class="comment"><label class="unselectable">Unknwn this page.</label></div>
</div>
</div>

View File

@ -0,0 +1,49 @@
.full-mode {
width: 100%;
height: 100%;
top: 0;
left: 0;
margin: 0;
padding: 0;
border: 0;
float: left;
display: block;
}
.centered {
position: relative;
max-width: 75%;
padding: 16px 32px 16px 32px;
top: 50%;
transform: ~'translate(0, -50%)';
font-size: 30px;
font-weight: 600;
text-align: center;
line-height: 200%;
}
.error {
font-size: 55px;
background-size: 45px;
/*background-attachment: fixed;*/
background-position: 0% 50%;
padding: 0 0 0 58px;
margin: 17px 0 17px 0;
text-shadow: 0px 0px 4px #000000;
color: rgb(160, 44, 44);
i {
font-size: 55px;
}
}
.comment {
background-size: 45px;
/*background-attachment: fixed;*/
background-position: 0% 50%;
padding: 0 0 0 58px;
margin: 17px 0 17px 0;
text-shadow: 0px 0px 4px #07213a;
color: white;
}

View File

@ -0,0 +1,16 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component } from '@angular/core';
@Component({
selector: 'app-404-not-found',
templateUrl: './404.html',
styleUrls: ['./404.less'],
})
export class NotFound404Scene {
constructor() {}
}

View File

@ -0,0 +1 @@
<p>error-viewer works!</p>

View File

@ -0,0 +1,19 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-error-viewer',
templateUrl: './error-viewer.html',
styleUrls: ['./error-viewer.less'],
})
export class ErrorViewerScene implements OnInit {
constructor(private route: ActivatedRoute) {}
ngOnInit() {}
}

View File

@ -0,0 +1,10 @@
<div class="full-mode">
<div class="centered">
<div class="error">
<label class="unselectable"><i class="material-icons">gpp_bad</i> 403 Forbidden</label>
</div>
<div class="comment">
<label class="unselectable">You don't have permission to access this resource.</label>
</div>
</div>
</div>

View File

@ -0,0 +1,50 @@
.full-mode {
width: 100%;
height: 100%;
top: 0;
left: 0;
margin: 0;
padding: 0;
border: 0;
float: left;
display: block;
}
.centered {
position: relative;
max-width: 500px;
padding: 16px 32px 16px 32px;
top: 50%;
left: 50%;
transform: ~'translate(-50%, -50%)';
font-size: 30px;
font-weight: 600;
text-align: left;
line-height: 200%;
}
.error {
font-size: 55px;
background-size: 45px;
/*background-attachment: fixed;*/
background-position: 0% 50%;
padding: 0 0 0 58px;
margin: 17px 0 17px 0;
text-shadow: 0px 0px 4px #000000;
color: rgb(160, 44, 44);
i {
font-size: 55px;
}
}
.comment {
background-size: 45px;
/*background-attachment: fixed;*/
background-position: 0% 50%;
padding: 0 0 0 58px;
margin: 17px 0 17px 0;
text-shadow: 0px 0px 4px #07213a;
color: white;
}

View File

@ -0,0 +1,16 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component } from '@angular/core';
@Component({
selector: 'app-forbidden',
templateUrl: './forbidden.html',
styleUrls: ['./forbidden.less'],
})
export class ForbiddenScene {
constructor() {}
}

View File

@ -0,0 +1,8 @@
<div class="full-mode">
<div class="centered">
<div class="error">
<label class="unselectable"><i class="material-icons">login</i> Not registered!</label>
</div>
<div class="comment"><label class="unselectable">you must login to access to this website.</label></div>
</div>
</div>

View File

@ -0,0 +1,49 @@
.full-mode {
width: 100%;
height: 100%;
top: 0;
left: 0;
margin: 0;
padding: 0;
border: 0;
float: left;
display: block;
}
.centered {
position: relative;
max-width: 75%;
padding: 16px 32px 16px 32px;
top: 50%;
transform: ~'translate(0, -50%)';
font-size: 30px;
font-weight: 600;
text-align: center;
line-height: 200%;
}
.error {
font-size: 55px;
background-size: 45px;
/*background-attachment: fixed;*/
background-position: 0% 50%;
padding: 0 0 0 58px;
margin: 17px 0 17px 0;
text-shadow: 0px 0px 4px #000000;
color: rgb(160, 44, 44);
i {
font-size: 55px;
}
}
.comment {
background-size: 45px;
/*background-attachment: fixed;*/
background-position: 0% 50%;
padding: 0 0 0 58px;
margin: 17px 0 17px 0;
text-shadow: 0px 0px 4px #07213a;
color: white;
}

View File

@ -0,0 +1,16 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component } from '@angular/core';
@Component({
selector: 'app-home-out',
templateUrl: './home-out.html',
styleUrls: ['./home-out.less'],
})
export class HomeOutScene {
constructor() {}
}

View File

@ -0,0 +1,8 @@
//import { ErrorViewerScene } from "./error-viewer/error-viewer";
import { ErrorViewerScene } from './error-viewer/error-viewer';
import { ForbiddenScene } from './forbidden/forbidden';
import { SsoScene } from './sso/sso';
import { HomeOutScene } from './home-out/home-out';
import { NotFound404Scene } from './404/404';
export { ErrorViewerScene, SsoScene, ForbiddenScene, HomeOutScene, NotFound404Scene };

View File

@ -0,0 +1,40 @@
<div class="full">
<div class="color-background-vignette container-global">
<div class="containerCenter">
<label for="login_field"><b>LOGIN (after SSO)</b></label>
</div>
<div class="imgContainer">
<img src="assets/images/avatar_generic.svg" alt="Avatar" class="avatar" />
</div>
@if(token === '__CANCEL__') {
<div class="container">
<label for="login_field"><b>ERROR: </b> Request cancel of connection !</label>
</div>
}
@else if(token === '__FAIL__') {
<div class="container">
<label for="login_field"><b>ERROR: </b> Connection FAIL !</label>
</div>
}
@else if(token === '__LOGOUT__') {
<div class="container">
<label for="login_field"><b>Dis-connected: </b> Redirect soon! </label>
</div>
}
@else if(token === 'ERROR_ME') {
<div class="container">
<label for="login_field"><b>ERROR: </b> can not retreive user local informations!</label>
</div>
}
@else {
<div
class="container">
<label for="login_field"><b>Connected: </b> Redirect soon!</label>
</div>
<div
class="container">
<label for="login_field"><b>Welcome back: </b> {{userName}}</label>
</div>
}
</div>
</div>

View File

@ -0,0 +1,108 @@
.full {
width: 100%;
height: 100%;
right: 0;
margin: 0;
padding: 0;
border: 0;
float: right;
display: block;
}
input[type='text'],
input[type='password'] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
.login {
margin: 2% 2%;
width: 45%;
}
.cancel {
margin: 2% 2%;
width: 45%;
}
.containerCenter {
text-align: center;
margin: 15px 0 0 0;
}
.imgContainer {
text-align: center;
margin: 15px 0 0 0;
}
img.avatar {
width: 150px;
border-radius: 50%;
}
.container-global {
padding: 16px 32px 16px 32px;
}
.container {
padding: 16px 0 0 0;
}
span.psw {
float: right;
padding-top: 16px;
}
.forgot {
color: #00b;
font-size: 14px;
float: right;
buttum: 0;
line-height: 24px;
}
.error {
background-color: #f44336;
position: absolute;
z-index: 10;
display: block;
max-width: 450px;
padding: 5px 8px;
margin: 2px 0 0;
font-size: 16px;
font-weight: 400;
border-style: solid;
border-width: 0px;
box-sizing: border-box;
&:after,
&:before {
bottom: 100%;
left: 25px;
border: solid transparent;
content: ' ';
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&:after {
border-bottom-color: #f44336;
border-width: 10px;
margin-left: -10px;
}
}
.container-global {
position: relative;
max-width: 400px;
padding: 16px 32px 16px 32px;
top: 50%;
left: 50%;
transform: ~'translate(-50%, -50%)';
box-shadow: 0px 8px 20px 0 rgba(0, 0, 0, 0.9);
}

View File

@ -0,0 +1,85 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { SSOService, UserService } from '../../service';
@Component({
selector: 'app-sso',
templateUrl: './sso.html',
styleUrls: ['./sso.less'],
})
export class SsoScene implements OnInit {
ssoData?: string;
token?: string;
keepConnected?: boolean;
userName?: string;
constructor(
private router: Router,
private route: ActivatedRoute,
private ssoService: SSOService,
private userService: UserService
) {
console.error('retrieve data after SSO');
}
ngOnInit() {
let self = this;
console.log(`call after SSO back: ${window.location.href}`);
const ssoData = this.route.snapshot.paramMap.get('data');
if (ssoData == null) {
this.ssoData = undefined;
} else {
this.ssoData = ssoData;
}
const token = this.route.snapshot.paramMap.get('token');
if (token == null) {
this.token = undefined;
} else {
this.token = token;
}
const keepConnected = this.route.snapshot.paramMap.get('keepConnected');
if (keepConnected == null) {
this.keepConnected = undefined;
} else {
this.keepConnected = keepConnected === 'true';
}
console.log(`ssoData: '${ssoData}'`);
console.log(`token: '${token}'`);
console.log(`keepConnected: '${keepConnected}'`);
if (this.token !== '__CANCEL__' && this.token !== '__FAIL__' && this.token !== '__LOGOUT__') {
const destination = this.ssoData;
console.log(`ssoData (decoded): '${destination}'`);
const realDst = this.ssoService.unHashLocalData(destination ?? '');
console.log(`realDst: '${realDst}'`);
this.userService
.startSession(this.token ?? '', this.keepConnected ?? false)
.then((userName: string) => {
self.userName = userName;
setTimeout(function () {
self.router.navigate([realDst], { replaceUrl: true });
}, 500); // 2 seconds
})
.catch(() => {
this.token = 'ERROR_ME';
});
} else if (this.token === '__LOGOUT__') {
this.userService.removeSession();
const destination = this.ssoData;
console.error(`ssoData (decoded): '${destination}'`);
// sample : ZZ**DST:home
const realDst = this.ssoService.unHashLocalData(destination ?? '');
console.error(`realDst: '${realDst}'`);
setTimeout(function () {
self.router.navigate([realDst], { replaceUrl: true });
}, 500); // 2 seconds
} else {
this.userService.removeSession();
}
}
}

View File

@ -0,0 +1,42 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Injectable } from '@angular/core';
@Injectable()
export class CookiesService {
constructor() {}
set(cname: string, cvalue: string, exdays: number): void {
if (this.get(cname) !== '') {
// reset previous cookies...
document.cookie = `${cname}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
}
let ddd = new Date();
ddd.setTime(ddd.getTime() + exdays * 24 * 60 * 60 * 1000);
let expires = `expires=${ddd.toUTCString()}`;
document.cookie = `${cname}=${cvalue};${expires};path=/`;
}
remove(cname: string): void {
this.set(cname, '', 0);
}
get(cname: string): string | undefined {
let name = `${cname}=`;
let coolies = document.cookie.split(';');
for (let iii = 0; iii < coolies.length; iii++) {
let ccc = coolies[iii];
while (ccc.charAt(0) === ' ') {
ccc = ccc.substring(1);
}
if (ccc.indexOf(name) === 0) {
return ccc.substring(name.length, ccc.length);
}
}
return '';
}
}

View File

@ -0,0 +1,497 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Inject, Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpEventType } from '@angular/common/http';
import { SessionService } from './session';
import { isArrayOfs, isBoolean, isNullOrUndefined, isNumber, isString } from '../utils';
import { Environment } from '../model/environment';
export enum HTTPRequestModel {
POST = 'POST',
GET = 'GET',
PUT = 'PUT',
DELETE = 'DELETE',
PATCH = 'PATCH',
}
export enum HTTPMimeType {
ALL = '*/*',
JSON = 'application/json',
OCTET_STREAM = 'application/octet-stream',
IMAGE = 'image/*',
IMAGE_JPEG = 'image/jpeg',
IMAGE_PNG = 'image/png',
}
export type UrlPath = string | boolean | number | bigint | (string | bigint | boolean | number)[];
export type ProgressCallback = (count: number, total: number) => void;
export interface HTTPRequest {
server?: string;
endPoint: UrlPath;
requestType: HTTPRequestModel;
accept: HTTPMimeType;
contentType: HTTPMimeType;
params?: object;
body?: any;
authorization?: string;
disableTocken?: boolean;
// progress interface to notify progression of the streaming.
progress?: ProgressCallback;
}
export interface ModelResponseHttp {
status: number;
data: any;
}
/**
* This service permit to add some data like token and authorization.. ir automatically get the token if needed and retake it if expired...
*/
@Injectable()
export class HttpWrapperService {
private displayReturn: boolean = false;
constructor(
// https://stackoverflow.com/questions/56883120/angular-service-provide-with-a-default-value
@Inject('ENVIRONMENT') private environment: Environment,
private http: HttpClient,
private session: SessionService) { }
addTokenIfNeeded(headerOption: any): any {
const token = this.session.getToken();
if (!isNullOrUndefined(token)) {
if (headerOption.Authorization === undefined) {
headerOption.Authorization = `Bearer ${token}`;
}
}
return headerOption;
}
request(properties: HTTPRequest): Promise<Response> {
let connectionAddresses = this.createRESTCall2({
server: properties.server,
api: properties.endPoint,
inputOptions: properties.params,
});
let headers: any = {
Accept: properties.accept,
//'Content-Type': properties.contentType,
};
if (properties.authorization !== undefined && properties.authorization !== null) {
headers['Authorization'] = properties.authorization;
}
if (properties.requestType !== HTTPRequestModel.GET) {
headers['Content-Type'] = properties.contentType;
}
if (
properties.disableTocken === undefined ||
properties.disableTocken === null ||
properties.disableTocken === false
) {
headers = this.addTokenIfNeeded(headers);
}
//console.log(`header: ${JSON.stringify(headers)}`);
let body = properties.body;
if (properties.contentType === HTTPMimeType.JSON) {
body = JSON.stringify(properties.body);
}
console.log(`Call ${connectionAddresses}`)
const result = fetch(connectionAddresses, {
method: properties.requestType,
headers,
body,
});
return result;
}
requestJson(properties: HTTPRequest): Promise<ModelResponseHttp> {
return new Promise((resolve, reject) => {
this.request(properties)
.then((response: Response) => {
if (response.status >= 200 && response.status <= 299) {
const contentType = response.headers.get('Content-Type');
if (contentType === HTTPMimeType.JSON) {
response
.json()
.then((value: any) => {
//console.log(`RECEIVE ==> ${response.status}=${ JSON.stringify(value, null, 2)}`);
resolve({ status: response.status, data: value });
})
.catch((reason: any) => {
reject({ status: 999, data: reason });
});
} else {
console.error(`content Type is not Json: ${contentType}`);
reject({ status: 999, data: `content Type is not Json: ${contentType}` });
}
} else {
console.error(`wring correct status: ${response.status}`);
reject({ status: 900, data: response });
}
})
.catch((error: any) => {
reject({
time: Date(),
status: 999,
error: error,
statusMessage: "Fetch error",
message: "http-wrapper.ts detect an error in the fetch request"
});
});
});
}
requestImage(properties: HTTPRequest): Promise<ModelResponseHttp> {
return new Promise((resolve, reject) => {
this.request(properties)
.then((response: Response) => {
if (response.status >= 200 && response.status <= 299) {
const contentType = response.headers.get('Content-Type');
//console.log(`REICEIVE ==> ${response.status}`);
resolve({ status: 900, data: response });
/*
resolve({ status:response.status, data:value });
}).catch((reason:any)=> {
reject({ status:999, data:reason });
});
} else {
console.error(`content Type is not Json: ${contentType}`)
reject({ status:999, data:`content Type is not Json: ${contentType}` });
}
*/
} else {
console.error(`wring correct status: ${response.status}`);
resolve({ status: 900, data: response });
}
})
.catch((error: any) => {
reject({
time: Date(),
status: 999,
error: error,
statusMessage: "Fetch image error",
message: "http-wrapper.ts detect an error in the fetch request"
});
});
});
}
createRESTCall2({
api,
server,
inputOptions,
addURLToken,
}: {
server?: string;
api: UrlPath;
inputOptions?: object;
addURLToken?: boolean;
}) {
if (isNullOrUndefined(server)) {
server = this.environment.defaultServer;
}
const basePage = this.environment.server[server];
let addressServerRest = basePage;
if (!basePage.endsWith('/')) {
addressServerRest = `${basePage}/`;
}
let options = inputOptions;
if (isNullOrUndefined(options)) {
options = {};
}
let out = addressServerRest;
if (isArrayOfs(api, isString, isNumber, isBoolean)) {
for (let iii = 0; iii < api.length; iii++) {
let elem = api[iii];
if (out.endsWith('/')) {
out += elem;
} else {
out += `/${elem}`;
}
}
} else {
out += api;
}
let first = true;
let keys = Object.keys(options);
for (let iii = 0; iii < keys.length; iii++) {
if (first === false) {
out = `${out}&`;
} else {
out = `${out}?`;
first = false;
}
out = out + keys[iii];
if (options[keys[iii]] != null) {
out = out + '=' + options[keys[iii]];
}
}
//console.log(`try to get session : ${this.session.getToken()}`);
if (!isNullOrUndefined(this.session.getToken())) {
if (addURLToken !== undefined && addURLToken === true) {
if (first === false) {
out = `${out}&`;
} else {
out = `${out}?`;
}
out = out + `Authorization=Bearer ${this.session.getToken()}`;
}
}
return out;
}
// Deprecated ...
createRESTCall(api: string, inputOptions?: any) {
let basePage = this.environment.server.get(this.environment.defaultServer);
let addressServerRest = `${basePage}/`;
let options = inputOptions;
if (options === undefined) {
options = [];
}
let out = addressServerRest + api;
let first = true;
for (let iii = 0; iii < options.length; iii++) {
if (first === false) {
out = `${out}&`;
} else {
out = `${out}?`;
first = false;
}
out = out + options[iii];
}
return out;
}
// Deprecated ... old model stream
post(uriRest: string, headerOption: any, data: any, progress?: ProgressCallback) {
//console.log(`-------------------------------------------------------\nHTTP-wrapper POST '${ uriRest }'\n\t\theaderOption=${ JSON.stringify(headerOption, null, 2)}\n\t\tdata=${ JSON.stringify(data, null, 2)}`);
this.addTokenIfNeeded(headerOption);
let connectionAddresses = this.createRESTCall(uriRest, {});
return new Promise((resolve, reject) => {
if (this.displayReturn === true) {
console.log(`call POST ${connectionAddresses} data=${JSON.stringify(data, null, 2)}`);
}
let request = this.http.post(connectionAddresses, data, {
headers: new HttpHeaders(headerOption),
reportProgress: true,
observe: 'events',
});
let self = this;
request.subscribe(
(res: any) => {
if (self.displayReturn === true) {
console.log(`!! data ${JSON.stringify(res, null, 2)}`);
}
if (res.type === HttpEventType.Sent) {
/* res.type === 0 */
//console.log('post : Sent');
} else if (res.type === HttpEventType.UploadProgress) {
/* res.type === 1 */
// console.log("post : " + res.loaded + " / " + res.total);
if (progress) {
progress(res.loaded, res.total);
}
} else if (res.type === HttpEventType.ResponseHeader) {
/* res.type === 2 */
//console.log('post : get header');
} else if (res.type === HttpEventType.DownloadProgress) {
/* res.type === 3 */
//console.log(`post : get DownloadProgress ${ res.loaded}`);
} else if (res.type === HttpEventType.Response) {
/* res.type === 4 */
//console.log('post : get response');
if (res.httpCode) {
resolve({ status: res.httpCode, data: res });
} else {
resolve({ status: 200, data: res });
}
} else if (res.type === HttpEventType.User) {
/* res.type === 5 */
//console.log('post : get User');
} else {
console.log(`post : get unknown ... ${res.type}`);
}
},
error => {
if (self.displayReturn === true) {
console.log(`an error occured status: ${error.status}`);
console.log(`answer: ${JSON.stringify(error, null, 2)}`);
}
reject({ status: error.status, data: error.error });
}
);
});
}
// Deprecated ... old model stream
uploadMultipart(base: string, multipart: FormData, progress: ProgressCallback): any {
//console.log(`Upload multipart to ${ base}`);
let url = base;
let self = this;
return new Promise((resolve, reject) => {
let headers = {
// 'Content-Type': 'multipart/form-data',
}; // new Headers();
self.post(url, headers, multipart, progress).then(
(response: any) => {
//console.log(`URL: ${ url }\nRespond(${ response.status }): ${ JSON.stringify(response.data, null, 2)}`);
if (response.status >= 200 && response.status <= 299) {
resolve(response.data.body);
return;
}
reject('An error occured');
},
(response: any) => {
if (typeof response.data === 'undefined') {
reject('return ERROR undefined');
} else {
reject('return ERROR ...'); // + JSON.stringify(response, null, 2));
}
}
);
});
}
// Complex wrapper to simplify interaction:
getSpecific(urlPath: UrlPath): any {
return new Promise((resolve, reject) => {
this.requestJson({
endPoint: urlPath,
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
contentType: HTTPMimeType.JSON,
}).then(
(response: ModelResponseHttp) => {
//console.log("URL: " + url + "\nRespond(" + response.status + "): " + JSON.stringify(response.data, null, 2));
if (response.status === 200) {
resolve(response.data);
return;
}
reject('An error occured');
},
(response: ModelResponseHttp) => {
if (isNullOrUndefined(response.data)) {
reject('return ERROR undefined');
} else {
reject(`return ERROR ${JSON.stringify(response.data, null, 2)}`);
}
}
);
});
}
// Complex wrapper to simplify interaction:s
patchSpecific(urlPath: UrlPath, data: object): Promise<ModelResponseHttp> {
return new Promise((resolve, reject) => {
this.request({
endPoint: urlPath,
requestType: HTTPRequestModel.PATCH,
accept: HTTPMimeType.JSON,
contentType: HTTPMimeType.JSON,
body: data,
})
.then((response: Response) => {
if (response.status >= 200 && response.status <= 201) {
const contentType = response.headers.get('Content-Type');
if (contentType === HTTPMimeType.JSON) {
response
.json()
.then((value: any) => {
//console.log(`REICEIVE ==> ${response.status}=${ JSON.stringify(value, null, 2)}`);
resolve({ status: response.status, data: value });
})
.catch((reason: any) => {
reject({ status: 999, data: reason });
});
} else {
console.error(`content Type is not Json: ${contentType}`);
reject({ status: 998, data: `content Type is not Json: ${contentType}` });
}
} else {
console.error(`wrong correct status: ${response.status}`);
reject({ status: 900, data: response });
}
})
.catch((error: any) => {
reject({ status: error.status, data: error.error });
});
});
}
postSpecific(urlPath: UrlPath, data: object): Promise<ModelResponseHttp> {
return new Promise((resolve, reject) => {
this.request({
endPoint: urlPath,
requestType: HTTPRequestModel.POST,
accept: HTTPMimeType.JSON,
contentType: HTTPMimeType.JSON,
body: data,
})
.then((response: Response) => {
if (response.status >= 200 && response.status <= 201) {
const contentType = response.headers.get('Content-Type');
if (contentType === HTTPMimeType.JSON) {
response
.json()
.then((value: any) => {
//console.log(`REICEIVE ==> ${response.status}=${ JSON.stringify(value, null, 2)}`);
resolve({ status: response.status, data: value });
})
.catch((reason: any) => {
reject({ status: 999, data: reason });
});
} else {
console.error(`content Type is not Json: ${contentType}`);
reject({ status: 998, data: `content Type is not Json: ${contentType}` });
}
} else {
console.error(`wrong correct status: ${response.status}`);
reject({ status: 900, data: response });
}
})
.catch((error: any) => {
reject({ status: error.status, data: error.error });
});
});
}
deleteSpecific(urlPath: UrlPath): Promise<ModelResponseHttp> {
return new Promise((resolve, reject) => {
this.request({
endPoint: urlPath,
requestType: HTTPRequestModel.DELETE,
accept: HTTPMimeType.JSON,
contentType: HTTPMimeType.JSON,
})
.then((response: Response) => {
if (response.status >= 200 && response.status <= 201) {
const contentType = response.headers.get('Content-Type');
if (contentType === HTTPMimeType.JSON) {
response
.json()
.then((value: any) => {
//console.log(`REICEIVE ==> ${response.status}=${ JSON.stringify(value, null, 2)}`);
resolve({ status: response.status, data: value });
})
.catch((reason: any) => {
reject({ status: 999, data: reason });
});
} else {
console.error(`content Type is not Json: ${contentType}`);
reject({ status: 998, data: `content Type is not Json: ${contentType}` });
}
} else {
console.error(`wrong correct status: ${response.status}`);
reject({ status: 900, data: response });
}
})
.catch((error: any) => {
reject({ status: error.status, data: error.error });
});
});
}
}

View File

@ -0,0 +1,27 @@
import { CookiesService } from './cookies';
import { StorageService } from './local-storage';
import { NotificationService } from './notification';
import { PopInService } from './popin';
import {
OnlyAdminGuard,
OnlyUnregisteredGuardHome,
OnlyUsersGuard,
OnlyUsersGuardHome,
SessionService,
} from './session';
import { SSOService } from './sso';
import { UserService } from './user';
export {
CookiesService,
StorageService,
PopInService,
SessionService,
UserService,
SSOService,
OnlyUsersGuard,
OnlyUsersGuardHome,
OnlyUnregisteredGuardHome,
OnlyAdminGuard,
NotificationService,
};

View File

@ -0,0 +1,54 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Inject, Injectable } from '@angular/core';
import { Environment } from '../model/environment';
@Injectable()
export class StorageService {
private baseLocalStorageName: string;
constructor(
@Inject('ENVIRONMENT') environment: Environment,
) {
this.baseLocalStorageName = environment.applName + '_';
}
set(cname: string, tmpValue: string): void {
//console.debug(`storage set: ${cname} : ${tmpValue}`);
localStorage.setItem(this.baseLocalStorageName + cname, tmpValue);
}
// limit at the current session ...
setSession(cname: string, tmpValue: string): void {
sessionStorage.setItem(this.baseLocalStorageName + cname, tmpValue);
}
remove(cname: string): void {
//console.debug(`storage remove: ${cname}`);
localStorage.removeItem(this.baseLocalStorageName + cname);
}
removeSession(cname: string): void {
sessionStorage.removeItem(this.baseLocalStorageName + cname);
}
get(cname: string): string | undefined {
//console.debug(`storage get: ${cname}`);
// TODO check expire day...
const data = localStorage.getItem(this.baseLocalStorageName + cname);
//console.debug(`get value form the storage (1): ${data}`)
if (data === null || data === undefined) {
return undefined;
}
return data;
}
getSession(cname: string): string | undefined {
const data = sessionStorage.getItem(this.baseLocalStorageName + cname);
if (data === null || data === undefined) {
return undefined;
}
return data;
}
}

View File

@ -0,0 +1,25 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Injectable } from '@angular/core';
@Injectable()
export class NotificationService {
constructor() {
console.log('Start Notification Service');
}
errorRaw(data: any) {
console.error(`get notification error: ${data}`)
}
warningRaw(data: any) {
console.info(`get notification warning: ${data}`)
}
infoRaw(data: any) {
console.info(`get notification info: ${data}`)
}
}

View File

@ -0,0 +1,55 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Injectable } from '@angular/core';
@Injectable()
export class PopInService {
private popinList: any[] = [];
constructor() {
console.log('Start PopIn Service');
}
add(_popin: any) {
// add popin to array of active popin
this.popinList.push(_popin);
}
remove(_id: string) {
// remove popin from array of active popin
for (let iii = 0; iii < this.popinList.length; iii++) {
if (this.popinList[iii].id === _id) {
this.popinList.splice(iii, 1);
return;
}
}
}
open(_id: string) {
console.log("Try to open pop-in: '" + _id + "'");
// open popin specified by id
for (let iii = 0; iii < this.popinList.length; iii++) {
console.log(` check: ${this.popinList[iii].id}`);
if (this.popinList[iii].id === _id) {
console.log(" ==>find it ...");
this.popinList[iii].open();
return;
}
}
console.log(" ==> NOT found !!!!!");
}
close(_id: string) {
// close popin specified by id
for (let iii = 0; iii < this.popinList.length; iii++) {
if (this.popinList[iii].id === _id) {
this.popinList[iii].close();
return;
}
}
}
}

View File

@ -0,0 +1,180 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Injectable, Output, EventEmitter, Inject } from '@angular/core';
import { Router } from '@angular/router';
import { Environment } from '../model/environment';
import { isNullOrUndefined } from '../utils';
export enum UserRoles222 {
admin = 'admin',
user = 'user',
guest = 'guest',
}
@Injectable()
export class SessionService {
private tokenJwt?: string;
public userLogin?: string;
public userId?: string;
public right: any = {};
@Output() change: EventEmitter<boolean> = new EventEmitter();
constructor(
@Inject('ENVIRONMENT') private environment: Environment,
) { }
/**
* @brief Create a new session.
*
* @param userId -
* @param userLogin -
* @param tokenJwt -
*/
create({
userId,
userLogin,
tokenJwt,
}: {
userId: string;
userLogin: string;
tokenJwt: string;
}) {
console.log(
`Session Create: userId=${userId} userLogin=${userLogin} tokenJwt = ${tokenJwt}`
);
this.tokenJwt = tokenJwt;
this.userId = userId;
this.userLogin = userLogin;
this.right = this.parseToken(tokenJwt);
console.log(`Retrieve right: ${JSON.stringify(this.right, null, 4)}`);
this.change.emit(true);
}
b64_to_utf8(str: string): string {
return decodeURIComponent(window.atob(str));
}
parseToken(token: string): any {
const cut = token.split('.');
const decoded = this.b64_to_utf8(cut[1]);
const jsonModel = JSON.parse(decoded);
if (isNullOrUndefined(jsonModel.right)) {
return {};
}
if (isNullOrUndefined(jsonModel.right[this.environment.applName])) {
return {};
}
return jsonModel.right[this.environment.applName];
}
/**
* @brief destroy the current session.
*/
destroy() {
console.log('Session REMOVE');
this.tokenJwt = undefined;
this.userId = undefined;
this.userLogin = undefined;
this.right = {};
this.change.emit(false);
}
getToken(): string | undefined {
return this.tokenJwt;
}
islogged() {
return this.userId !== null;
}
hasRight(type: UserRoles222): boolean {
if (type === UserRoles222.admin) {
if (isNullOrUndefined(this.right.ADMIN)) {
return false;
}
return this.right.ADMIN;
}
if (type === UserRoles222.user) {
if (isNullOrUndefined(this.right.USER)) {
return false;
}
return this.right.USER;
}
if (type === UserRoles222.guest) {
// all the other ... maybe unneeded
return true;
}
return false;
}
hasNotRight(rightType: UserRoles222) {
return !this.hasRight(rightType);
}
getLogin() {
return this.userLogin;
}
getAvatar() {
return 'assets/images/avatar_generic.svg';
/* This is not ready:
if(this.userAvatar === false) {
return 'assets/images/avatar_generic.svg';
}
return this.userId;
*/
}
}
@Injectable({ providedIn: 'root' })
export class OnlyUsersGuard {
constructor(private sessionService: SessionService, private router: Router) { }
canActivate() {
console.log('OnlyLoggedInUsers');
if (this.sessionService.hasRight(UserRoles222.user) || this.sessionService.hasRight(UserRoles222.admin)) {
return true;
} else {
this.router.navigateByUrl('/forbidden');
return false;
}
}
}
@Injectable({ providedIn: 'root' })
export class OnlyUsersGuardHome {
constructor(private sessionService: SessionService, private router: Router) { }
canActivate() {
if (this.sessionService.hasRight(UserRoles222.user) || this.sessionService.hasRight(UserRoles222.admin)) {
return true;
} else {
this.router.navigateByUrl('/unregistered');
return false;
}
}
}
@Injectable({ providedIn: 'root' })
export class OnlyUnregisteredGuardHome {
constructor(private sessionService: SessionService, private router: Router) { }
canActivate() {
if (!this.sessionService.islogged()) {
return true;
} else {
this.router.navigateByUrl('/home');
return false;
}
}
}
@Injectable({ providedIn: 'root' })
export class OnlyAdminGuard {
constructor(private sessionService: SessionService, private router: Router) { }
canActivate() {
if (this.sessionService.hasRight(UserRoles222.user)) {
return true;
} else {
this.router.navigateByUrl('/forbidden');
return false;
}
}
}

View File

@ -0,0 +1,138 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Inject, Injectable } from '@angular/core';
import { Environment } from '../model/environment';
import { getApplicationLocation, isInArray, isNullOrUndefined } from '../utils';
import { HttpWrapperService, HTTPRequestModel, HTTPMimeType, ModelResponseHttp } from './http-wrapper_kjdhqslkjf';
@Injectable()
export class SSOService {
signUpEnable?: boolean;
constructor(
@Inject('ENVIRONMENT') private environment: Environment,
private http: HttpWrapperService,
) {
console.log('Start SSOService');
}
utf8_to_b64(str: string): string {
// remove unneeded "=" padding
return window.btoa(encodeURIComponent(str)).replace('=', '');
}
b64_to_utf8(str: string): string {
return decodeURIComponent(window.atob(str));
}
/**
* Request SSO connection
*/
hashLocalData(data?: string): string {
if (
!isNullOrUndefined(data) &&
!isInArray(data, [
'',
'null',
'NULL',
'undefined',
'---',
'unregistered',
'unregistered/',
'forbidden',
'forbidden/',
])
) {
return this.utf8_to_b64(data);
}
const pathName = getApplicationLocation();
if (isInArray(pathName, ['sso', '/sso', '/sso/'])) {
return this.utf8_to_b64('home');
}
if (
!isNullOrUndefined(pathName) &&
!isInArray(pathName, [
'',
'null',
'NULL',
'undefined',
'---',
'unregistered',
'unregistered/',
'forbidden',
'forbidden/',
])
) {
return this.utf8_to_b64(pathName);
}
return this.utf8_to_b64('home');
}
/**
* Request SSO connection
*/
unHashLocalData(data: string): string | undefined {
if (isNullOrUndefined(data) || isInArray(data, ['', 'null', 'NULL', 'undefined', '---'])) {
return undefined;
}
try {
return this.b64_to_utf8(data);
} catch (ex) {
console.error(`Can not convert the data: ${data}`);
}
return undefined;
}
/**
* Request Open SSO Global website
*/
requestOpenSite(): void {
window.location.href = this.environment.ssoSite;
}
/**
* Request SSO connection
*/
requestSignIn(name?: string): void {
window.location.href = this.environment.ssoSignIn + this.hashLocalData(name);
}
/**
* Request SSO Disconnect
*/
requestSignOut(name?: string): void {
window.location.href = this.environment.ssoSignOut + this.hashLocalData(name);
}
/**
* Request SSO signUp
*/
requestSignUp(name?: string): void {
window.location.href = this.environment.ssoSignUp + this.hashLocalData(name);
}
/**
* Checo if the signUp is authorized (generate by te SSO)
* @returns a promise of the State for sign-up
*/
checkSignUpEnable(): Promise<boolean> {
let self = this;
return new Promise((resolve, reject) => {
if (isNullOrUndefined(self.signUpEnable)) {
this.http
.requestJson({
server: 'karso',
endPoint: 'system_config/is_sign_up_availlable',
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
contentType: HTTPMimeType.JSON,
})
.then((response: ModelResponseHttp) => {
self.signUpEnable = response.data.signup;
resolve(self.signUpEnable ?? false);
})
.catch((error: any) => {
reject(`return ERROR ${JSON.stringify(error, null, 2)}`);
});
return;
}
resolve(self.signUpEnable);
});
}
}

View File

@ -0,0 +1,267 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { Inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { StorageService } from '../service/local-storage';
import { SessionService } from './session';
import { SSOService } from './sso';
import { getApplicationLocation, isNullOrUndefined, sha512 } from '../utils';
import { HttpWrapperService, HTTPRequestModel, HTTPMimeType, ModelResponseHttp } from './http-wrapper_kjdhqslkjf';
import { Environment } from '../model/environment';
/*
interface MessageLogIn {
login: string;
method: string;
time: number;
password: string;
}
interface MessageAnswer_USER_CONNECT {
sessionId: string;
login: string;
eMail: string;
role: string;
avatar: string;
}
*/
@Injectable()
export class UserService {
// 0: Not hide password; 1 hide password;
private identificationVersion: number = 1;
private cookiesRememberMe = 'remember-me';
private cookiesToken = 'token';
constructor(
@Inject('ENVIRONMENT') private environment: Environment,
private router: Router,
private storageService: StorageService,
private http: HttpWrapperService,
private sessionService: SessionService,
private ssoService: SSOService
) {
console.log('Start UserService');
}
/**
* Disconnect the user from the current session ==> this remove the curreent Token
*/
logOut(): void {
this.removeSession();
this.ssoService.requestSignOut('home');
}
removeSession(): void {
this.storageService.remove(this.cookiesRememberMe);
this.storageService.removeSession(this.cookiesToken);
this.storageService.remove(this.cookiesToken);
this.sessionService.destroy();
}
private isTokenUpToDate(token?: string): boolean {
if (isNullOrUndefined(token) || token.length < 25) {
return false;
}
// Separate the Data:
const elems = token.split('.');
if (elems.length !== 3) {
return false;
}
// const tokenHeader = decodeURIComponent(window.atob( elems[0] ));
const tokenData = decodeURIComponent(window.atob(elems[1]));
// console.error(`Retreive local token: \nheader=${tokenHeader} \ndata=${tokenData}`);
const parsedData = JSON.parse(tokenData);
console.debug(
`Retreive token exp data=${new Date(parsedData.exp * 1000).toISOString()} < ${new Date().toISOString()}`
);
const expireIn = new Date(parsedData.exp * 1000);
const nowTime = new Date();
// TODO: set a marging of 2 hours...
return expireIn > nowTime;
}
getRememberMe(): boolean {
return this.storageService.get(this.cookiesRememberMe) === 'true';
}
/**
* Check if the system can be connected
*/
checkAutoConnect(): Promise<void> {
let locationOrigin = getApplicationLocation();
const self = this;
return new Promise<void>((resolve, reject) => {
// Need to use the windows global route to prevent the log in cycle ...
// And in the mlain application position, the route does not have curently root the page
let pathName = window.location.pathname;
// console.log("start Path-name: '" + pathName + "'");
// console.log("check with: '" + environment.applName + "/sso/" + "'");
if (pathName.startsWith('/sso/') || pathName.startsWith(this.environment.applName + '/sso/')) {
console.log(' ==> SSo section');
reject();
return;
}
console.log(' ==> Check if need reconnect?');
let rememberMe = self.getRememberMe();
// TODO: in case of jest reload ==> no need to manage the SSO ==> just keep the token ... it in enought...
let token: undefined | string = undefined;
if (
isNullOrUndefined(this.environment.tokenStoredInPermanentStorage) ||
this.environment.tokenStoredInPermanentStorage !== true
) {
token = self.storageService.getSession(self.cookiesToken);
} else {
token = self.storageService.get(self.cookiesToken);
}
// TODO: check validity of the Token:
if (self.isTokenUpToDate(token)) {
// remove in case of fail !!!
this.storageService.removeSession(this.cookiesToken);
this.storageService.remove(this.cookiesToken);
self.startSession(token ?? '', rememberMe)
.then(() => {
self.router.navigateByUrl(locationOrigin);
console.log(`update global URL = ${locationOrigin}`);
resolve();
})
.catch(() => {
// jump in the sign-in page (automatically of request remember-me)
if (rememberMe) {
// jump to the sso !!! (remove local data to prevent login loop)
this.storageService.remove(this.cookiesRememberMe);
this.storageService.remove(this.cookiesToken);
this.storageService.removeSession(this.cookiesToken);
self.ssoService.requestSignIn(locationOrigin);
reject();
}
resolve();
});
} else {
console.log(`Get previous connection ... `);
if (rememberMe) {
// jump to the sso !!! (remove local data to prevent login loop)
this.storageService.remove(this.cookiesRememberMe);
this.storageService.remove(this.cookiesToken);
this.storageService.removeSession(this.cookiesToken);
self.ssoService.requestSignIn(locationOrigin);
reject();
}
resolve();
}
});
}
startSession(token: string, rememberMe: boolean): Promise<string> {
const self = this;
return new Promise((resolve, reject) => {
self.retrieveMe(token)
.then((value2: boolean) => {
if (rememberMe === true) {
self.storageService.set(self.cookiesRememberMe, rememberMe ? 'true' : 'false');
}
if (
isNullOrUndefined(this.environment.tokenStoredInPermanentStorage) ||
this.environment.tokenStoredInPermanentStorage !== true
) {
self.storageService.setSession(self.cookiesToken, token);
} else {
self.storageService.set(self.cookiesToken, token);
}
resolve(self.sessionService.getLogin() ?? '');
})
.catch(() => {
reject('sdfsdfsdf');
});
});
}
retrieveMe(token: string): Promise<boolean> {
console.log(`AuthService.loginWithToken ... '${token}'`);
const self = this;
return new Promise((resolve, reject) => {
this.http
.requestJson({
// server: 'karso',
endPoint: 'users/me',
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
contentType: HTTPMimeType.JSON,
authorization: `Bearer ${token}`, // special case, the token is set after this request...
})
.then((response: ModelResponseHttp) => {
// TODO: check type ...
console.log(`loginWithToken : get some data to check: ${JSON.stringify(response.data)}`);
self.sessionService.create({
//sessionId: response.data.sessionId,
userId: response.data.id,
userLogin: response.data.login,
//userEMail: response.data.email,
//userAdmin: response.data.admin,
//userBlocked: response.data.blocked,
//userRemoved: response.data.removed,
//userAvatar: response.data.avatar,
tokenJwt: token,
});
resolve(true);
})
.catch((error: any) => {
reject(`return ERROR ${JSON.stringify(error, null, 2)}`);
});
});
}
create(login: string, email: string, password: string) {
return this.createSha(login, email, sha512(password));
}
createSha(login: string, email: string, password: string) {
let data = {
method: 'v?',
login: login,
email: email,
password: password,
};
console.log(`call users data=${JSON.stringify(data, null, 2)}`);
if (this.identificationVersion === 1) {
data.method = 'v1';
}
return new Promise((resolve, reject) => {
this.http
.requestJson({
server: 'karso',
endPoint: 'users',
requestType: HTTPRequestModel.POST,
accept: HTTPMimeType.JSON,
contentType: HTTPMimeType.JSON,
body: data,
})
.then((response: ModelResponseHttp) => {
// TODO: check type ...
console.log(`createSha : get some data to check: ${JSON.stringify(response.data)}`);
resolve(response.data);
})
.catch((error: any) => {
reject(`return ERROR ${JSON.stringify(error, null, 2)}`);
});
});
}
isAuthenticated(): boolean {
// return !!Session.userId;
return false;
}
isAuthorized(authorizedRoles: string): boolean {
/*
if (!angular.isArray(_authorizedRoles)) {
authorizedRoles = [_authorizedRoles];
}
return ( authService.isAuthenticated()
&& _authorizedRoles.indexOf(Session.userRole) !== -1
);
*/
return false;
}
}

View File

@ -0,0 +1,21 @@
//import { environment } from 'environments/environment';
export function getApplicationLocation(): string {
let pathName = location.pathname;
return pathName;
//console.log("start Path-name: '" + pathName + "'");
//console.log("check with: '" + environment.applName + "/sso/" + "'");
/*
if (pathName.startsWith('/' + environment.applName + '/')) {
pathName = pathName.substring(environment.applName.length + 2);
} else if (pathName.startsWith('/' + environment.applName)) {
pathName = pathName.substring(environment.applName.length + 1);
} else if (pathName.startsWith(environment.applName + '/')) {
pathName = pathName.substring(environment.applName.length + 1);
} else if (pathName.startsWith(environment.applName)) {
pathName = pathName.substring(environment.applName.length);
}
return pathName;
*/
}

View File

@ -0,0 +1,13 @@
export function arrayUnique(array: any[]) {
var a = array.concat();
for (var i = 0; i < a.length; ++i) {
for (var j = i + 1; j < a.length; ++j) {
if (a[i] === a[j])
a.splice(j--, 1);
}
}
return a;
}

View File

@ -0,0 +1,95 @@
/** @file
* @author Edouard DUPIN
* @copyright 2024, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { isNullOrUndefined } from "./validator";
export class DataStore<TYPE> {
private data?: TYPE[];
private dataPromise?: { resolve: (response: TYPE[]) => void, reject: (error: Error) => void }[];
constructor(private _gets: () => Promise<TYPE[]>, private primaryKey: string = "id") {
}
public getData(): Promise<TYPE[]> {
let self = this;
if (!isNullOrUndefined(this.data)) {
return new Promise((resolve, reject) => {
resolve(self.data ?? []);
});
}
if (isNullOrUndefined(this.dataPromise)) {
this.dataPromise = [];
return new Promise((resolve, reject) => {
self._gets().then((response: TYPE[]) => {
self.data = response;
if (self.dataPromise) {
for (let iii = 0; iii < self.dataPromise.length; iii++) {
self.dataPromise[iii].resolve(self.data);
}
}
resolve(self.data);
}).catch(error => {
console.log(`[E] ${self.constructor.name}: can not get data from remote server:`);
if (self.dataPromise) {
for (let iii = 0; iii < self.dataPromise.length; iii++) {
self.dataPromise[iii].reject(error);
}
}
reject(error);
});
});
}
return new Promise((resolve, reject) => {
if (!isNullOrUndefined(self.data)) {
resolve(self.data);
return;
}
if (self.dataPromise) {
self.dataPromise.push({ resolve: resolve, reject: reject });
}
});
}
public get(id: any): Promise<TYPE> {
const self = this;
return new Promise((resolve, reject) => {
self.getData()
.then((value: TYPE[]) => {
for (let iii = 0; iii < value.length; iii++) {
if (value[iii][this.primaryKey] == id) {
resolve(value[iii]);
return;
}
}
reject("Not FOUND");
}).catch(error => reject(error));
});
}
public updateValue(value: TYPE) {
console.log(`[E] Not implemented Updater`);
if (this.data) {
for (let iii = 0; iii < this.data.length; iii++) {
if (this.data[iii][this.primaryKey] == value[this.primaryKey]) {
this.data[iii] = value;
return;
}
}
this.data.push(value);
}
}
public delete(value: any): void {
if (this.data) {
for (let iii = 0; iii < this.data.length; iii++) {
if (this.data[iii][this.primaryKey] == value) {
this.data.splice(iii, 1);
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More