From f0157a36fbbfc1257e78068a6606b9c31dc2c95f Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Sun, 1 Sep 2024 00:29:52 -0700
Subject: [PATCH] Gotta start somewhere
---
src-ui/src/app/app.module.ts | 2 +
...stom-fields-lookup-dropdown.component.html | 44 +++++
...stom-fields-lookup-dropdown.component.scss | 8 +
...custom-fields-lookup-dropdown.component.ts | 158 ++++++++++++++++++
.../filter-editor.component.html | 11 +-
.../filter-editor.component.spec.ts | 34 ++--
.../filter-editor/filter-editor.component.ts | 110 +++++-------
src-ui/src/app/data/filter-rule-type.ts | 8 +
src/documents/models.py | 1 +
9 files changed, 275 insertions(+), 101 deletions(-)
create mode 100644 src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.html
create mode 100644 src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.scss
create mode 100644 src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.ts
diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts
index 005de5369..75b050767 100644
--- a/src-ui/src/app/app.module.ts
+++ b/src-ui/src/app/app.module.ts
@@ -108,6 +108,7 @@ import { FileDropComponent } from './components/file-drop/file-drop.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
+import { CustomFieldsLookupDropdownComponent } from './components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component'
import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
import { PdfViewerModule } from 'ng2-pdf-viewer'
import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component'
@@ -485,6 +486,7 @@ function initializeApp(settings: SettingsService) {
CustomFieldsComponent,
CustomFieldEditDialogComponent,
CustomFieldsDropdownComponent,
+ CustomFieldsLookupDropdownComponent,
ProfileEditDialogComponent,
DocumentLinkComponent,
PreviewPopupComponent,
diff --git a/src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.html b/src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.html
new file mode 100644
index 000000000..1a170f770
--- /dev/null
+++ b/src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+ @for (query of selectionModel.queries; track query; let i = $index) {
+
+
+
+
+ @switch (query.operator) {
+ @case ('exists') {
+
+ }
+ @default {
+
+ }
+ }
+
+
+
+ }
+
+
+
diff --git a/src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.scss b/src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.scss
new file mode 100644
index 000000000..bfd1bd9b7
--- /dev/null
+++ b/src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.scss
@@ -0,0 +1,8 @@
+.dropdown-menu {
+ width: 450px;
+}
+
+::ng-deep .ng-select-container {
+ border-top-right-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+}
diff --git a/src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.ts b/src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.ts
new file mode 100644
index 000000000..229e59111
--- /dev/null
+++ b/src-ui/src/app/components/common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component.ts
@@ -0,0 +1,158 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core'
+import { Subject, first, takeUntil } from 'rxjs'
+import { CustomField } from 'src/app/data/custom-field'
+import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
+
+export class CustomFieldQuery {
+ public changed = new Subject()
+
+ private _field: string
+ set field(value: string) {
+ this._field = value
+ this.changed.next(this)
+ }
+ get field(): string {
+ return this._field
+ }
+
+ private _operator: string
+ set operator(value: string) {
+ this._operator = value
+ this.changed.next(this)
+ }
+ get operator(): string {
+ return this._operator
+ }
+
+ private _value: string
+ set value(value: string) {
+ this._value = value
+ this.changed.next(this)
+ }
+ get value(): string {
+ return this._value
+ }
+
+ constructor(
+ field: string = null,
+ operator: string = null,
+ value: string = null
+ ) {
+ this.field = field
+ this.operator = operator
+ this.value = value
+ }
+}
+
+export class CustomFieldQueriesModel {
+ // matchingModel: MatchingModel
+ queries: CustomFieldQuery[] = []
+
+ changed = new Subject()
+
+ public clear(fireEvent = true) {
+ this.queries = []
+ if (fireEvent) {
+ this.changed.next(this)
+ }
+ }
+
+ public addQuery(query: CustomFieldQuery = new CustomFieldQuery()) {
+ this.queries.push(query)
+ query.changed.subscribe(() => {
+ if (query.field && query.operator && query.value) {
+ this.changed.next(this)
+ }
+ })
+ }
+
+ public removeQuery(index: number) {
+ const query = this.queries.splice(index, 1)[0]
+ query.changed.complete()
+ this.changed.next(this)
+ }
+}
+
+@Component({
+ selector: 'pngx-custom-fields-lookup-dropdown',
+ templateUrl: './custom-fields-lookup-dropdown.component.html',
+ styleUrls: ['./custom-fields-lookup-dropdown.component.scss'],
+})
+export class CustomFieldsLookupDropdownComponent {
+ @Input()
+ title: string
+
+ @Input()
+ filterPlaceholder: string = ''
+
+ @Input()
+ icon: string
+
+ @Input()
+ allowSelectNone: boolean = false
+
+ @Input()
+ editing = false
+
+ @Input()
+ applyOnClose = false
+
+ get name(): string {
+ return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
+ }
+
+ @Input()
+ disabled: boolean = false
+
+ _selectionModel: CustomFieldQueriesModel = new CustomFieldQueriesModel()
+
+ @Input()
+ set selectionModel(model: CustomFieldQueriesModel) {
+ model.changed.subscribe((updatedModel) => {
+ this.selectionModelChange.next(updatedModel)
+ })
+ this._selectionModel = model
+ }
+
+ get selectionModel(): CustomFieldQueriesModel {
+ return this._selectionModel
+ }
+
+ @Output()
+ selectionModelChange = new EventEmitter()
+
+ customFields: CustomField[] = []
+
+ private unsubscribeNotifier: Subject = new Subject()
+
+ constructor(protected customFieldsService: CustomFieldsService) {
+ this.getFields()
+ }
+
+ ngOnDestroy(): void {
+ this.unsubscribeNotifier.next(this)
+ this.unsubscribeNotifier.complete()
+ }
+
+ private getFields() {
+ this.customFieldsService
+ .listAll()
+ .pipe(first(), takeUntil(this.unsubscribeNotifier))
+ .subscribe((result) => {
+ this.customFields = result.results
+ })
+ }
+
+ public addQuery() {
+ this.selectionModel.addQuery()
+ }
+
+ public removeQuery(index: number) {
+ this.selectionModel.removeQuery(index)
+ }
+
+ getOperatorsForField(field: CustomField): string[] {
+ return ['exact', 'in', 'isnull', 'exists']
+ // TODO: implement this
+ }
+}
diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
index 99ef0cdc7..9308733ec 100644
--- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
+++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
@@ -86,15 +86,10 @@
}
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.CustomField) && customFields.length > 0) {
-
+ >
}
{
}))
it('should ingest filter rules for has all custom fields', fakeAsync(() => {
- expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
- 0
- )
+ expect(component.customFieldQueriesModel.getSelectedItems()).toHaveLength(0)
component.filterRules = [
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
@@ -852,10 +850,10 @@ describe('FilterEditorComponent', () => {
value: '43',
},
]
- expect(component.customFieldSelectionModel.logicalOperator).toEqual(
+ expect(component.customFieldQueriesModel.logicalOperator).toEqual(
LogicalOperator.And
)
- expect(component.customFieldSelectionModel.getSelectedItems()).toEqual(
+ expect(component.customFieldQueriesModel.getSelectedItems()).toEqual(
custom_fields
)
// coverage
@@ -869,9 +867,7 @@ describe('FilterEditorComponent', () => {
}))
it('should ingest filter rules for has any custom fields', fakeAsync(() => {
- expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
- 0
- )
+ expect(component.customFieldQueriesModel.getSelectedItems()).toHaveLength(0)
component.filterRules = [
{
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
@@ -882,10 +878,10 @@ describe('FilterEditorComponent', () => {
value: '43',
},
]
- expect(component.customFieldSelectionModel.logicalOperator).toEqual(
+ expect(component.customFieldQueriesModel.logicalOperator).toEqual(
LogicalOperator.Or
)
- expect(component.customFieldSelectionModel.getSelectedItems()).toEqual(
+ expect(component.customFieldQueriesModel.getSelectedItems()).toEqual(
custom_fields
)
// coverage
@@ -898,25 +894,19 @@ describe('FilterEditorComponent', () => {
}))
it('should ingest filter rules for has any custom field', fakeAsync(() => {
- expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
- 0
- )
+ expect(component.customFieldQueriesModel.getSelectedItems()).toHaveLength(0)
component.filterRules = [
{
rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS,
value: '1',
},
]
- expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
- 1
- )
- expect(component.customFieldSelectionModel.get(null)).toBeTruthy()
+ expect(component.customFieldQueriesModel.getSelectedItems()).toHaveLength(1)
+ expect(component.customFieldQueriesModel.get(null)).toBeTruthy()
}))
it('should ingest filter rules for exclude tag(s)', fakeAsync(() => {
- expect(component.customFieldSelectionModel.getExcludedItems()).toHaveLength(
- 0
- )
+ expect(component.customFieldQueriesModel.getExcludedItems()).toHaveLength(0)
component.filterRules = [
{
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
@@ -927,10 +917,10 @@ describe('FilterEditorComponent', () => {
value: '43',
},
]
- expect(component.customFieldSelectionModel.logicalOperator).toEqual(
+ expect(component.customFieldQueriesModel.logicalOperator).toEqual(
LogicalOperator.And
)
- expect(component.customFieldSelectionModel.getExcludedItems()).toEqual(
+ expect(component.customFieldQueriesModel.getExcludedItems()).toEqual(
custom_fields
)
// coverage
diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
index fe1f6cc8c..f51862b7e 100644
--- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
+++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
@@ -12,7 +12,7 @@ import {
import { Tag } from 'src/app/data/tag'
import { Correspondent } from 'src/app/data/correspondent'
import { DocumentType } from 'src/app/data/document-type'
-import { Observable, Subject, Subscription, from } from 'rxjs'
+import { Observable, Subject, from } from 'rxjs'
import {
catchError,
debounceTime,
@@ -63,6 +63,7 @@ import {
FILTER_HAS_CUSTOM_FIELDS_ALL,
FILTER_HAS_ANY_CUSTOM_FIELDS,
FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
+ FILTER_CUSTOM_FIELDS_LOOKUP,
} from 'src/app/data/filter-rule-type'
import {
FilterableDropdownSelectionModel,
@@ -92,13 +93,16 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { CustomField } from 'src/app/data/custom-field'
import { SearchService } from 'src/app/services/rest/search.service'
+import {
+ CustomFieldQueriesModel,
+ CustomFieldQuery,
+} from '../../common/custom-fields-lookup-dropdown/custom-fields-lookup-dropdown.component'
const TEXT_FILTER_TARGET_TITLE = 'title'
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
const TEXT_FILTER_TARGET_ASN = 'asn'
const TEXT_FILTER_TARGET_FULLTEXT_QUERY = 'fulltext-query'
const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = 'fulltext-morelike'
-const TEXT_FILTER_TARGET_CUSTOM_FIELDS = 'custom-fields'
const TEXT_FILTER_MODIFIER_EQUALS = 'equals'
const TEXT_FILTER_MODIFIER_NULL = 'is null'
@@ -134,10 +138,6 @@ const DEFAULT_TEXT_FILTER_TARGET_OPTIONS = [
name: $localize`Title & content`,
},
{ id: TEXT_FILTER_TARGET_ASN, name: $localize`ASN` },
- {
- id: TEXT_FILTER_TARGET_CUSTOM_FIELDS,
- name: $localize`Custom fields`,
- },
{
id: TEXT_FILTER_TARGET_FULLTEXT_QUERY,
name: $localize`Advanced search`,
@@ -321,7 +321,7 @@ export class FilterEditorComponent
correspondentSelectionModel = new FilterableDropdownSelectionModel()
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
storagePathSelectionModel = new FilterableDropdownSelectionModel()
- customFieldSelectionModel = new FilterableDropdownSelectionModel()
+ customFieldQueriesModel = new CustomFieldQueriesModel()
dateCreatedBefore: string
dateCreatedAfter: string
@@ -356,7 +356,7 @@ export class FilterEditorComponent
this.storagePathSelectionModel.clear(false)
this.tagSelectionModel.clear(false)
this.correspondentSelectionModel.clear(false)
- this.customFieldSelectionModel.clear(false)
+ this.customFieldQueriesModel.clear(false)
this._textFilter = null
this._moreLikeId = null
this.dateAddedBefore = null
@@ -383,8 +383,7 @@ export class FilterEditorComponent
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
break
case FILTER_CUSTOM_FIELDS_TEXT:
- this._textFilter = rule.value
- this.textFilterTarget = TEXT_FILTER_TARGET_CUSTOM_FIELDS
+ console.log('FILTER_CUSTOM_FIELDS_TEXT', rule.value)
break
case FILTER_FULLTEXT_QUERY:
let allQueryArgs = rule.value.split(',')
@@ -523,35 +522,28 @@ export class FilterEditorComponent
false
)
break
- case FILTER_HAS_CUSTOM_FIELDS_ALL:
- this.customFieldSelectionModel.logicalOperator = LogicalOperator.And
- this.customFieldSelectionModel.set(
- rule.value ? +rule.value : null,
- ToggleableItemState.Selected,
- false
+ case FILTER_CUSTOM_FIELDS_LOOKUP:
+ // TODO: fully implement
+ const query = JSON.parse(rule.value)
+ this.customFieldQueriesModel.addQuery(
+ new CustomFieldQuery(query[0], query[1], query[2])
)
break
+ case FILTER_HAS_CUSTOM_FIELDS_ALL:
+ console.log('FILTER_HAS_CUSTOM_FIELDS_ALL', rule.value)
+ // TODO: fully implement
+ break
case FILTER_HAS_CUSTOM_FIELDS_ANY:
- this.customFieldSelectionModel.logicalOperator = LogicalOperator.Or
- this.customFieldSelectionModel.set(
- rule.value ? +rule.value : null,
- ToggleableItemState.Selected,
- false
- )
+ console.log('FILTER_HAS_CUSTOM_FIELDS_ANY', rule.value)
+ // TODO: fully implement
break
case FILTER_HAS_ANY_CUSTOM_FIELDS:
- this.customFieldSelectionModel.set(
- null,
- ToggleableItemState.Selected,
- false
- )
+ console.log('FILTER_HAS_ANY_CUSTOM_FIELDS', rule.value)
+ // TODO: fully implement
break
case FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS:
- this.customFieldSelectionModel.set(
- rule.value ? +rule.value : null,
- ToggleableItemState.Excluded,
- false
- )
+ console.log('FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS', rule.value)
+ // TODO: fully implement
break
case FILTER_ASN_ISNULL:
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
@@ -655,15 +647,6 @@ export class FilterEditorComponent
})
}
}
- if (
- this._textFilter &&
- this.textFilterTarget == TEXT_FILTER_TARGET_CUSTOM_FIELDS
- ) {
- filterRules.push({
- rule_type: FILTER_CUSTOM_FIELDS_TEXT,
- value: this._textFilter,
- })
- }
if (
this._textFilter &&
this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_QUERY
@@ -768,35 +751,24 @@ export class FilterEditorComponent
})
})
}
- if (this.customFieldSelectionModel.isNoneSelected()) {
+ let queries = this.customFieldQueriesModel.queries
+ .filter((query) => query.field && query.operator)
+ .map((query) => [query.field, query.operator, query.value])
+ console.log(
+ 'this.customFieldQueriesModel.queries',
+ this.customFieldQueriesModel.queries
+ )
+ console.log('queries', queries)
+ if (queries.length > 0) {
filterRules.push({
- rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS,
- value: 'false',
+ rule_type: FILTER_CUSTOM_FIELDS_LOOKUP,
+ value:
+ queries.length === 1
+ ? JSON.stringify(queries[0])
+ : JSON.stringify(queries),
})
- } else {
- const customFieldFilterType =
- this.customFieldSelectionModel.logicalOperator == LogicalOperator.And
- ? FILTER_HAS_CUSTOM_FIELDS_ALL
- : FILTER_HAS_CUSTOM_FIELDS_ANY
- this.customFieldSelectionModel
- .getSelectedItems()
- .filter((field) => field.id)
- .forEach((field) => {
- filterRules.push({
- rule_type: customFieldFilterType,
- value: field.id?.toString(),
- })
- })
- this.customFieldSelectionModel
- .getExcludedItems()
- .filter((field) => field.id)
- .forEach((field) => {
- filterRules.push({
- rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
- value: field.id?.toString(),
- })
- })
}
+ // TODO: fully implement custom fields
if (this.dateCreatedBefore) {
filterRules.push({
rule_type: FILTER_CREATED_BEFORE,
@@ -1079,10 +1051,6 @@ export class FilterEditorComponent
this.storagePathSelectionModel.apply()
}
- onCustomFieldsDropdownOpen() {
- this.customFieldSelectionModel.apply()
- }
-
updateTextFilter(text, updateRules = true) {
this._textFilter = text
if (updateRules) {
diff --git a/src-ui/src/app/data/filter-rule-type.ts b/src-ui/src/app/data/filter-rule-type.ts
index 9a87a421c..7fa1d2179 100644
--- a/src-ui/src/app/data/filter-rule-type.ts
+++ b/src-ui/src/app/data/filter-rule-type.ts
@@ -55,6 +55,8 @@ export const FILTER_HAS_CUSTOM_FIELDS_ANY = 39
export const FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS = 40
export const FILTER_HAS_ANY_CUSTOM_FIELDS = 41
+export const FILTER_CUSTOM_FIELDS_LOOKUP = 42
+
export const FILTER_RULE_TYPES: FilterRuleType[] = [
{
id: FILTER_TITLE,
@@ -317,6 +319,12 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
multi: false,
default: true,
},
+ {
+ id: FILTER_CUSTOM_FIELDS_LOOKUP,
+ filtervar: 'custom_field_lookup',
+ datatype: 'string',
+ multi: false,
+ },
]
export interface FilterRuleType {
diff --git a/src/documents/models.py b/src/documents/models.py
index 24e8c2b26..ed6560934 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -507,6 +507,7 @@ class SavedViewFilterRule(models.Model):
(39, _("has custom field in")),
(40, _("does not have custom field in")),
(41, _("does not have custom field")),
+ (42, _("custom fields lookup")),
]
saved_view = models.ForeignKey(