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 @@ +
+ + +
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(