@corp-products/ui-components 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/eslint.config.js +34 -0
- package/ng-package.json +7 -0
- package/package.json +19 -0
- package/project.json +29 -0
- package/src/index.ts +13 -0
- package/src/lib/app-accordion/app-accordion.component.html +15 -0
- package/src/lib/app-accordion/app-accordion.component.scss +0 -0
- package/src/lib/app-accordion/app-accordion.component.spec.ts +21 -0
- package/src/lib/app-accordion/app-accordion.component.ts +21 -0
- package/src/lib/app-accordion/index.ts +2 -0
- package/src/lib/app-button/app-button.component.html +13 -0
- package/src/lib/app-button/app-button.component.scss +0 -0
- package/src/lib/app-button/app-button.component.ts +28 -0
- package/src/lib/app-button/app-button.ts +15 -0
- package/src/lib/app-button/index.ts +2 -0
- package/src/lib/app-dropdown-menu/app-dropdown-menu.component.html +25 -0
- package/src/lib/app-dropdown-menu/app-dropdown-menu.component.scss +39 -0
- package/src/lib/app-dropdown-menu/app-dropdown-menu.component.spec.ts +21 -0
- package/src/lib/app-dropdown-menu/app-dropdown-menu.component.ts +43 -0
- package/src/lib/app-dropdown-menu/app-dropdown-menu.ts +17 -0
- package/src/lib/app-dropdown-menu/index.ts +2 -0
- package/src/lib/app-dropdown-menu/menu-popup.pipe.ts +18 -0
- package/src/lib/app-tabs/app-tab.interface.ts +26 -0
- package/src/lib/app-tabs/app-tabs.component.html +35 -0
- package/src/lib/app-tabs/app-tabs.component.scss +103 -0
- package/src/lib/app-tabs/app-tabs.component.spec.ts +21 -0
- package/src/lib/app-tabs/app-tabs.component.ts +48 -0
- package/src/lib/app-tabs/index.ts +2 -0
- package/src/lib/confirmation-dialog/confirmation-dialog.component.html +54 -0
- package/src/lib/confirmation-dialog/confirmation-dialog.component.scss +0 -0
- package/src/lib/confirmation-dialog/confirmation-dialog.component.spec.ts +22 -0
- package/src/lib/confirmation-dialog/confirmation-dialog.component.ts +51 -0
- package/src/lib/dynamic-form/dynamic-form.component.html +39 -0
- package/src/lib/dynamic-form/dynamic-form.component.scss +0 -0
- package/src/lib/dynamic-form/dynamic-form.component.spec.ts +21 -0
- package/src/lib/dynamic-form/dynamic-form.component.ts +29 -0
- package/src/lib/dynamic-form/dynamic-form.interface.ts +56 -0
- package/src/lib/form-components/@utils/form-utils.ts +12 -0
- package/src/lib/form-components/@utils/validations/error-keys.enum.ts +24 -0
- package/src/lib/form-components/@utils/validations/form-validation.service.ts +53 -0
- package/src/lib/form-components/@utils/validations/index.ts +3 -0
- package/src/lib/form-components/@utils/validations/validation-message.pipe.ts +24 -0
- package/src/lib/form-components/components/auto-complete/auto-complete.component.html +48 -0
- package/src/lib/form-components/components/auto-complete/auto-complete.component.scss +0 -0
- package/src/lib/form-components/components/auto-complete/auto-complete.component.spec.ts +21 -0
- package/src/lib/form-components/components/auto-complete/auto-complete.component.ts +48 -0
- package/src/lib/form-components/components/base-input.component.ts +41 -0
- package/src/lib/form-components/components/date-picker/date-picker.component.html +53 -0
- package/src/lib/form-components/components/date-picker/date-picker.component.scss +4 -0
- package/src/lib/form-components/components/date-picker/date-picker.component.spec.ts +23 -0
- package/src/lib/form-components/components/date-picker/date-picker.component.ts +45 -0
- package/src/lib/form-components/components/input/input.component.html +59 -0
- package/src/lib/form-components/components/input/input.component.scss +3 -0
- package/src/lib/form-components/components/input/input.component.spec.ts +21 -0
- package/src/lib/form-components/components/input/input.component.ts +32 -0
- package/src/lib/form-components/components/select/select.component.html +90 -0
- package/src/lib/form-components/components/select/select.component.scss +0 -0
- package/src/lib/form-components/components/select/select.component.spec.ts +21 -0
- package/src/lib/form-components/components/select/select.component.ts +51 -0
- package/src/lib/form-components/components/select-button/select-button.component.html +21 -0
- package/src/lib/form-components/components/select-button/select-button.component.scss +0 -0
- package/src/lib/form-components/components/select-button/select-button.component.spec.ts +21 -0
- package/src/lib/form-components/components/select-button/select-button.component.ts +22 -0
- package/src/lib/form-components/components/switcher/switch.component.html +5 -0
- package/src/lib/form-components/components/switcher/switch.component.scss +0 -0
- package/src/lib/form-components/components/switcher/switch.component.spec.ts +21 -0
- package/src/lib/form-components/components/switcher/switch.component.ts +25 -0
- package/src/lib/form-components/index.ts +9 -0
- package/src/lib/form-components/interfaces/index.ts +1 -0
- package/src/lib/form-components/interfaces/label-value.ts +4 -0
- package/src/lib/ico-moon-icon/ico-moon-icon.component.ts +23 -0
- package/src/lib/read-more/read-more.component.html +17 -0
- package/src/lib/read-more/read-more.component.scss +0 -0
- package/src/lib/read-more/read-more.component.spec.ts +21 -0
- package/src/lib/read-more/read-more.component.ts +21 -0
- package/src/lib/side-bar/side-bar.component.html +25 -0
- package/src/lib/side-bar/side-bar.component.scss +5 -0
- package/src/lib/side-bar/side-bar.component.spec.ts +21 -0
- package/src/lib/side-bar/side-bar.component.ts +32 -0
- package/src/lib/side-bar-dynamic/data-injector.pipe.ts +15 -0
- package/src/lib/side-bar-dynamic/dynamic-sidebar.service.ts +116 -0
- package/src/lib/side-bar-dynamic/side-bar-dynamic.component.html +51 -0
- package/src/lib/side-bar-dynamic/side-bar-dynamic.component.scss +5 -0
- package/src/lib/side-bar-dynamic/side-bar-dynamic.component.spec.ts +21 -0
- package/src/lib/side-bar-dynamic/side-bar-dynamic.component.ts +37 -0
- package/src/lib/side-bar-dynamic/side-bar-utils.ts +30 -0
- package/src/lib/side-bar-dynamic/sidebar-config.ts +48 -0
- package/src/lib/user-autocomplete-card/user-autocomplete-card.component.html +20 -0
- package/src/lib/user-autocomplete-card/user-autocomplete-card.component.scss +0 -0
- package/src/lib/user-autocomplete-card/user-autocomplete-card.component.spec.ts +21 -0
- package/src/lib/user-autocomplete-card/user-autocomplete-card.component.ts +21 -0
- package/src/lib/user-info/user-info.component.html +10 -0
- package/src/lib/user-info/user-info.component.ts +11 -0
- package/tsconfig.json +25 -0
- package/tsconfig.lib.json +12 -0
- package/tsconfig.lib.prod.json +9 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
@if (dynamicDialogConfig.data) {
|
|
2
|
+
<div>
|
|
3
|
+
<div class="bg-gray-150 p-4 flex items-center">
|
|
4
|
+
<div class="flex-grow flex gap-2">
|
|
5
|
+
@if (dynamicDialogConfig.data.headerIcon) {
|
|
6
|
+
<i [class]="dynamicDialogConfig.data.headerIcon"></i>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
<h3 class="m-0 font-bold text-[16px]">
|
|
10
|
+
{{ dynamicDialogConfig.data.header }}
|
|
11
|
+
</h3>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<app-button (click)="close()" icon="font-icon-close" [style]="'secondary'" variant="text"/>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
@if (dynamicDialogConfig.data) {
|
|
18
|
+
<div class="p-4">
|
|
19
|
+
@if (dynamicDialogConfig.data.dialogIcon) {
|
|
20
|
+
<em [class]="dynamicDialogConfig.data.dialogIcon"></em>
|
|
21
|
+
}
|
|
22
|
+
<div>
|
|
23
|
+
{{ dynamicDialogConfig.data.message }}
|
|
24
|
+
</div>
|
|
25
|
+
@if (dynamicDialogConfig.data.hint) {
|
|
26
|
+
<div class="font-bold text-[14px] mt-2">
|
|
27
|
+
{{ dynamicDialogConfig.data.hint }}
|
|
28
|
+
</div>
|
|
29
|
+
}
|
|
30
|
+
</div>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@if (dynamicDialogConfig.data.inputForm) {
|
|
34
|
+
<app-dynamic-form [dynamicFormData]="dialogFormData"></app-dynamic-form>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
<div class="p-4 flex gap-3">
|
|
38
|
+
<app-button
|
|
39
|
+
[title]="dynamicDialogConfig.data?.confirmLabel || ('actions.confirm' | translate)" class="w-[50%]"
|
|
40
|
+
[disabled]="!!(dialogFormData && dialogFormData.formGroup?.invalid)"
|
|
41
|
+
[icon]="dynamicDialogConfig.data.confirmBtnIcon || ''"
|
|
42
|
+
[iconPos]="dynamicDialogConfig.data.confirmBtnPosition || 'left'"
|
|
43
|
+
[style]="dynamicDialogConfig.data.confirmBtnStyle ||'primary'"
|
|
44
|
+
(click)="submit()"/>
|
|
45
|
+
|
|
46
|
+
<app-button
|
|
47
|
+
[title]="dynamicDialogConfig.data?.closeLabel || ('actions.cancel' | translate)"
|
|
48
|
+
class="w-[50%]"
|
|
49
|
+
[style]="'secondary'"
|
|
50
|
+
[variant]="'outlined'"
|
|
51
|
+
(click)="close()"/>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
|
2
|
+
|
|
3
|
+
import { ConfirmationDialogComponent } from "./confirmation-dialog.component";
|
|
4
|
+
|
|
5
|
+
describe("SharedConfirmDialogComponent", () => {
|
|
6
|
+
let component: ConfirmationDialogComponent;
|
|
7
|
+
let fixture: ComponentFixture<ConfirmationDialogComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [ConfirmationDialogComponent]
|
|
12
|
+
}).compileComponents();
|
|
13
|
+
|
|
14
|
+
fixture = TestBed.createComponent(ConfirmationDialogComponent);
|
|
15
|
+
component = fixture.componentInstance;
|
|
16
|
+
fixture.detectChanges();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should create", () => {
|
|
20
|
+
expect(component).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Component, OnDestroy, OnInit, inject } from "@angular/core";
|
|
2
|
+
import { NavigationStart, Router } from "@angular/router";
|
|
3
|
+
import { AvatarModule } from "primeng/avatar";
|
|
4
|
+
import { filter, Subscription } from "rxjs";
|
|
5
|
+
import { AppButtonComponent } from "../app-button/app-button.component";
|
|
6
|
+
import { DialogService, DynamicDialog, DynamicDialogConfig, DynamicDialogRef } from "primeng/dynamicdialog";
|
|
7
|
+
import { DynamicFormComponent } from "../dynamic-form/dynamic-form.component";
|
|
8
|
+
import { DynamicFormData } from "../dynamic-form/dynamic-form.interface";
|
|
9
|
+
import {TranslatePipe} from "@ngx-translate/core";
|
|
10
|
+
|
|
11
|
+
@Component({
|
|
12
|
+
selector: "app-confirm-dialog",
|
|
13
|
+
templateUrl: "./confirmation-dialog.component.html",
|
|
14
|
+
styleUrls: ["./confirmation-dialog.component.scss"],
|
|
15
|
+
standalone: true,
|
|
16
|
+
imports: [AppButtonComponent, AvatarModule, DynamicDialog, DynamicFormComponent, TranslatePipe],
|
|
17
|
+
providers: [DialogService]
|
|
18
|
+
})
|
|
19
|
+
export class ConfirmationDialogComponent implements OnInit, OnDestroy {
|
|
20
|
+
router = inject(Router);
|
|
21
|
+
dialogService = inject(DialogService);
|
|
22
|
+
dynamicDialogConfig = inject(DynamicDialogConfig);
|
|
23
|
+
private readonly _ref = inject(DynamicDialogRef);
|
|
24
|
+
private readonly _subscription = new Subscription();
|
|
25
|
+
dialogFormData: DynamicFormData;
|
|
26
|
+
|
|
27
|
+
ngOnInit() {
|
|
28
|
+
// closing when navigating back from the browser
|
|
29
|
+
this._subscription.add(
|
|
30
|
+
this.router.events.pipe(filter((event) => event instanceof NavigationStart)).subscribe(() => {
|
|
31
|
+
if (this.dynamicDialogConfig) {
|
|
32
|
+
this._ref.close();
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
this.dialogFormData = this.dynamicDialogConfig.data?.inputForm;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ngOnDestroy(): void {
|
|
40
|
+
this._subscription.unsubscribe();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
submit() {
|
|
44
|
+
const submitData = { submitted: true, data: this.dialogFormData?.formGroup?.value };
|
|
45
|
+
this._ref.close(this.dynamicDialogConfig.data.inputForm ? submitData : true);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
close() {
|
|
49
|
+
this._ref.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<div class="border-t mx-4 py-2">
|
|
2
|
+
<form [formGroup]="formGroup">
|
|
3
|
+
<div class="grid grid-cols-12 gap-x-2">
|
|
4
|
+
@for (inputName of inputsNames; track $index) {
|
|
5
|
+
<div
|
|
6
|
+
[ngClass]="inputsMap[inputName].rowSize === 'half' ? 'col-span-6 md:col-span-6': 'col-span-12 md:col-span-12'">
|
|
7
|
+
@switch (inputsMap[inputName].fieldType) {
|
|
8
|
+
@case (fieldType.DATE_PICKER) {
|
|
9
|
+
<stc-date-picker
|
|
10
|
+
[minDate]="inputsMap[inputName]?.dateRange?.min"
|
|
11
|
+
[id]="inputsMap[inputName].inputId"
|
|
12
|
+
[control]="getFormControl(inputName, formGroup)"
|
|
13
|
+
[name]="inputName"
|
|
14
|
+
[label]="inputsMap[inputName].label"
|
|
15
|
+
[showIcon]="inputsMap[inputName].showIcon || true"
|
|
16
|
+
[isTimeOnly]="inputsMap[inputName].isTimeOnly || false"/>
|
|
17
|
+
}
|
|
18
|
+
@case (fieldType.SELECT_BUTTON) {
|
|
19
|
+
<stc-select-button
|
|
20
|
+
[control]="getFormControl(inputName, formGroup)"
|
|
21
|
+
[id]="inputsMap[inputName].inputId"
|
|
22
|
+
[name]="inputName"
|
|
23
|
+
[label]="inputsMap[inputName].label"
|
|
24
|
+
[options]="inputsMap[inputName].selectButtonOptions || []"/>
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
</div>
|
|
28
|
+
}
|
|
29
|
+
</div>
|
|
30
|
+
<div class="col-span-12">
|
|
31
|
+
<small class="p-error text-red-700">
|
|
32
|
+
@for (error of formGroup.errors | validationErrors: dynamicFormData.formValidationErrorsKeys;
|
|
33
|
+
track error) {
|
|
34
|
+
{{ error }}
|
|
35
|
+
}
|
|
36
|
+
</small>
|
|
37
|
+
</div>
|
|
38
|
+
</form>
|
|
39
|
+
</div>
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
|
2
|
+
import { DynamicFormComponent } from "./dynamic-form.component";
|
|
3
|
+
|
|
4
|
+
describe("DynamicFormComponent", () => {
|
|
5
|
+
let component: DynamicFormComponent;
|
|
6
|
+
let fixture: ComponentFixture<DynamicFormComponent>;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
await TestBed.configureTestingModule({
|
|
10
|
+
imports: [DynamicFormComponent]
|
|
11
|
+
}).compileComponents();
|
|
12
|
+
|
|
13
|
+
fixture = TestBed.createComponent(DynamicFormComponent);
|
|
14
|
+
component = fixture.componentInstance;
|
|
15
|
+
fixture.detectChanges();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should create", () => {
|
|
19
|
+
expect(component).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Component, Input, OnInit } from "@angular/core";
|
|
2
|
+
import { CommonModule } from "@angular/common";
|
|
3
|
+
import { FormGroup, ReactiveFormsModule } from "@angular/forms";
|
|
4
|
+
import { DynamicFormData, FormFieldTypeEnum, InputsMap } from "./dynamic-form.interface";
|
|
5
|
+
import {DatePickerComponent, FormUtils, SelectButtonComponent, ValidationErrorsPipe} from "../form-components";
|
|
6
|
+
import { TranslateModule } from "@ngx-translate/core";
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: "app-dynamic-form",
|
|
10
|
+
standalone: true,
|
|
11
|
+
imports: [CommonModule, ReactiveFormsModule, DatePickerComponent, ValidationErrorsPipe, TranslateModule, SelectButtonComponent],
|
|
12
|
+
templateUrl: "./dynamic-form.component.html",
|
|
13
|
+
styleUrl: "./dynamic-form.component.scss"
|
|
14
|
+
})
|
|
15
|
+
export class DynamicFormComponent implements OnInit {
|
|
16
|
+
@Input({ required: true }) dynamicFormData: DynamicFormData;
|
|
17
|
+
inputsNames: string[] = [];
|
|
18
|
+
formGroup: FormGroup;
|
|
19
|
+
inputsMap: InputsMap;
|
|
20
|
+
readonly fieldType = FormFieldTypeEnum;
|
|
21
|
+
getFormControl = FormUtils.getFormControl;
|
|
22
|
+
|
|
23
|
+
ngOnInit(): void {
|
|
24
|
+
this.formGroup = this.dynamicFormData?.formGroup as FormGroup;
|
|
25
|
+
this.inputsMap = this.dynamicFormData?.inputsMap as InputsMap;
|
|
26
|
+
this.inputsNames = Object.keys(this.inputsMap || {});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {FormGroup} from "@angular/forms";
|
|
2
|
+
import {LabelValue} from "../form-components";
|
|
3
|
+
|
|
4
|
+
export type InputType = "text" | "textarea";
|
|
5
|
+
export type InputContentType = "text" | "email" | "password" | "number";
|
|
6
|
+
|
|
7
|
+
export interface Dropdown<T = unknown> {
|
|
8
|
+
id?: string;
|
|
9
|
+
keyValue: T;
|
|
10
|
+
label: string;
|
|
11
|
+
hidden?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface InputsMapData {
|
|
16
|
+
// General props
|
|
17
|
+
label: string;
|
|
18
|
+
rowSize?: "half" | "full";
|
|
19
|
+
fieldType: FormFieldTypeEnum;
|
|
20
|
+
inputId?: string;
|
|
21
|
+
|
|
22
|
+
// Date
|
|
23
|
+
dateRange?: DateRangeInterface;
|
|
24
|
+
isTimeOnly?: boolean;
|
|
25
|
+
showIcon?: boolean;
|
|
26
|
+
|
|
27
|
+
//select button
|
|
28
|
+
selectButtonOptions?: LabelValue<any>[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface InputsMap {
|
|
32
|
+
[key: string]: InputsMapData;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DynamicFormData {
|
|
36
|
+
isActive?: boolean;
|
|
37
|
+
formGroup: FormGroup | null;
|
|
38
|
+
inputsMap: InputsMap | null;
|
|
39
|
+
title?: string;
|
|
40
|
+
isReadOnlyForm?: boolean;
|
|
41
|
+
formValidationErrorsKeys?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DateRangeInterface {
|
|
45
|
+
min?: Date | null; // Static range, hard coded
|
|
46
|
+
max?: Date | null; // Static range, hard coded
|
|
47
|
+
notBeforeDateInput?: string; // For dynamic date range validation
|
|
48
|
+
notAfterDateInput?: string; // For dynamic date range validation
|
|
49
|
+
notBeforeOrSameDateInput?: string;
|
|
50
|
+
notAfterOrSameDateInput?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export enum FormFieldTypeEnum {
|
|
54
|
+
DATE_PICKER = "date-picker",
|
|
55
|
+
SELECT_BUTTON = "select-button"
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {FormControl, FormGroup} from "@angular/forms";
|
|
2
|
+
|
|
3
|
+
export class FormUtils {
|
|
4
|
+
static getFormControl(controlName: string, form: FormGroup): FormControl {
|
|
5
|
+
if (!form) throw new Error(`Form is not initialized.`);
|
|
6
|
+
const formControl = form.get(controlName) as FormControl;
|
|
7
|
+
|
|
8
|
+
if (!formControl) throw new Error(`There's no form control with given name. '${controlName}'`);
|
|
9
|
+
|
|
10
|
+
return formControl;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export enum BasicErrorKeysEnum {
|
|
2
|
+
required = "REQUIRED",
|
|
3
|
+
email = "EMAIL",
|
|
4
|
+
pattern = "PATTERN",
|
|
5
|
+
invalidArFormat = "INVALID_AR_FORMAT",
|
|
6
|
+
invalidLink = "INVALID_LINK",
|
|
7
|
+
endDateBeforeStartDate = "END_DATE_BEFORE_START_DATE",
|
|
8
|
+
startDateEqualsEndDate = "START_DATE_EQUALS_END_DATE",
|
|
9
|
+
endTimeBeforeStartTime = "END_TIME_BEFORE_START_TIME",
|
|
10
|
+
startTimeEqualsEndTime = "START_TIME_EQUALS_END_TIME",
|
|
11
|
+
integer = "INTEGER",
|
|
12
|
+
positiveNumber = "POSITIVE_NUMBER",
|
|
13
|
+
fileSelected = "FILE_SELECTED",
|
|
14
|
+
default = "DEFAULT"
|
|
15
|
+
}
|
|
16
|
+
export enum ErrorsWithValuesKeysEnum {
|
|
17
|
+
minlength = "MIN_LENGTH",
|
|
18
|
+
maxlength = "MAX_LENGTH",
|
|
19
|
+
min = "MIN",
|
|
20
|
+
max = "MAX",
|
|
21
|
+
maxSize = "MAX_SIZE",
|
|
22
|
+
maxFiles = "MAX_FILES",
|
|
23
|
+
allowedTypes = "ALLOWED_TYPES"
|
|
24
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { inject, Injectable } from "@angular/core";
|
|
2
|
+
import { TranslateService } from "@ngx-translate/core";
|
|
3
|
+
import { InterpolationParameters } from "@ngx-translate/core/lib/translate.service";
|
|
4
|
+
import { BasicErrorKeysEnum, ErrorsWithValuesKeysEnum } from "./error-keys.enum";
|
|
5
|
+
|
|
6
|
+
@Injectable({
|
|
7
|
+
providedIn: "root"
|
|
8
|
+
})
|
|
9
|
+
export class FormValidationService {
|
|
10
|
+
private translate = inject(TranslateService);
|
|
11
|
+
|
|
12
|
+
private getTranslation(key: string, interpolateParams?: InterpolationParameters): string {
|
|
13
|
+
return this.translate.instant(`VALIDATION.${key}`, interpolateParams);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getErrorMessage(errorKey: string, errorValue: any): string {
|
|
17
|
+
if (this.isBasicErrorKey(errorKey)) {
|
|
18
|
+
return this.getTranslation(BasicErrorKeysEnum[errorKey as keyof typeof BasicErrorKeysEnum]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (this.isErrorWithValueKey(errorKey)) {
|
|
22
|
+
return this.getErrorWithValueMessage(errorKey as keyof typeof ErrorsWithValuesKeysEnum, errorValue);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return this.getTranslation(BasicErrorKeysEnum.default);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Basic error keys are the keys that don't have any values to interpolate. like required, email, etc.
|
|
29
|
+
private isBasicErrorKey(key: string): key is keyof typeof BasicErrorKeysEnum {
|
|
30
|
+
return Object.keys(BasicErrorKeysEnum).includes(key as BasicErrorKeysEnum);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Error keys with values are the keys that have values to interpolate. like minlength, maxlength, etc.
|
|
34
|
+
private isErrorWithValueKey(key: string): key is keyof typeof ErrorsWithValuesKeysEnum {
|
|
35
|
+
return Object.keys(ErrorsWithValuesKeysEnum).includes(key as ErrorsWithValuesKeysEnum);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private getErrorWithValueMessage(errorKey: keyof typeof ErrorsWithValuesKeysEnum, errorValue: any): string {
|
|
39
|
+
const messages: Record<keyof typeof ErrorsWithValuesKeysEnum, (value: any) => string> = {
|
|
40
|
+
minlength: (val) =>
|
|
41
|
+
this.getTranslation(ErrorsWithValuesKeysEnum.minlength, { requiredLength: val?.requiredLength, actualLength: val?.actualLength }),
|
|
42
|
+
maxlength: (val) =>
|
|
43
|
+
this.getTranslation(ErrorsWithValuesKeysEnum.maxlength, { requiredLength: val?.requiredLength, actualLength: val?.actualLength }),
|
|
44
|
+
min: (val) => this.getTranslation(ErrorsWithValuesKeysEnum.min, { min: val?.min }),
|
|
45
|
+
max: (val) => this.getTranslation(ErrorsWithValuesKeysEnum.max, { max: val?.max }),
|
|
46
|
+
maxSize: (val) => this.getTranslation(ErrorsWithValuesKeysEnum.maxSize, { size: val?.requiredLength }),
|
|
47
|
+
maxFiles: (val) => this.getTranslation(ErrorsWithValuesKeysEnum.maxFiles, { size: val?.requiredLength }),
|
|
48
|
+
allowedTypes: (val) => this.getTranslation(ErrorsWithValuesKeysEnum.allowedTypes, { types: val?.join(", ") })
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return messages[errorKey](errorValue);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { inject, Pipe, PipeTransform } from "@angular/core";
|
|
2
|
+
import { ValidationErrors } from "@angular/forms";
|
|
3
|
+
import { FormValidationService } from "./form-validation.service";
|
|
4
|
+
|
|
5
|
+
@Pipe({
|
|
6
|
+
name: "validationErrors",
|
|
7
|
+
standalone: true,
|
|
8
|
+
pure: true
|
|
9
|
+
})
|
|
10
|
+
export class ValidationErrorsPipe implements PipeTransform {
|
|
11
|
+
private formValidationService = inject(FormValidationService);
|
|
12
|
+
|
|
13
|
+
// allowed keys here to handle errors in case of cross-validators like startDate and endDate validators,
|
|
14
|
+
// we pass this custom key to handle the error messages only for the allowed keys
|
|
15
|
+
transform(errors: ValidationErrors | null, allowedKeys?: string[]): string[] {
|
|
16
|
+
if (!errors) return [];
|
|
17
|
+
|
|
18
|
+
return Object.keys(errors)
|
|
19
|
+
.filter((errorKey) => !allowedKeys || allowedKeys.includes(errorKey)) // Filter errors if allowedKeys are provided
|
|
20
|
+
.map((errorKey) => {
|
|
21
|
+
return this.formValidationService.getErrorMessage(errorKey, errors[errorKey]);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<div class="field flex flex-col gap-2 my-3 relative">
|
|
2
|
+
@if (label) {
|
|
3
|
+
<label [for]="inputId">
|
|
4
|
+
{{ label }}
|
|
5
|
+
@if (required) {
|
|
6
|
+
<span [class.text-red-700]="isInvalid">*</span>
|
|
7
|
+
}
|
|
8
|
+
</label>
|
|
9
|
+
}
|
|
10
|
+
@if (!required) {
|
|
11
|
+
<span class="absolute top-[6px] left-0 text-[10px] text-gray-400">{{'forms.config.optional' | translate}}</span>
|
|
12
|
+
}
|
|
13
|
+
<p-auto-complete
|
|
14
|
+
(completeMethod)="search($event)"
|
|
15
|
+
(onSelect)="onSelect($event)"
|
|
16
|
+
[delay]="delay"
|
|
17
|
+
[disabled]="disabled"
|
|
18
|
+
[formControl]="control"
|
|
19
|
+
[id]="inputId"
|
|
20
|
+
[inputStyleClass]="'reset-default-styles w-full' + (basicInput ? ' basic-style': ' ')"
|
|
21
|
+
[minLength]="minLengthToSearch"
|
|
22
|
+
[name]="name"
|
|
23
|
+
[ngClass]="{ 'p-invalid ng-dirty ng-invalid': isInvalid}"
|
|
24
|
+
[placeholder]="placeholder"
|
|
25
|
+
[styleClass]="'w-full'"
|
|
26
|
+
[suggestions]="items">
|
|
27
|
+
<ng-template let-item pTemplate="item">
|
|
28
|
+
@if (selectedItemTemplate) {
|
|
29
|
+
<ng-container
|
|
30
|
+
[ngTemplateOutletContext]="{ $implicit: item }"
|
|
31
|
+
[ngTemplateOutlet]="selectedItemTemplate"
|
|
32
|
+
/>
|
|
33
|
+
}
|
|
34
|
+
</ng-template>
|
|
35
|
+
</p-auto-complete>
|
|
36
|
+
|
|
37
|
+
@if (hint) {
|
|
38
|
+
<small class="p-mt-1">{{ hint }}</small>
|
|
39
|
+
}
|
|
40
|
+
@if (isInvalid && (control.dirty || control.touched)) {
|
|
41
|
+
<small class="p-error text-red-700">
|
|
42
|
+
@for (error of control.errors | validationErrors; track error) {
|
|
43
|
+
{{ error }}<br>
|
|
44
|
+
}
|
|
45
|
+
</small>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
</div>
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { AutoCompleteComponent } from './auto-complete.component';
|
|
3
|
+
|
|
4
|
+
describe('AutoCompleteComponent', () => {
|
|
5
|
+
let component: AutoCompleteComponent;
|
|
6
|
+
let fixture: ComponentFixture<AutoCompleteComponent>;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
await TestBed.configureTestingModule({
|
|
10
|
+
imports: [AutoCompleteComponent],
|
|
11
|
+
}).compileComponents();
|
|
12
|
+
|
|
13
|
+
fixture = TestBed.createComponent(AutoCompleteComponent);
|
|
14
|
+
component = fixture.componentInstance;
|
|
15
|
+
fixture.detectChanges();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should create', () => {
|
|
19
|
+
expect(component).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { JsonPipe, NgClass, NgIf, NgTemplateOutlet } from "@angular/common";
|
|
2
|
+
import { Component, EventEmitter, Input, Output, TemplateRef } from "@angular/core";
|
|
3
|
+
import { ReactiveFormsModule } from "@angular/forms";
|
|
4
|
+
import { ValidationErrorsPipe } from "../../@utils/validations/validation-message.pipe";
|
|
5
|
+
import { PrimeTemplate } from "primeng/api";
|
|
6
|
+
import { AutoComplete, AutoCompleteCompleteEvent, AutoCompleteSelectEvent } from "primeng/autocomplete";
|
|
7
|
+
import { BaseInputComponent } from "../base-input.component";
|
|
8
|
+
import {TranslatePipe} from "@ngx-translate/core";
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: "stc-auto-complete",
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [
|
|
14
|
+
ReactiveFormsModule,
|
|
15
|
+
AutoComplete,
|
|
16
|
+
PrimeTemplate,
|
|
17
|
+
NgIf,
|
|
18
|
+
NgTemplateOutlet,
|
|
19
|
+
NgClass,
|
|
20
|
+
JsonPipe,
|
|
21
|
+
ValidationErrorsPipe,
|
|
22
|
+
TranslatePipe
|
|
23
|
+
],
|
|
24
|
+
templateUrl: "./auto-complete.component.html",
|
|
25
|
+
styleUrl: "./auto-complete.component.scss"
|
|
26
|
+
})
|
|
27
|
+
export class AutoCompleteComponent extends BaseInputComponent {
|
|
28
|
+
@Input() selectedItemTemplate: TemplateRef<unknown> | null = null;
|
|
29
|
+
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
|
|
30
|
+
@Output() onSearch: EventEmitter<string> = new EventEmitter<string>();
|
|
31
|
+
@Output() selectOption: EventEmitter<AutoCompleteSelectEvent> = new EventEmitter<AutoCompleteSelectEvent>();
|
|
32
|
+
@Input() items: any[] = [];
|
|
33
|
+
@Input() minLengthToSearch = 3;
|
|
34
|
+
@Input() delay = 300; // default value
|
|
35
|
+
@Input() basicInput!: boolean;
|
|
36
|
+
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
search(event: AutoCompleteCompleteEvent) {
|
|
42
|
+
this.onSearch.emit(event.query);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onSelect(event: AutoCompleteSelectEvent) {
|
|
46
|
+
this.selectOption.emit(event);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
|
2
|
+
import { FormControl, Validators } from "@angular/forms";
|
|
3
|
+
import { Subject, takeUntil } from "rxjs";
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
template: ""
|
|
7
|
+
})
|
|
8
|
+
export abstract class BaseInputComponent implements OnInit, OnDestroy {
|
|
9
|
+
@Input({ required: true }) control!: FormControl;
|
|
10
|
+
@Input() name: string = "";
|
|
11
|
+
@Input() label?: string;
|
|
12
|
+
@Input() placeholder: string = "";
|
|
13
|
+
@Input() inputId!: string;
|
|
14
|
+
@Input() readonly: boolean = false;
|
|
15
|
+
@Input() disabled: boolean = false;
|
|
16
|
+
@Input() hint?: string;
|
|
17
|
+
|
|
18
|
+
protected destroy$ = new Subject<void>();
|
|
19
|
+
|
|
20
|
+
get required(): boolean {
|
|
21
|
+
return this.control.hasValidator(Validators.required);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get isInvalid(): boolean {
|
|
25
|
+
return this.control.invalid && this.control.touched;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ngOnInit() {
|
|
29
|
+
this.inputId = `input-${this.name + "-" + Math.random().toString(36).substring(7)}`;
|
|
30
|
+
this.control.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => {
|
|
31
|
+
if (v) {
|
|
32
|
+
this.control.markAsTouched();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ngOnDestroy(): void {
|
|
38
|
+
this.destroy$.next();
|
|
39
|
+
this.destroy$.complete();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div class="field flex flex-col gap-2 my-3 relative" >
|
|
2
|
+
@if (label) {
|
|
3
|
+
<label [for]="inputId">
|
|
4
|
+
{{ label }}
|
|
5
|
+
@if (required) {
|
|
6
|
+
<span [class.text-red-700]="isInvalid">*</span>
|
|
7
|
+
}
|
|
8
|
+
</label>
|
|
9
|
+
}
|
|
10
|
+
@if (!required) {
|
|
11
|
+
<span class="absolute top-[6px] left-0 text-[10px] text-gray-400">{{'forms.config.optional' | translate}}</span>
|
|
12
|
+
}
|
|
13
|
+
<p-datepicker
|
|
14
|
+
[selectionMode]="selectionMode"
|
|
15
|
+
[disabled]="disabled"
|
|
16
|
+
[formControl]="control"
|
|
17
|
+
[iconDisplay]="'input'"
|
|
18
|
+
[showClear]="showClear"
|
|
19
|
+
(onClear)="afterClearDate()"
|
|
20
|
+
[id]="inputId"
|
|
21
|
+
[inputStyleClass]="'reset-default-styles ' + (basicInput ? 'basic-style' : '')"
|
|
22
|
+
[ngClass]="{ 'p-invalid ng-dirty ng-invalid': isInvalid }"
|
|
23
|
+
[placeholder]="placeholder"
|
|
24
|
+
[showIcon]="showIcon" [styleClass]="'w-full'"
|
|
25
|
+
appendTo="body"
|
|
26
|
+
[timeOnly]="isTimeOnly"
|
|
27
|
+
[hourFormat]="hourFormat"
|
|
28
|
+
[minDate]="minDate"
|
|
29
|
+
[maxDate]="maxDate">
|
|
30
|
+
@if (isTimeOnly) {
|
|
31
|
+
<ng-template #inputicon let-clickCallBack="clickCallBack">
|
|
32
|
+
<i class="text-[18px] font-icon-time-clock" (click)="clickCallBack($event)"></i>
|
|
33
|
+
</ng-template>
|
|
34
|
+
<ng-template pTemplate="footer">
|
|
35
|
+
<div class="p-datepicker-buttonbar">
|
|
36
|
+
<button pButton type="button" class="p-button-text" (click)="selectCurrentTime($event)">الان
|
|
37
|
+
</button>
|
|
38
|
+
<button pButton type="button" class="p-button-text" (click)="clearButtonClick($event)"> الغاء</button>
|
|
39
|
+
</div>
|
|
40
|
+
</ng-template>
|
|
41
|
+
}
|
|
42
|
+
</p-datepicker>
|
|
43
|
+
@if (hint) {
|
|
44
|
+
<small class="p-mt-1">{{ hint }}</small>
|
|
45
|
+
}
|
|
46
|
+
@if (isInvalid && (control.dirty || control.touched)) {
|
|
47
|
+
<small class="p-error text-red-700">
|
|
48
|
+
@for (error of control.errors | validationErrors; track error) {
|
|
49
|
+
{{ error }}<br>
|
|
50
|
+
}
|
|
51
|
+
</small>
|
|
52
|
+
}
|
|
53
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
|
2
|
+
import {
|
|
3
|
+
DatePickerComponent
|
|
4
|
+
} from 'nx-BOD-workspace/libs/ui-components/src/lib/form-components/components/date-picker/date-picker.component';
|
|
5
|
+
|
|
6
|
+
describe('DatePickerComponent', () => {
|
|
7
|
+
let component: DatePickerComponent;
|
|
8
|
+
let fixture: ComponentFixture<DatePickerComponent>;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
await TestBed.configureTestingModule({
|
|
12
|
+
imports: [DatePickerComponent],
|
|
13
|
+
}).compileComponents();
|
|
14
|
+
|
|
15
|
+
fixture = TestBed.createComponent(DatePickerComponent);
|
|
16
|
+
component = fixture.componentInstance;
|
|
17
|
+
fixture.detectChanges();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should create', () => {
|
|
21
|
+
expect(component).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
});
|