fix: fecha filtros simultaneos na geral
This commit is contained in:
parent
0d7186ce59
commit
9a635ac167
|
|
@ -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 { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
|
||||
|
||||
@Component({
|
||||
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() placeholder = 'Selecione uma opção';
|
||||
@Input() labelKey = 'label';
|
||||
|
|
@ -29,11 +33,22 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
|||
isOpen = false;
|
||||
value: any = null;
|
||||
searchTerm = '';
|
||||
private readonly dropdownId = `custom-select-${CustomSelectComponent.nextDropdownId++}`;
|
||||
private readonly dropdownSyncSub: Subscription;
|
||||
|
||||
private onChange: (value: any) => 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 {
|
||||
this.value = value;
|
||||
|
|
@ -49,7 +64,7 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
|||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
if (this.disabled) this.isOpen = false;
|
||||
if (this.disabled) this.close();
|
||||
}
|
||||
|
||||
get displayLabel(): string {
|
||||
|
|
@ -65,13 +80,21 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
|||
|
||||
toggle(): void {
|
||||
if (this.disabled) return;
|
||||
this.isOpen = !this.isOpen;
|
||||
if (!this.isOpen) this.searchTerm = '';
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dropdownCoordinator.requestOpen(this.dropdownId);
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
close(notifyCoordinator = true): void {
|
||||
this.isOpen = false;
|
||||
this.searchTerm = '';
|
||||
if (notifyCoordinator) {
|
||||
this.dropdownCoordinator.clear(this.dropdownId);
|
||||
}
|
||||
}
|
||||
|
||||
selectOption(option: any): void {
|
||||
|
|
@ -148,4 +171,9 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
|||
onEsc(): void {
|
||||
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 { Geral } from './geral';
|
||||
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
|
||||
|
||||
describe('Geral', () => {
|
||||
let component: Geral;
|
||||
|
|
@ -171,4 +172,20 @@ describe('Geral', () => {
|
|||
expect(params.get('includeAssignedReservaInAll')).toBeNull();
|
||||
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 MveAuditRun,
|
||||
} from '../../services/mve-audit.service';
|
||||
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
|
||||
import { firstValueFrom, Subscription, filter } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||
|
|
@ -361,9 +362,14 @@ interface MveApplySelectionSummary {
|
|||
styleUrls: ['./geral.scss']
|
||||
})
|
||||
export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||
private static nextFilterDropdownScopeId = 0;
|
||||
|
||||
readonly vm = this;
|
||||
readonly batchMassColumnGuide = BATCH_MASS_COLUMN_GUIDE;
|
||||
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('excelInput') excelInput!: ElementRef<HTMLInputElement>;
|
||||
|
|
@ -385,7 +391,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
private tenantSyncService: TenantSyncService,
|
||||
private solicitacoesLinhasService: SolicitacoesLinhasService,
|
||||
private tableExportService: TableExportService,
|
||||
private mveAuditService: MveAuditService
|
||||
private mveAuditService: MveAuditService,
|
||||
private dropdownCoordinator: DropdownCoordinatorService
|
||||
) {}
|
||||
|
||||
private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'lines');
|
||||
|
|
@ -509,6 +516,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
private editingId: string | null = null;
|
||||
private searchTimer: any = null;
|
||||
private navigationSub?: Subscription;
|
||||
private dropdownSyncSub?: Subscription;
|
||||
private keepPageOnNextGroupsLoad = false;
|
||||
|
||||
private searchResolvedClient: string | null = null;
|
||||
|
|
@ -968,12 +976,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
let changed = false;
|
||||
|
||||
if (this.showClientMenu && !insideClient) {
|
||||
this.showClientMenu = false;
|
||||
this.closeClientDropdown();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (this.showAdditionalMenu && !insideAdditional) {
|
||||
this.showAdditionalMenu = false;
|
||||
this.closeAdditionalDropdown();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
|
@ -999,12 +1007,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
let changed = false;
|
||||
|
||||
if (this.showClientMenu) {
|
||||
this.showClientMenu = false;
|
||||
this.closeClientDropdown();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (this.showAdditionalMenu) {
|
||||
this.showAdditionalMenu = false;
|
||||
this.closeAdditionalDropdown();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
|
@ -1018,6 +1026,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
ngOnDestroy(): void {
|
||||
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||
this.navigationSub?.unsubscribe();
|
||||
this.dropdownSyncSub?.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -1035,6 +1044,16 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.selectedAdditionalServices = [];
|
||||
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() {
|
||||
|
|
@ -2536,27 +2555,43 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
toggleClientMenu() {
|
||||
if (this.isClientRestricted) return;
|
||||
if (!this.showClientMenu) this.showAdditionalMenu = false;
|
||||
this.showClientMenu = !this.showClientMenu;
|
||||
if (this.showClientMenu) {
|
||||
this.closeClientDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dropdownCoordinator.requestOpen(this.clientDropdownId);
|
||||
this.showClientMenu = true;
|
||||
}
|
||||
|
||||
toggleAdditionalMenu() {
|
||||
if (this.isClientRestricted) return;
|
||||
if (!this.showAdditionalMenu) this.showClientMenu = false;
|
||||
this.showAdditionalMenu = !this.showAdditionalMenu;
|
||||
if (this.showAdditionalMenu) {
|
||||
this.closeAdditionalDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
closeClientDropdown() {
|
||||
this.dropdownCoordinator.requestOpen(this.additionalDropdownId);
|
||||
this.showAdditionalMenu = true;
|
||||
}
|
||||
|
||||
closeClientDropdown(notifyCoordinator = true) {
|
||||
this.showClientMenu = false;
|
||||
if (notifyCoordinator) {
|
||||
this.dropdownCoordinator.clear(this.clientDropdownId);
|
||||
}
|
||||
}
|
||||
|
||||
closeAdditionalDropdown() {
|
||||
closeAdditionalDropdown(notifyCoordinator = true) {
|
||||
this.showAdditionalMenu = false;
|
||||
if (notifyCoordinator) {
|
||||
this.dropdownCoordinator.clear(this.additionalDropdownId);
|
||||
}
|
||||
}
|
||||
|
||||
closeFilterDropdowns() {
|
||||
this.showClientMenu = false;
|
||||
this.showAdditionalMenu = false;
|
||||
this.closeClientDropdown();
|
||||
this.closeAdditionalDropdown();
|
||||
}
|
||||
|
||||
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