Repository navigation
Added component #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added component #112
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,37 +1,34 @@ | ||
| import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; | ||
| import { TestBed } from "@angular/core/testing"; | ||
| import { TestBed, waitForAsync } from "@angular/core/testing"; | ||
| import { RouterTestingModule } from "@angular/router/testing"; | ||
| import { AuthService } from "@shared/services/auth/auth.service"; | ||
| import { SharedModule } from "@shared/shared.module"; | ||
| import { SpinnerService } from "@shared/services/spinners/spinner-service"; | ||
| import { | ||
| MockAuthService, | ||
| mostUsedImports, | ||
| mostUsedServices, | ||
| testUtilStubs, | ||
| } from "@shared/test-utils"; | ||
| import { AppComponent } from "./app.component"; | ||
|
|
||
| describe("AppComponent", () => { | ||
| beforeEach(async () => { | ||
| await TestBed.configureTestingModule({ | ||
| beforeEach(waitForAsync(() => { | ||
| TestBed.configureTestingModule({ | ||
| providers: [ | ||
| ...mostUsedServices, | ||
| { provide: AuthService, useClass: MockAuthService }, | ||
| ...testUtilStubs, | ||
| ...mostUsedServices, | ||
| SpinnerService, | ||
| ], | ||
| imports: [RouterTestingModule, ...mostUsedImports], | ||
| declarations: [AppComponent], | ||
| imports: [RouterTestingModule, SharedModule], | ||
| schemas: [CUSTOM_ELEMENTS_SCHEMA], | ||
| }).compileComponents(); | ||
| }); | ||
| })); | ||
|
|
||
| it("should create the app", () => { | ||
| const fixture = TestBed.createComponent(AppComponent); | ||
| const app = fixture.componentInstance; | ||
| expect(app).toBeTruthy(); | ||
| }); | ||
|
|
||
| it(`should have as title 'Techinterview.space'`, () => { | ||
| const fixture = TestBed.createComponent(AppComponent); | ||
| const app = fixture.componentInstance; | ||
| expect(app.title).toEqual("Techinterview.space"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,43 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { FormControl, FormGroup, Validators } from "@angular/forms"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CreateSalariesHistoricalDataRecordTemplateRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SalariesHistoricalDataRecordTemplateDto, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@services/salaries-historical-data-templates.service"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class HistoricalDataTemplateEditForm extends FormGroup { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly item: SalariesHistoricalDataRecordTemplateDto | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: new FormControl(item?.name ?? null, [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Validators.required, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Validators.maxLength(100), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| professionIds: new FormControl(item?.professionIds ?? [], [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Validators.required, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasItemToEdit(): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.item != null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getItemId(): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.item?.id ?? null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createRequestOrNull(): CreateSalariesHistoricalDataRecordTemplateRequest | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!this.valid) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.markAllAsTouched(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const professionIds = this.value.professionIds as Array<number>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: this.value.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| professionIds: professionIds ?? [], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type safety concern and redundant null coalescing. Line 36 uses a type assertion Line 40's null coalescing 🔎 Improve type safety createRequestOrNull(): CreateSalariesHistoricalDataRecordTemplateRequest | null {
if (!this.valid) {
this.markAllAsTouched();
return null;
}
- const professionIds = this.value.professionIds as Array<number>;
+ const professionIds = (this.value.professionIds as Array<number> | null) ?? [];
return {
name: this.value.name,
- professionIds: professionIds ?? [],
+ professionIds: professionIds,
};
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| <app-page-header>Шаблоны исторических данных</app-page-header> | ||
|
|
||
| <div class="container my-5" *ngIf="items; else dataLoading"> | ||
| <div class="card"> | ||
| <div class="card-body"> | ||
| <div class="mb-2"> | ||
| <button class="btn btn-sm btn-primary" (click)="create()"> | ||
| Создать | ||
| </button> | ||
| </div> | ||
|
|
||
| <div | ||
| class="mb-3 table-responsive" | ||
| *ngIf="items && items.length > 0; else nothingToShow" | ||
| > | ||
| <table class="table table-hover table-sm"> | ||
| <thead class="small"> | ||
| <tr class="small"> | ||
| <th>ID</th> | ||
| <th>Название</th> | ||
| <th>Специальности</th> | ||
| <th>Создано</th> | ||
| <th>Обновлено</th> | ||
| <th>Действия</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| <tr class="small" *ngFor="let item of items; let i = index"> | ||
| <td> | ||
| <span class="text-muted small" [title]="item.id">...{{ item.id.split('-').pop() }}</span> | ||
| </td> | ||
| <td>{{ item.name }}</td> | ||
| <td> | ||
| <a | ||
| *ngIf="item.professionIds && item.professionIds.length > 0" | ||
| href="#" | ||
| class="text-primary" | ||
| (click)="openProfessionsModal(item.professionIds); $event.preventDefault()" | ||
| > | ||
| {{ item.professionIds.length }} специальност{{ item.professionIds.length === 1 ? 'ь' : (item.professionIds.length < 5 ? 'и' : 'ей') }} | ||
| </a> | ||
| <span *ngIf="!item.professionIds || item.professionIds.length === 0">-</span> | ||
| </td> | ||
| <td>{{ item.createdAt | date: "yyyy-MM-dd HH:mm" }}</td> | ||
| <td>{{ item.updatedAt | date: "yyyy-MM-dd HH:mm" }}</td> | ||
| <td> | ||
| <div class="btn-group" role="group"> | ||
| <button | ||
| type="button" | ||
| class="btn btn-outline-dark dropdown-toggle btn-sm" | ||
| data-bs-toggle="dropdown" | ||
| aria-expanded="false" | ||
| > | ||
| Действия | ||
| </button> | ||
| <ul class="dropdown-menu"> | ||
| <li> | ||
| <a class="dropdown-item" (click)="openEditDlg(item)" | ||
| >Редактировать</a | ||
| > | ||
| </li> | ||
| <li> | ||
| <a | ||
| class="dropdown-item text-danger" | ||
| href="#" | ||
| (click)="delete(item); $event.preventDefault()" | ||
| >Удалить</a | ||
| > | ||
| </li> | ||
| </ul> | ||
| </div> | ||
| </td> | ||
| </tr> | ||
|
Comment on lines
+28
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Add trackBy functions for performance. Multiple
This can cause unnecessary re-renders, especially after edit/delete operations. 🔎 Proposed fixAdd to component: trackById(index: number, item: { id: string }): string {
return item.id;
}
trackByProfessionId(index: number, profession: LabelEntityDto): number {
return profession.id;
}Update template: -<tr class="small" *ngFor="let item of items; let i = index">
+<tr class="small" *ngFor="let item of items; trackBy: trackById">
-*ngFor="let profession of selectedProfessionsForModal"
+*ngFor="let profession of selectedProfessionsForModal; trackBy: trackByProfessionId"As per coding guidelines, trackBy functions are required for ngFor loops. Also applies to: 157-161 🤖 Prompt for AI Agents |
||
| </tbody> | ||
| </table> | ||
| </div> | ||
|
|
||
| <div class="" *ngIf="source && source.results && source.results.length > 0"> | ||
| <app-pagination-buttons | ||
| [source]="source" | ||
| (pageChange)="loadData($event)" | ||
| ></app-pagination-buttons> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <ng-template #nothingToShow> | ||
| <div class="card"> | ||
| <div class="card-body"> | ||
| <div class="text-center">Нет данных</div> | ||
| </div> | ||
| </div> | ||
| </ng-template> | ||
|
|
||
| <ng-template #dataLoading> | ||
| <div class="container mt-5"> | ||
| <app-data-loader></app-data-loader> | ||
| </div> | ||
| </ng-template> | ||
|
|
||
| <app-dialog | ||
| [show]="!!editForm" | ||
| (closed)="onEditModalDlgClose()" | ||
| [header]="editForm?.hasItemToEdit() ? 'Редактирование шаблона' : 'Новый шаблон'" | ||
| > | ||
| <form | ||
| class="mb-5" | ||
| *ngIf="!!editForm" | ||
| [formGroup]="editForm" | ||
| (ngSubmit)="onEditFormSubmit()" | ||
| > | ||
| <div class="mb-3"> | ||
| <label>Название шаблона</label> | ||
| <input type="text" class="form-control" formControlName="name" /> | ||
| <app-field-error [field]="editForm.get('name')"></app-field-error> | ||
| </div> | ||
|
|
||
| <div class="mb-3"> | ||
| <label>Специальности</label> | ||
| <ng-select | ||
| [items]="professionsAsOptions" | ||
| [hideSelected]="true" | ||
| [selectOnTab]="true" | ||
| bindLabel="label" | ||
| bindValue="item" | ||
| placeholder="Выберите специальности" | ||
| formControlName="professionIds" | ||
| [multiple]="true" | ||
| [clearable]="true" | ||
| > | ||
| </ng-select> | ||
| <app-field-error | ||
| [field]="editForm.get('professionIds')" | ||
| ></app-field-error> | ||
| </div> | ||
|
|
||
| <div class="mb-3"> | ||
| <button class="btn btn-primary" type="submit">Сохранить</button> | ||
| </div> | ||
| </form> | ||
| </app-dialog> | ||
|
|
||
| <app-confirm-dialog | ||
| *ngIf="confirmDeletionMessage" | ||
| [message]="confirmDeletionMessage" | ||
| ></app-confirm-dialog> | ||
|
|
||
| <app-dialog | ||
| [show]="!!selectedProfessionsForModal" | ||
| (closed)="closeProfessionsModal()" | ||
| [header]="'Специальности'" | ||
| > | ||
| <ul class="list-group" *ngIf="selectedProfessionsForModal"> | ||
| <li | ||
| class="list-group-item d-flex justify-content-between align-items-center" | ||
| *ngFor="let profession of selectedProfessionsForModal" | ||
| > | ||
| <span>{{ profession.title }}</span> | ||
| <span class="badge bg-secondary">ID: {{ profession.id }}</span> | ||
| </li> | ||
| </ul> | ||
| </app-dialog> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // Component styles | ||
|
|
||
| // Ensure ng-select dropdown is visible inside modal | ||
| :host ::ng-deep { | ||
| .modal-body { | ||
| overflow: visible; | ||
| } | ||
|
|
||
| .modal-content { | ||
| overflow: visible; | ||
| } | ||
| } | ||
|
|
||
| // Ensure action dropdown is visible outside table container | ||
| // Using overflow: clip on x-axis preserves horizontal scroll behavior if needed | ||
| // while allowing dropdowns to overflow vertically | ||
| .table-responsive { | ||
| overflow: visible; | ||
|
|
||
| .dropdown-menu { | ||
| position: absolute; | ||
| z-index: 1050; | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validators.requireddoesn't validate array length.The
Validators.requiredvalidator only checks for null/undefined, so an empty array[]would pass validation. For array fields that must contain at least one item, useValidators.minLength(1)or a custom validator.🔎 Fix array validation
professionIds: new FormControl(item?.professionIds ?? [], [ Validators.required, + Validators.minLength(1), ]),Or create a custom validator:
📝 Committable suggestion
🤖 Prompt for AI Agents