Compare commits

...
18 changed files with 163 additions and 32 deletions
+11
View File
@@ -48,6 +48,7 @@ def seed_database() -> None:
from django.utils import timezone from django.utils import timezone
from documents.models import Correspondent from documents.models import Correspondent
from documents.models import CustomField
from documents.models import Document from documents.models import Document
from documents.models import DocumentType from documents.models import DocumentType
from documents.models import Note from documents.models import Note
@@ -81,6 +82,16 @@ def seed_database() -> None:
path="e2e/{created_year}/{title}", path="e2e/{created_year}/{title}",
owner=admin, owner=admin,
) )
CustomField.objects.create(
name="Test Select Field",
data_type=CustomField.FieldDataType.SELECT,
extra_data={
"select_options": [
{"id": "abc123", "label": "Alpha"},
{"id": "def456", "label": "Beta"},
],
},
)
today = timezone.localdate() today = timezone.localdate()
documents = [] documents = []
@@ -207,3 +207,41 @@ test('bulk edit', async ({ page }) => {
await page.getByRole('button', { name: 'Confirm' }).click() await page.getByRole('button', { name: 'Confirm' }).click()
await bulkEditPromise await bulkEditPromise
}) })
test('permissions dropdown stays open when selecting a user', async ({
page,
}) => {
await page.goto('/documents')
await page.getByRole('button', { name: 'Permissions' }).click()
const dropdown = page.locator(
'pngx-permissions-filter-dropdown .dropdown-menu'
)
await expect(dropdown).toBeVisible()
await dropdown.locator('ng-select').click()
await page.getByRole('option', { name: 'playwright' }).click()
await expect(dropdown).toBeVisible()
await expect(dropdown.locator('.ng-value-label')).toHaveText('playwright')
})
test('custom fields query dropdown stays open when building a query', async ({
page,
}) => {
await page.goto('/documents')
await page.getByRole('button', { name: 'Custom fields' }).click()
const dropdown = page.locator(
'pngx-custom-fields-query-dropdown .dropdown-menu'
)
await expect(dropdown).toBeVisible()
// field picker
await dropdown.locator('ng-select').first().click()
await page.getByRole('option', { name: 'Test Select Field' }).click()
await expect(dropdown).toBeVisible()
// value picker, shown once the operator takes a list of options
await dropdown.locator('select').first().selectOption({ label: 'In' })
await dropdown.locator('ng-select').last().click()
await page.getByRole('option', { name: 'Alpha' }).click()
await expect(dropdown).toBeVisible()
await expect(dropdown.locator('.ng-value-label').last()).toHaveText('Alpha')
})
+1 -1
View File
@@ -24,7 +24,7 @@
"@angular/platform-browser": "~22.1.3", "@angular/platform-browser": "~22.1.3",
"@angular/router": "~22.1.3", "@angular/router": "~22.1.3",
"@ng-bootstrap/ng-bootstrap": "^21.0.0", "@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "~23.6.0", "@ng-select/ng-select": "~24.0.2",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
+7 -5
View File
@@ -36,8 +36,8 @@ importers:
specifier: ^21.0.0 specifier: ^21.0.0
version: 21.0.0(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/forms@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))(@angular/localize@22.1.3(@angular/compiler-cli@22.1.3(@angular/compiler@22.1.3)(typescript@6.0.3))(@angular/compiler@22.1.3))(@popperjs/core@2.11.8)(rxjs@7.8.2) version: 21.0.0(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/forms@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))(@angular/localize@22.1.3(@angular/compiler-cli@22.1.3(@angular/compiler@22.1.3)(typescript@6.0.3))(@angular/compiler@22.1.3))(@popperjs/core@2.11.8)(rxjs@7.8.2)
'@ng-select/ng-select': '@ng-select/ng-select':
specifier: ~23.6.0 specifier: ~24.0.2
version: 23.6.0(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/forms@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2)) version: 24.0.2(@angular/cdk@22.1.4(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/forms@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))
'@ngneat/dirty-check-forms': '@ngneat/dirty-check-forms':
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/forms@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))(@angular/router@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))(lodash-es@4.18.1)(rxjs@7.8.2) version: 3.0.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/forms@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))(@angular/router@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))(lodash-es@4.18.1)(rxjs@7.8.2)
@@ -1490,10 +1490,11 @@ packages:
'@popperjs/core': ^2.11.8 '@popperjs/core': ^2.11.8
rxjs: ^6.5.3 || ^7.4.0 rxjs: ^6.5.3 || ^7.4.0
'@ng-select/ng-select@23.6.0': '@ng-select/ng-select@24.0.2':
resolution: {integrity: sha512-0I84OZ9gncphNR26zuqOwpB/XWhPav+IP+EfJd9lDNeu3UT9Y0JhTBburCXEXcbltwQkiN6bMCKdRF93hrFHxw==} resolution: {integrity: sha512-EcOQanLp5dNxkDOxK75z7+B3gZW8LvQofzwL5AgWUn04EkJOXRI7Wi6Ylp7+jIAi9yPsTwjznrRd+wSl9EZGAQ==}
engines: {node: ^22.12.0 || ^24.0.0 || ^26.0.0} engines: {node: ^22.12.0 || ^24.0.0 || ^26.0.0}
peerDependencies: peerDependencies:
'@angular/cdk': ^22.0.0
'@angular/common': ^22.0.0 '@angular/common': ^22.0.0
'@angular/core': ^22.0.0 '@angular/core': ^22.0.0
'@angular/forms': ^22.0.0 '@angular/forms': ^22.0.0
@@ -5769,8 +5770,9 @@ snapshots:
rxjs: 7.8.2 rxjs: 7.8.2
tslib: 2.8.1 tslib: 2.8.1
'@ng-select/ng-select@23.6.0(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/forms@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))': '@ng-select/ng-select@24.0.2(@angular/cdk@22.1.4(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/forms@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2))':
dependencies: dependencies:
'@angular/cdk': 22.1.4(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2)
'@angular/common': 22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2) '@angular/common': 22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2)
'@angular/core': 22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2) '@angular/core': 22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)
'@angular/forms': 22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2) '@angular/forms': 22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(@angular/compiler@22.1.3)(rxjs@7.8.2)))(rxjs@7.8.2)
@@ -6,7 +6,7 @@
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge> <pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
} }
</button> </button>
<div class="px-3 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}"> <div [id]="dropdownMenuId" class="px-3 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
<ng-container *ngTemplateOutlet="list; context: { queries: selectionModel.queries }"></ng-container> <ng-container *ngTemplateOutlet="list; context: { queries: selectionModel.queries }"></ng-container>
</div> </div>
</div> </div>
@@ -64,10 +64,11 @@
[disabled]="disabled" [disabled]="disabled"
[virtualScroll]="getSelectOptionsForField(atom.field)?.length > 100" [virtualScroll]="getSelectOptionsForField(atom.field)?.length > 100"
[searchFn]="selectOptionSearchFn" [searchFn]="selectOptionSearchFn"
[appendTo]="selectAppendTo"
(mousedown)="$event.stopImmediatePropagation()" (mousedown)="$event.stopImmediatePropagation()"
></ng-select> ></ng-select>
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.DocumentLink) { } @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.DocumentLink) {
<pngx-input-document-link [(ngModel)]="atom.value" class="w-25 form-select doc-link-select p-0" placeholder="Search docs..." i18n-placeholder [minimal]="true"></pngx-input-document-link> <pngx-input-document-link [(ngModel)]="atom.value" class="w-25 form-select doc-link-select p-0" placeholder="Search docs..." i18n-placeholder [minimal]="true" [appendTo]="selectAppendTo"></pngx-input-document-link>
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Monetary) { } @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Monetary) {
<input class="w-25 form-control rounded-end" type="text" inputmode="decimal" <input class="w-25 form-control rounded-end" type="text" inputmode="decimal"
[ngModel]="atom.value" [ngModel]="atom.value"
@@ -88,6 +89,7 @@
bindLabel="name" bindLabel="name"
bindValue="id" bindValue="id"
[searchFn]="customFieldSearchFn" [searchFn]="customFieldSearchFn"
[appendTo]="selectAppendTo"
(mousedown)="$event.stopImmediatePropagation()" (mousedown)="$event.stopImmediatePropagation()"
></ng-select> ></ng-select>
<select class="w-25 form-select" [(ngModel)]="atom.operator" [disabled]="disabled"> <select class="w-25 form-select" [(ngModel)]="atom.operator" [disabled]="disabled">
@@ -121,7 +123,7 @@
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container> <ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
} }
@case (CustomFieldQueryOperator.Contains) { @case (CustomFieldQueryOperator.Contains) {
<pngx-input-document-link [(ngModel)]="atom.value" class="w-25 form-select doc-link-select p-0" placeholder="Search docs..." i18n-placeholder [minimal]="true"></pngx-input-document-link> <pngx-input-document-link [(ngModel)]="atom.value" class="w-25 form-select doc-link-select p-0" placeholder="Search docs..." i18n-placeholder [minimal]="true" [appendTo]="selectAppendTo"></pngx-input-document-link>
} }
@case (CustomFieldQueryOperator.In) { @case (CustomFieldQueryOperator.In) {
<ng-select <ng-select
@@ -133,6 +135,7 @@
[disabled]="disabled" [disabled]="disabled"
[multiple]="true" [multiple]="true"
[searchFn]="selectOptionSearchFn" [searchFn]="selectOptionSearchFn"
[appendTo]="selectAppendTo"
(mousedown)="$event.stopImmediatePropagation()" (mousedown)="$event.stopImmediatePropagation()"
></ng-select> ></ng-select>
} }
@@ -1,3 +1,4 @@
import { _IdGenerator } from '@angular/cdk/a11y'
import { import {
getLocaleNumberSymbol, getLocaleNumberSymbol,
NgClass, NgClass,
@@ -250,6 +251,18 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
@Input() @Input()
useDropdown: boolean = true useDropdown: boolean = true
private readonly idGenerator = inject(_IdGenerator)
public readonly dropdownMenuId = this.idGenerator.getId(
'pngx-custom-fields-query-dropdown-'
)
/**
* Keep ng-select dropdown panels inside the dropdown menu
*/
get selectAppendTo(): string {
return this.useDropdown ? `#${this.dropdownMenuId}` : null
}
get name(): string { get name(): string {
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
} }
@@ -3,7 +3,7 @@
<i-bs width="1em" height="1em" name="calendar-event-fill"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div> <i-bs width="1em" height="1em" name="calendar-event-fill"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span> <pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
</button> </button>
<div class="dropdown-menu date-dropdown shadow p-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div [id]="dropdownMenuId" class="dropdown-menu date-dropdown shadow p-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<h6 class="dropdown-header border-bottom" i18n>Created</h6> <h6 class="dropdown-header border-bottom" i18n>Created</h6>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<div class="list-group-item d-flex p-2 select-item" role="menuitem"> <div class="list-group-item d-flex p-2 select-item" role="menuitem">
@@ -21,6 +21,7 @@
bindValue="id" bindValue="id"
bindLabel="name" bindLabel="name"
clearable="false" clearable="false"
[appendTo]="'#' + dropdownMenuId"
placeholder="Relative dates" placeholder="Relative dates"
i18n-placeholder i18n-placeholder
(change)="onSetCreatedRelativeDate($event)"> (change)="onSetCreatedRelativeDate($event)">
@@ -97,6 +98,7 @@
bindValue="id" bindValue="id"
bindLabel="name" bindLabel="name"
clearable="false" clearable="false"
[appendTo]="'#' + dropdownMenuId"
placeholder="Relative dates" placeholder="Relative dates"
i18n-placeholder i18n-placeholder
(change)="onSetAddedRelativeDate($event)"> (change)="onSetAddedRelativeDate($event)">
@@ -23,6 +23,7 @@ import { SettingsService } from 'src/app/services/settings.service'
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter' import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
import { pngxPopperOptions } from 'src/app/utils/popper-options' import { pngxPopperOptions } from 'src/app/utils/popper-options'
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component' import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
import { _IdGenerator } from '@angular/cdk/a11y'
export interface DateSelection { export interface DateSelection {
createdTo?: string createdTo?: string
@@ -68,6 +69,10 @@ export enum RelativeDate {
}) })
export class DatesDropdownComponent implements OnInit, OnDestroy { export class DatesDropdownComponent implements OnInit, OnDestroy {
public popperOptions = pngxPopperOptions public popperOptions = pngxPopperOptions
private readonly idGenerator = inject(_IdGenerator)
public readonly dropdownMenuId = this.idGenerator.getId(
'pngx-dates-dropdown-'
)
constructor() { constructor() {
const settings = inject(SettingsService) const settings = inject(SettingsService)
@@ -33,6 +33,7 @@
[compareWith]="compareDocuments" [compareWith]="compareDocuments"
[trackByFn]="trackByFn" [trackByFn]="trackByFn"
[minTermLength]="2" [minTermLength]="2"
[appendTo]="appendTo"
[loading]="loading()" [loading]="loading()"
[typeahead]="documentsInput$" [typeahead]="documentsInput$"
(mousedown)="$event.stopImmediatePropagation()" (mousedown)="$event.stopImmediatePropagation()"
@@ -81,6 +81,12 @@ export class DocumentLinkComponent
@Input() @Input()
placeholder: string = $localize`Search for documents` placeholder: string = $localize`Search for documents`
/**
* Parent for ng-select dropdown, needed to prevent close on click.
*/
@Input()
appendTo: string = null
get selectedDocumentIDs(): number[] { get selectedDocumentIDs(): number[] {
return this.selectedDocuments.map((d) => d.id) return this.selectedDocuments.map((d) => d.id)
} }
@@ -22,7 +22,7 @@
} }
// Dropdown hierarchy reveal for ng-select options // Dropdown hierarchy reveal for ng-select options
:host ::ng-deep .ng-dropdown-panel .ng-option { ::ng-deep .ng-dropdown-panel .ng-option {
overflow-x: auto !important; overflow-x: auto !important;
.tag-option-row { .tag-option-row {
@@ -41,12 +41,12 @@
} }
} }
:host ::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-reveal, ::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-reveal,
:host ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-reveal { ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-reveal {
max-width: 1000px; max-width: 1000px;
} }
::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-indicator, ::ng-deep .ng-dropdown-panel .ng-option:hover .hierarchy-indicator,
:host ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-indicator { ::ng-deep .ng-dropdown-panel .ng-option.ng-option-marked .hierarchy-indicator {
background: transparent; background: transparent;
} }
@@ -3,7 +3,7 @@
<i-bs name="person-fill-lock"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div> <i-bs name="person-fill-lock"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span> <pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
</button> </button>
<div class="dropdown-menu permission-filter-dropdown shadow py-0 w-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div [id]="dropdownMenuId" class="dropdown-menu permission-filter-dropdown shadow py-0 w-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled"> <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
@@ -58,7 +58,10 @@
<button *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }" class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" [disabled]="disabled"> <button *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }" class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
@if (selectionModel.ownerFilter() === OwnerFilterType.OTHERS) { @if (selectionModel.ownerFilter() === OwnerFilterType.OTHERS) {
<i-bs width="1em" height="1em" name="check"></i-bs> <a class="text-reset focus-variants" href="javascript:void(0)" (click)="clearIncludeUsers()" aria-label="Clear selected users" i18n-aria-label>
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
</a>
} }
</div> </div>
<div class="me-1 w-100"> <div class="me-1 w-100">
@@ -69,6 +72,7 @@
(ngModelChange)="selectionModel.includeUsers.set($event)" (ngModelChange)="selectionModel.includeUsers.set($event)"
[disabled]="disabled" [disabled]="disabled"
[clearable]="false" [clearable]="false"
[appendTo]="'#' + dropdownMenuId"
[items]="users()" [items]="users()"
bindLabel="username" bindLabel="username"
multiple="true" multiple="true"
@@ -6,3 +6,18 @@
min-width: 1em; min-width: 1em;
min-height: 1em; min-height: 1em;
} }
.focus-variants {
.variant-focused {
display: none;
}
&:hover, &:focus {
.variant-unfocused {
display: none;
}
.variant-focused {
display: block;
}
}
}
@@ -171,6 +171,21 @@ describe('PermissionsFilterDropdownComponent', () => {
expect(component.selectionModel.ownerFilter()).toEqual(OwnerFilterType.NONE) expect(component.selectionModel.ownerFilter()).toEqual(OwnerFilterType.NONE)
}) })
it('should clear selected users', () => {
component.selectionModel.includeUsers.set([12])
component.onUserSelect()
component.selectionModel.hideUnowned.set(true)
expect(component.selectionModel.ownerFilter()).toEqual(
OwnerFilterType.OTHERS
)
component.clearIncludeUsers()
expect(component.selectionModel.includeUsers()).toEqual([])
expect(component.selectionModel.ownerFilter()).toEqual(OwnerFilterType.NONE)
expect(component.selectionModel.hideUnowned()).toBeTruthy()
})
it('should emit a selection model depending on the type of owner filter set', () => { it('should emit a selection model depending on the type of owner filter set', () => {
const emitted = () => ({ const emitted = () => ({
excludeUsers: ownerFilterSetResult.excludeUsers(), excludeUsers: ownerFilterSetResult.excludeUsers(),
@@ -1,3 +1,4 @@
import { _IdGenerator } from '@angular/cdk/a11y'
import { NgClass } from '@angular/common' import { NgClass } from '@angular/common'
import { import {
Component, Component,
@@ -70,6 +71,11 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
public OwnerFilterType = OwnerFilterType public OwnerFilterType = OwnerFilterType
private readonly idGenerator = inject(_IdGenerator)
public readonly dropdownMenuId = this.idGenerator.getId(
'pngx-permissions-filter-dropdown-'
)
@Input() @Input()
title: string title: string
@@ -205,6 +211,11 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
this.ownerFilterSet.emit(this.selectionModel) this.ownerFilterSet.emit(this.selectionModel)
} }
clearIncludeUsers() {
this.selectionModel.includeUsers.set([])
this.onUserSelect()
}
onUserSelect() { onUserSelect() {
this.selectionModel.ownerFilter.set( this.selectionModel.ownerFilter.set(
this.selectionModel.includeUsers()?.length this.selectionModel.includeUsers()?.length
+16 -11
View File
@@ -374,17 +374,6 @@ th[pngxSortable] {
} }
} }
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked {
background-color: var(--pngx-bg-darker) !important;
color: var(--pngx-body-color-accent) !important;
}
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected,
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected.ng-option-marked {
font-weight: bold;
background-color: var(--pngx-bg-alt2) !important;
}
&.ng-select-multiple .ng-select-container .ng-value-container .ng-placeholder, &.ng-select-multiple .ng-select-container .ng-value-container .ng-placeholder,
.ng-select-container .ng-value-container .ng-placeholder { .ng-select-container .ng-value-container .ng-placeholder {
top: 7px; top: 7px;
@@ -402,6 +391,22 @@ th[pngxSortable] {
} }
} }
// ng-select renders the dropdown panel in a CDK overlay outside the component
.ng-dropdown-panel {
font-size: var(--pngx-body-font-size);
.ng-dropdown-panel-items .ng-option.ng-option-marked {
background-color: var(--pngx-bg-darker) !important;
color: var(--pngx-body-color-accent) !important;
}
.ng-dropdown-panel-items .ng-option.ng-option-selected,
.ng-dropdown-panel-items .ng-option.ng-option-selected.ng-option-marked {
font-weight: bold;
background-color: var(--pngx-bg-alt2) !important;
}
}
.paperless-input-tags { .paperless-input-tags {
.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value { .ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value {
background-color: transparent; background-color: transparent;
+2 -2
View File
@@ -237,8 +237,8 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,%3csvg xmlns='h
mix-blend-mode: luminosity; mix-blend-mode: luminosity;
} }
.paperless-input-select .ng-select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option:not(.ng-option-selected):hover, .ng-dropdown-panel .ng-dropdown-panel-items .ng-option:not(.ng-option-selected):hover,
.paperless-input-select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked { .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked {
background-color: var(--bs-light) !important; background-color: var(--bs-light) !important;
} }
+3 -3
View File
@@ -708,9 +708,9 @@
} }
}, },
"node_modules/postcss-selector-parser": { "node_modules/postcss-selector-parser": {
"version": "6.1.2", "version": "6.1.4",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {