Fix: fix modal create closing open dropdown

This commit is contained in:
shamoon
2026-08-16 15:51:36 -07:00
parent d88b281eb2
commit 0af1e41753
4 changed files with 54 additions and 14 deletions
@@ -1,4 +1,4 @@
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)" [popperOptions]="popperOptions">
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)" [popperOptions]="popperOptions" [autoClose]="!creating()">
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
<i-bs name="{{icon}}"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
@if (!editing && selectionModel.totalCount > 0) {
@@ -3,6 +3,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { NEVER, Subject } from 'rxjs'
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
import {
DEFAULT_MATCHING_ALGORITHM,
@@ -48,6 +49,7 @@ const negativeNullItem = {
let selectionModel: FilterableDropdownSelectionModel
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const createModalRef = () => ({ closed: NEVER, dismissed: NEVER }) as any
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
let component: FilterableDropdownComponent
@@ -868,7 +870,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
expect(getRootDocCount(rootWithoutCounts.id)).toEqual(0)
})
it('should set support create, keep open model and call createRef method', async () => {
it('should keep the dropdown open while the create modal is active', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
component.selectionModel = selectionModel
@@ -882,20 +884,44 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
fixture.detectChanges()
component.filterText = 'Test Filter Text'
component.createRef = jest.fn()
const modalClosed = new Subject<void>()
component.createRef = jest.fn(
() =>
({
closed: modalClosed,
dismissed: NEVER,
}) as any
)
component.createClicked()
expect(component.creating).toBeTruthy()
expect(component.creating()).toBeTruthy()
expect(component.createRef).toHaveBeenCalledWith('Test Filter Text')
fixture.detectChanges()
expect(component.dropdown.autoClose).toBeFalsy()
document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
document.body.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }))
await wait(10)
expect(component.dropdown.isOpen()).toBeTruthy()
// Also cover a close that was already scheduled before autoClose changed.
const openSpy = jest.spyOn(component.dropdown, 'open')
component.dropdownOpenChange(false)
expect(openSpy).toHaveBeenCalled() // should keep open
component.dropdownOpenChange(false)
expect(openSpy).toHaveBeenCalledTimes(2) // modal interactions keep it open
modalClosed.next()
fixture.detectChanges()
expect(component.creating()).toBeFalsy()
expect(component.dropdown.autoClose).toBeTruthy()
expect(component.dropdown.isOpen()).toBeTruthy()
})
it('should call create on enter inside filter field if 0 items remain while editing', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
component.editing = true
component.createRef = jest.fn()
component.createRef = jest.fn(createModalRef)
const createSpy = jest.spyOn(component, 'createClicked')
expect(component.selectionModel.getSelectedItems()).toEqual([])
fixture.nativeElement
@@ -915,7 +941,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
component.selectionModel.items = []
component.icon = 'tag-fill'
component.editing = true
component.createRef = jest.fn()
component.createRef = jest.fn(createModalRef)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).not.toContain('Create')
@@ -15,9 +15,13 @@ import {
signal,
} from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import {
NgbDropdown,
NgbDropdownModule,
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, filter, takeUntil } from 'rxjs'
import { Subject, filter, first, merge, takeUntil } from 'rxjs'
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
import { MatchingModel } from 'src/app/data/matching-model'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
@@ -759,7 +763,7 @@ export class FilterableDropdownComponent
disabled = false
@Input()
createRef: (name) => void
createRef: (name: string) => NgbModalRef
@Input()
set documentCounts(counts: SelectionDataItem[]) {
@@ -777,7 +781,7 @@ export class FilterableDropdownComponent
@Input()
showExtraButtonIfEmpty: boolean = false
creating: boolean = false
readonly creating = signal(false)
@Output()
apply = new EventEmitter<ChangedItems>()
@@ -854,12 +858,18 @@ export class FilterableDropdownComponent
}
createClicked() {
this.creating = true
this.createRef(this.filterText)
this.creating.set(true)
const modal = this.createRef(this.filterText)
merge(modal.closed, modal.dismissed)
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe(() => this.creating.set(false))
}
dropdownOpenChange(open: boolean): void {
if (open) {
// Dont let a create modal close this
if (this.creating()) return
setTimeout(() => {
this.listFilterTextInput?.nativeElement.focus()
this.buttonsViewport?.checkViewportSize()
@@ -872,9 +882,8 @@ export class FilterableDropdownComponent
this.editing && !this.selectionModel.manyToOne
this.opened.next(this)
} else {
if (this.creating) {
if (this.creating()) {
this.dropdown?.open()
this.creating = false
} else {
this.filterText = ''
if (this.applyOnClose && this.selectionModel.isDirty()) {
@@ -762,6 +762,7 @@ export class BulkEditorComponent
this.tagSelectionModel.items = flattenTags(tags.results)
this.tagSelectionModel.toggle(newTag.id)
})
return modal
}
createCorrespondent(name: string) {
@@ -785,6 +786,7 @@ export class BulkEditorComponent
this.correspondentSelectionModel.items = correspondents.results
this.correspondentSelectionModel.toggle(newCorrespondent.id)
})
return modal
}
createDocumentType(name: string) {
@@ -806,6 +808,7 @@ export class BulkEditorComponent
this.documentTypeSelectionModel.items = documentTypes.results
this.documentTypeSelectionModel.toggle(newDocumentType.id)
})
return modal
}
createStoragePath(name: string) {
@@ -827,6 +830,7 @@ export class BulkEditorComponent
this.storagePathsSelectionModel.items = storagePaths.results
this.storagePathsSelectionModel.toggle(newStoragePath.id)
})
return modal
}
createCustomField(name: string) {
@@ -848,6 +852,7 @@ export class BulkEditorComponent
this.customFieldsSelectionModel.items = customFields.results
this.customFieldsSelectionModel.toggle(newCustomField.id)
})
return modal
}
applyDelete() {