Merge branch 'dev' into producao
This commit is contained in:
commit
6803ce9337
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
import { CustomSelectComponent } from './custom-select';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [FormsModule, CustomSelectComponent],
|
||||||
|
template: `
|
||||||
|
<app-select [options]="options" [(ngModel)]="firstValue"></app-select>
|
||||||
|
<app-select [options]="options" [(ngModel)]="secondValue"></app-select>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
class HostComponent {
|
||||||
|
options = [
|
||||||
|
{ label: 'Primeira', value: 'one' },
|
||||||
|
{ label: 'Segunda', value: 'two' },
|
||||||
|
];
|
||||||
|
firstValue = 'one';
|
||||||
|
secondValue = 'two';
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CustomSelectComponent', () => {
|
||||||
|
let fixture: ComponentFixture<HostComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [HostComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HostComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep only one select panel open at a time', () => {
|
||||||
|
const selectComponents = fixture.debugElement
|
||||||
|
.queryAll(By.directive(CustomSelectComponent))
|
||||||
|
.map((debugEl) => debugEl.componentInstance as CustomSelectComponent);
|
||||||
|
const triggerButtons = fixture.nativeElement.querySelectorAll('.app-select-trigger') as NodeListOf<HTMLButtonElement>;
|
||||||
|
|
||||||
|
triggerButtons[0].click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(selectComponents[0].isOpen).toBeTrue();
|
||||||
|
expect(selectComponents[1].isOpen).toBeFalse();
|
||||||
|
|
||||||
|
triggerButtons[1].click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(selectComponents[0].isOpen).toBeFalse();
|
||||||
|
expect(selectComponents[1].isOpen).toBeTrue();
|
||||||
|
expect(fixture.nativeElement.querySelectorAll('.app-select-panel').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { Component, ElementRef, HostListener, Input, forwardRef } from '@angular/core';
|
import { Component, ElementRef, HostListener, Input, OnDestroy, forwardRef } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-select',
|
selector: 'app-select',
|
||||||
|
|
@ -16,7 +18,9 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CustomSelectComponent implements ControlValueAccessor {
|
export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
|
||||||
|
private static nextDropdownId = 0;
|
||||||
|
|
||||||
@Input() options: any[] = [];
|
@Input() options: any[] = [];
|
||||||
@Input() placeholder = 'Selecione uma opção';
|
@Input() placeholder = 'Selecione uma opção';
|
||||||
@Input() labelKey = 'label';
|
@Input() labelKey = 'label';
|
||||||
|
|
@ -29,11 +33,22 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
value: any = null;
|
value: any = null;
|
||||||
searchTerm = '';
|
searchTerm = '';
|
||||||
|
private readonly dropdownId = `custom-select-${CustomSelectComponent.nextDropdownId++}`;
|
||||||
|
private readonly dropdownSyncSub: Subscription;
|
||||||
|
|
||||||
private onChange: (value: any) => void = () => {};
|
private onChange: (value: any) => void = () => {};
|
||||||
private onTouched: () => void = () => {};
|
private onTouched: () => void = () => {};
|
||||||
|
|
||||||
constructor(private host: ElementRef<HTMLElement>) {}
|
constructor(
|
||||||
|
private host: ElementRef<HTMLElement>,
|
||||||
|
private dropdownCoordinator: DropdownCoordinatorService
|
||||||
|
) {
|
||||||
|
this.dropdownSyncSub = this.dropdownCoordinator.activeDropdownId$.subscribe((activeId) => {
|
||||||
|
if (this.isOpen && activeId !== this.dropdownId) {
|
||||||
|
this.close(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
writeValue(value: any): void {
|
writeValue(value: any): void {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
|
@ -49,7 +64,7 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
|
|
||||||
setDisabledState(isDisabled: boolean): void {
|
setDisabledState(isDisabled: boolean): void {
|
||||||
this.disabled = isDisabled;
|
this.disabled = isDisabled;
|
||||||
if (this.disabled) this.isOpen = false;
|
if (this.disabled) this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
get displayLabel(): string {
|
get displayLabel(): string {
|
||||||
|
|
@ -65,13 +80,21 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
|
|
||||||
toggle(): void {
|
toggle(): void {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this.isOpen = !this.isOpen;
|
if (this.isOpen) {
|
||||||
if (!this.isOpen) this.searchTerm = '';
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dropdownCoordinator.requestOpen(this.dropdownId);
|
||||||
|
this.isOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): void {
|
close(notifyCoordinator = true): void {
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
this.searchTerm = '';
|
this.searchTerm = '';
|
||||||
|
if (notifyCoordinator) {
|
||||||
|
this.dropdownCoordinator.clear(this.dropdownId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectOption(option: any): void {
|
selectOption(option: any): void {
|
||||||
|
|
@ -148,4 +171,9 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
onEsc(): void {
|
onEsc(): void {
|
||||||
if (this.isOpen) this.close();
|
if (this.isOpen) this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.close();
|
||||||
|
this.dropdownSyncSub.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { HttpParams } from '@angular/common/http';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { Geral } from './geral';
|
import { Geral } from './geral';
|
||||||
|
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
|
||||||
|
|
||||||
describe('Geral', () => {
|
describe('Geral', () => {
|
||||||
let component: Geral;
|
let component: Geral;
|
||||||
|
|
@ -171,4 +172,20 @@ describe('Geral', () => {
|
||||||
expect(params.get('includeAssignedReservaInAll')).toBeNull();
|
expect(params.get('includeAssignedReservaInAll')).toBeNull();
|
||||||
expect(params.get('skil')).toBe('RESERVA');
|
expect(params.get('skil')).toBe('RESERVA');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should close custom filter dropdowns when another filter dropdown opens', () => {
|
||||||
|
const dropdownCoordinator = TestBed.inject(DropdownCoordinatorService);
|
||||||
|
|
||||||
|
component.toggleAdditionalMenu();
|
||||||
|
expect(component.showAdditionalMenu).toBeTrue();
|
||||||
|
|
||||||
|
component.toggleClientMenu();
|
||||||
|
expect(component.showClientMenu).toBeTrue();
|
||||||
|
expect(component.showAdditionalMenu).toBeFalse();
|
||||||
|
|
||||||
|
dropdownCoordinator.requestOpen('external-filter');
|
||||||
|
|
||||||
|
expect(component.showClientMenu).toBeFalse();
|
||||||
|
expect(component.showAdditionalMenu).toBeFalse();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
type MveAuditIssue,
|
type MveAuditIssue,
|
||||||
type MveAuditRun,
|
type MveAuditRun,
|
||||||
} from '../../services/mve-audit.service';
|
} from '../../services/mve-audit.service';
|
||||||
|
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
|
||||||
import { firstValueFrom, Subscription, filter } from 'rxjs';
|
import { firstValueFrom, Subscription, filter } from 'rxjs';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||||
|
|
@ -361,9 +362,14 @@ interface MveApplySelectionSummary {
|
||||||
styleUrls: ['./geral.scss']
|
styleUrls: ['./geral.scss']
|
||||||
})
|
})
|
||||||
export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
private static nextFilterDropdownScopeId = 0;
|
||||||
|
|
||||||
readonly vm = this;
|
readonly vm = this;
|
||||||
readonly batchMassColumnGuide = BATCH_MASS_COLUMN_GUIDE;
|
readonly batchMassColumnGuide = BATCH_MASS_COLUMN_GUIDE;
|
||||||
toastMessage = '';
|
toastMessage = '';
|
||||||
|
private readonly filterDropdownScopeId = Geral.nextFilterDropdownScopeId++;
|
||||||
|
private readonly clientDropdownId = `geral-client-filter-${this.filterDropdownScopeId}`;
|
||||||
|
private readonly additionalDropdownId = `geral-additional-filter-${this.filterDropdownScopeId}`;
|
||||||
|
|
||||||
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
||||||
@ViewChild('excelInput') excelInput!: ElementRef<HTMLInputElement>;
|
@ViewChild('excelInput') excelInput!: ElementRef<HTMLInputElement>;
|
||||||
|
|
@ -385,7 +391,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private tenantSyncService: TenantSyncService,
|
private tenantSyncService: TenantSyncService,
|
||||||
private solicitacoesLinhasService: SolicitacoesLinhasService,
|
private solicitacoesLinhasService: SolicitacoesLinhasService,
|
||||||
private tableExportService: TableExportService,
|
private tableExportService: TableExportService,
|
||||||
private mveAuditService: MveAuditService
|
private mveAuditService: MveAuditService,
|
||||||
|
private dropdownCoordinator: DropdownCoordinatorService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'lines');
|
private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'lines');
|
||||||
|
|
@ -509,6 +516,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private editingId: string | null = null;
|
private editingId: string | null = null;
|
||||||
private searchTimer: any = null;
|
private searchTimer: any = null;
|
||||||
private navigationSub?: Subscription;
|
private navigationSub?: Subscription;
|
||||||
|
private dropdownSyncSub?: Subscription;
|
||||||
private keepPageOnNextGroupsLoad = false;
|
private keepPageOnNextGroupsLoad = false;
|
||||||
|
|
||||||
private searchResolvedClient: string | null = null;
|
private searchResolvedClient: string | null = null;
|
||||||
|
|
@ -968,12 +976,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
if (this.showClientMenu && !insideClient) {
|
if (this.showClientMenu && !insideClient) {
|
||||||
this.showClientMenu = false;
|
this.closeClientDropdown();
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.showAdditionalMenu && !insideAdditional) {
|
if (this.showAdditionalMenu && !insideAdditional) {
|
||||||
this.showAdditionalMenu = false;
|
this.closeAdditionalDropdown();
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -999,12 +1007,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
if (this.showClientMenu) {
|
if (this.showClientMenu) {
|
||||||
this.showClientMenu = false;
|
this.closeClientDropdown();
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.showAdditionalMenu) {
|
if (this.showAdditionalMenu) {
|
||||||
this.showAdditionalMenu = false;
|
this.closeAdditionalDropdown();
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1018,6 +1026,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (this.searchTimer) clearTimeout(this.searchTimer);
|
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||||
this.navigationSub?.unsubscribe();
|
this.navigationSub?.unsubscribe();
|
||||||
|
this.dropdownSyncSub?.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
@ -1035,6 +1044,16 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.selectedAdditionalServices = [];
|
this.selectedAdditionalServices = [];
|
||||||
this.selectedClients = [];
|
this.selectedClients = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dropdownSyncSub = this.dropdownCoordinator.activeDropdownId$.subscribe((activeId) => {
|
||||||
|
if (activeId !== this.clientDropdownId) {
|
||||||
|
this.closeClientDropdown(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeId !== this.additionalDropdownId) {
|
||||||
|
this.closeAdditionalDropdown(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngAfterViewInit() {
|
async ngAfterViewInit() {
|
||||||
|
|
@ -2536,27 +2555,43 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
toggleClientMenu() {
|
toggleClientMenu() {
|
||||||
if (this.isClientRestricted) return;
|
if (this.isClientRestricted) return;
|
||||||
if (!this.showClientMenu) this.showAdditionalMenu = false;
|
if (this.showClientMenu) {
|
||||||
this.showClientMenu = !this.showClientMenu;
|
this.closeClientDropdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dropdownCoordinator.requestOpen(this.clientDropdownId);
|
||||||
|
this.showClientMenu = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAdditionalMenu() {
|
toggleAdditionalMenu() {
|
||||||
if (this.isClientRestricted) return;
|
if (this.isClientRestricted) return;
|
||||||
if (!this.showAdditionalMenu) this.showClientMenu = false;
|
if (this.showAdditionalMenu) {
|
||||||
this.showAdditionalMenu = !this.showAdditionalMenu;
|
this.closeAdditionalDropdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dropdownCoordinator.requestOpen(this.additionalDropdownId);
|
||||||
|
this.showAdditionalMenu = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
closeClientDropdown() {
|
closeClientDropdown(notifyCoordinator = true) {
|
||||||
this.showClientMenu = false;
|
this.showClientMenu = false;
|
||||||
|
if (notifyCoordinator) {
|
||||||
|
this.dropdownCoordinator.clear(this.clientDropdownId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAdditionalDropdown() {
|
closeAdditionalDropdown(notifyCoordinator = true) {
|
||||||
this.showAdditionalMenu = false;
|
this.showAdditionalMenu = false;
|
||||||
|
if (notifyCoordinator) {
|
||||||
|
this.dropdownCoordinator.clear(this.additionalDropdownId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeFilterDropdowns() {
|
closeFilterDropdowns() {
|
||||||
this.showClientMenu = false;
|
this.closeClientDropdown();
|
||||||
this.showAdditionalMenu = false;
|
this.closeAdditionalDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
selectClient(client: string | null) {
|
selectClient(client: string | null) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class DropdownCoordinatorService {
|
||||||
|
private readonly activeDropdownIdSubject = new BehaviorSubject<string | null>(null);
|
||||||
|
readonly activeDropdownId$ = this.activeDropdownIdSubject.asObservable();
|
||||||
|
|
||||||
|
get activeDropdownId(): string | null {
|
||||||
|
return this.activeDropdownIdSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestOpen(id: string): void {
|
||||||
|
if (!id || this.activeDropdownIdSubject.value === id) return;
|
||||||
|
this.activeDropdownIdSubject.next(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(id?: string): void {
|
||||||
|
if (id && this.activeDropdownIdSubject.value !== id) return;
|
||||||
|
if (this.activeDropdownIdSubject.value === null) return;
|
||||||
|
this.activeDropdownIdSubject.next(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue