- @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tags.length > 0) {
+ @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tagSelectionModel.items.length > 0) {
}
- @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondents.length > 0) {
+ @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondentSelectionModel.items.length > 0) {
}
- @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypes.length > 0) {
+ @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypeSelectionModel.items.length > 0) {
}
- @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePaths.length > 0) {
+ @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePathSelectionModel.items.length > 0) {
{
value: '12',
},
]
- expect(component.correspondentSelectionModel.logicalOperator).toEqual(
- LogicalOperator.Or
- )
expect(component.correspondentSelectionModel.intersection).toEqual(
Intersection.Include
)
@@ -681,6 +679,19 @@ describe('FilterEditorComponent', () => {
correspondents[0],
])
component.toggleCorrespondent(12) // coverage
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_CORRESPONDENT,
+ value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+ },
+ ]
+ expect(component.correspondentSelectionModel.intersection).toEqual(
+ Intersection.Exclude
+ )
+ expect(component.correspondentSelectionModel.getExcludedItems()).toEqual([
+ { id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
+ ])
}))
it('should ingest filter rules for has any of correspondents', fakeAsync(() => {
@@ -754,9 +765,6 @@ describe('FilterEditorComponent', () => {
value: '22',
},
]
- expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
- LogicalOperator.Or
- )
expect(component.documentTypeSelectionModel.intersection).toEqual(
Intersection.Include
)
@@ -764,6 +772,19 @@ describe('FilterEditorComponent', () => {
document_types[0],
])
component.toggleDocumentType(22) // coverage
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOCUMENT_TYPE,
+ value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+ },
+ ]
+ expect(component.documentTypeSelectionModel.intersection).toEqual(
+ Intersection.Exclude
+ )
+ expect(component.documentTypeSelectionModel.getExcludedItems()).toEqual([
+ { id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
+ ])
}))
it('should ingest filter rules for has any of document types', fakeAsync(() => {
@@ -780,9 +801,6 @@ describe('FilterEditorComponent', () => {
value: '23',
},
]
- expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
- LogicalOperator.Or
- )
expect(component.documentTypeSelectionModel.intersection).toEqual(
Intersection.Include
)
@@ -837,9 +855,6 @@ describe('FilterEditorComponent', () => {
value: '32',
},
]
- expect(component.storagePathSelectionModel.logicalOperator).toEqual(
- LogicalOperator.Or
- )
expect(component.storagePathSelectionModel.intersection).toEqual(
Intersection.Include
)
@@ -847,6 +862,19 @@ describe('FilterEditorComponent', () => {
storage_paths[0],
])
component.toggleStoragePath(32) // coverage
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_STORAGE_PATH,
+ value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+ },
+ ]
+ expect(component.storagePathSelectionModel.intersection).toEqual(
+ Intersection.Exclude
+ )
+ expect(component.storagePathSelectionModel.getExcludedItems()).toEqual([
+ { id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
+ ])
}))
it('should ingest filter rules for has any of storage paths', fakeAsync(() => {
@@ -1398,6 +1426,19 @@ describe('FilterEditorComponent', () => {
value: null,
},
])
+
+ const excludeButton = correspondentsFilterableDropdown.queryAll(
+ By.css('input[value=exclude]')
+ )[0]
+ excludeButton.nativeElement.checked = true
+ excludeButton.triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_CORRESPONDENT,
+ value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+ },
+ ])
}))
it('should convert user input to correct filter rules on document type selections', fakeAsync(() => {
@@ -1455,6 +1496,19 @@ describe('FilterEditorComponent', () => {
value: null,
},
])
+
+ const excludeButton = docTypesFilterableDropdown.queryAll(
+ By.css('input[value=exclude]')
+ )[0]
+ excludeButton.nativeElement.checked = true
+ excludeButton.triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_DOCUMENT_TYPE,
+ value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+ },
+ ])
}))
it('should convert user input to correct filter rules on storage path selections', fakeAsync(() => {
@@ -1512,6 +1566,19 @@ describe('FilterEditorComponent', () => {
value: null,
},
])
+
+ const excludeButton = storagePathsFilterableDropdown.queryAll(
+ By.css('input[value=exclude]')
+ )[0]
+ excludeButton.nativeElement.checked = true
+ excludeButton.triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_STORAGE_PATH,
+ value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+ },
+ ])
}))
it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => {
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 debd7b4b3..88f1be48b 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
@@ -26,14 +26,12 @@ import {
switchMap,
takeUntil,
} from 'rxjs/operators'
-import { Correspondent } from 'src/app/data/correspondent'
import { CustomField } from 'src/app/data/custom-field'
import {
CustomFieldQueryLogicalOperator,
CustomFieldQueryOperator,
} from 'src/app/data/custom-field-query'
import { Document } from 'src/app/data/document'
-import { DocumentType } from 'src/app/data/document-type'
import { FilterRule } from 'src/app/data/filter-rule'
import {
FILTER_ADDED_AFTER,
@@ -75,9 +73,8 @@ import {
FILTER_STORAGE_PATH,
FILTER_TITLE,
FILTER_TITLE_CONTENT,
+ NEGATIVE_NULL_FILTER_VALUE,
} from 'src/app/data/filter-rule-type'
-import { StoragePath } from 'src/app/data/storage-path'
-import { Tag } from 'src/app/data/tag'
import {
PermissionAction,
PermissionType,
@@ -251,7 +248,9 @@ export class FilterEditorComponent
case FILTER_HAS_CORRESPONDENT_ANY:
if (rule.value) {
return $localize`Correspondent: ${
- this.correspondents.find((c) => c.id == +rule.value)?.name
+ this.correspondentSelectionModel.items.find(
+ (c) => c.id == +rule.value
+ )?.name
}`
} else {
return $localize`Without correspondent`
@@ -261,7 +260,9 @@ export class FilterEditorComponent
case FILTER_HAS_DOCUMENT_TYPE_ANY:
if (rule.value) {
return $localize`Document type: ${
- this.documentTypes.find((dt) => dt.id == +rule.value)?.name
+ this.documentTypeSelectionModel.items.find(
+ (dt) => dt.id == +rule.value
+ )?.name
}`
} else {
return $localize`Without document type`
@@ -271,7 +272,9 @@ export class FilterEditorComponent
case FILTER_HAS_STORAGE_PATH_ANY:
if (rule.value) {
return $localize`Storage path: ${
- this.storagePaths.find((sp) => sp.id == +rule.value)?.name
+ this.storagePathSelectionModel.items.find(
+ (sp) => sp.id == +rule.value
+ )?.name
}`
} else {
return $localize`Without storage path`
@@ -279,7 +282,7 @@ export class FilterEditorComponent
case FILTER_HAS_TAGS_ALL:
return $localize`Tag: ${
- this.tags.find((t) => t.id == +rule.value)?.name
+ this.tagSelectionModel.items.find((t) => t.id == +rule.value)?.name
}`
case FILTER_HAS_ANY_TAG:
@@ -326,10 +329,6 @@ export class FilterEditorComponent
@ViewChild('textFilterInput')
textFilterInput: ElementRef
- tags: Tag[] = []
- correspondents: Correspondent[] = []
- documentTypes: DocumentType[] = []
- storagePaths: StoragePath[] = []
customFields: CustomField[] = []
tagDocumentCounts: SelectionDataItem[]
@@ -370,7 +369,7 @@ export class FilterEditorComponent
)
}
- tagSelectionModel = new FilterableDropdownSelectionModel()
+ tagSelectionModel = new FilterableDropdownSelectionModel(true)
correspondentSelectionModel = new FilterableDropdownSelectionModel()
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
storagePathSelectionModel = new FilterableDropdownSelectionModel()
@@ -551,6 +550,19 @@ export class FilterEditorComponent
)
break
case FILTER_CORRESPONDENT:
+ this.correspondentSelectionModel.intersection =
+ rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
+ ? Intersection.Exclude
+ : Intersection.Include
+ this.correspondentSelectionModel.set(
+ rule.value ? +rule.value : null,
+ this.correspondentSelectionModel.intersection ==
+ Intersection.Include
+ ? ToggleableItemState.Selected
+ : ToggleableItemState.Excluded,
+ false
+ )
+ break
case FILTER_HAS_CORRESPONDENT_ANY:
this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or
this.correspondentSelectionModel.intersection = Intersection.Include
@@ -569,6 +581,18 @@ export class FilterEditorComponent
)
break
case FILTER_DOCUMENT_TYPE:
+ this.documentTypeSelectionModel.intersection =
+ rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
+ ? Intersection.Exclude
+ : Intersection.Include
+ this.documentTypeSelectionModel.set(
+ rule.value ? +rule.value : null,
+ this.documentTypeSelectionModel.intersection == Intersection.Include
+ ? ToggleableItemState.Selected
+ : ToggleableItemState.Excluded,
+ false
+ )
+ break
case FILTER_HAS_DOCUMENT_TYPE_ANY:
this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or
this.documentTypeSelectionModel.intersection = Intersection.Include
@@ -587,6 +611,18 @@ export class FilterEditorComponent
)
break
case FILTER_STORAGE_PATH:
+ this.storagePathSelectionModel.intersection =
+ rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
+ ? Intersection.Exclude
+ : Intersection.Include
+ this.storagePathSelectionModel.set(
+ rule.value ? +rule.value : null,
+ this.storagePathSelectionModel.intersection == Intersection.Include
+ ? ToggleableItemState.Selected
+ : ToggleableItemState.Excluded,
+ false
+ )
+ break
case FILTER_HAS_STORAGE_PATH_ANY:
this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
this.storagePathSelectionModel.intersection = Intersection.Include
@@ -809,9 +845,21 @@ export class FilterEditorComponent
})
})
}
- if (this.correspondentSelectionModel.isNoneSelected()) {
+ if (
+ this.correspondentSelectionModel.isNoneSelected() &&
+ this.correspondentSelectionModel.intersection == Intersection.Include
+ ) {
filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null })
} else {
+ if (
+ this.correspondentSelectionModel.isNoneSelected() &&
+ this.correspondentSelectionModel.intersection == Intersection.Exclude
+ ) {
+ filterRules.push({
+ rule_type: FILTER_CORRESPONDENT,
+ value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+ })
+ }
this.correspondentSelectionModel
.getSelectedItems()
.forEach((correspondent) => {
@@ -822,6 +870,7 @@ export class FilterEditorComponent
})
this.correspondentSelectionModel
.getExcludedItems()
+ .filter((correspondent) => correspondent.id > 0)
.forEach((correspondent) => {
filterRules.push({
rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
@@ -829,9 +878,21 @@ export class FilterEditorComponent
})
})
}
- if (this.documentTypeSelectionModel.isNoneSelected()) {
+ if (
+ this.documentTypeSelectionModel.isNoneSelected() &&
+ this.documentTypeSelectionModel.intersection === Intersection.Include
+ ) {
filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null })
} else {
+ if (
+ this.documentTypeSelectionModel.isNoneSelected() &&
+ this.documentTypeSelectionModel.intersection == Intersection.Exclude
+ ) {
+ filterRules.push({
+ rule_type: FILTER_DOCUMENT_TYPE,
+ value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+ })
+ }
this.documentTypeSelectionModel
.getSelectedItems()
.forEach((documentType) => {
@@ -842,6 +903,7 @@ export class FilterEditorComponent
})
this.documentTypeSelectionModel
.getExcludedItems()
+ .filter((documentType) => documentType.id > 0)
.forEach((documentType) => {
filterRules.push({
rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
@@ -849,9 +911,21 @@ export class FilterEditorComponent
})
})
}
- if (this.storagePathSelectionModel.isNoneSelected()) {
+ if (
+ this.storagePathSelectionModel.isNoneSelected() &&
+ this.storagePathSelectionModel.intersection == Intersection.Include
+ ) {
filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
} else {
+ if (
+ this.storagePathSelectionModel.isNoneSelected() &&
+ this.storagePathSelectionModel.intersection == Intersection.Exclude
+ ) {
+ filterRules.push({
+ rule_type: FILTER_STORAGE_PATH,
+ value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+ })
+ }
this.storagePathSelectionModel
.getSelectedItems()
.forEach((storagePath) => {
@@ -862,6 +936,7 @@ export class FilterEditorComponent
})
this.storagePathSelectionModel
.getExcludedItems()
+ .filter((storagePath) => storagePath.id > 0)
.forEach((storagePath) => {
filterRules.push({
rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
@@ -1062,7 +1137,7 @@ export class FilterEditorComponent
) {
this.loadingCountTotal++
this.tagService.listAll().subscribe((result) => {
- this.tags = result.results
+ this.tagSelectionModel.items = result.results
this.maybeCompleteLoading()
})
}
@@ -1074,7 +1149,7 @@ export class FilterEditorComponent
) {
this.loadingCountTotal++
this.correspondentService.listAll().subscribe((result) => {
- this.correspondents = result.results
+ this.correspondentSelectionModel.items = result.results
this.maybeCompleteLoading()
})
}
@@ -1086,7 +1161,7 @@ export class FilterEditorComponent
) {
this.loadingCountTotal++
this.documentTypeService.listAll().subscribe((result) => {
- this.documentTypes = result.results
+ this.documentTypeSelectionModel.items = result.results
this.maybeCompleteLoading()
})
}
@@ -1098,7 +1173,7 @@ export class FilterEditorComponent
) {
this.loadingCountTotal++
this.storagePathService.listAll().subscribe((result) => {
- this.storagePaths = result.results
+ this.storagePathSelectionModel.items = result.results
this.maybeCompleteLoading()
})
}
diff --git a/src-ui/src/app/data/filter-rule-type.ts b/src-ui/src/app/data/filter-rule-type.ts
index bb2bf762c..7f0f0d56d 100644
--- a/src-ui/src/app/data/filter-rule-type.ts
+++ b/src-ui/src/app/data/filter-rule-type.ts
@@ -1,5 +1,7 @@
import { DataType } from './datatype'
+export const NEGATIVE_NULL_FILTER_VALUE = -1
+
// These correspond to src/documents/models.py and changes here require a DB migration (and vice versa)
export const FILTER_TITLE = 0
export const FILTER_CONTENT = 1
diff --git a/src-ui/src/app/utils/query-params.spec.ts b/src-ui/src/app/utils/query-params.spec.ts
index cc91f3f6c..c22c90d11 100644
--- a/src-ui/src/app/utils/query-params.spec.ts
+++ b/src-ui/src/app/utils/query-params.spec.ts
@@ -8,6 +8,7 @@ import {
FILTER_HAS_CUSTOM_FIELDS_ALL,
FILTER_HAS_CUSTOM_FIELDS_ANY,
FILTER_HAS_TAGS_ALL,
+ NEGATIVE_NULL_FILTER_VALUE,
} from '../data/filter-rule-type'
import {
filterRulesFromQueryParams,
@@ -97,6 +98,16 @@ describe('QueryParams Utils', () => {
correspondent__isnull: 1,
})
+ params = queryParamsFromFilterRules([
+ {
+ rule_type: FILTER_CORRESPONDENT,
+ value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+ },
+ ])
+ expect(params).toEqual({
+ correspondent__isnull: 0,
+ })
+
params = queryParamsFromFilterRules([
{
rule_type: FILTER_HAS_ANY_TAG,
diff --git a/src-ui/src/app/utils/query-params.ts b/src-ui/src/app/utils/query-params.ts
index d90167c5b..27716cc2d 100644
--- a/src-ui/src/app/utils/query-params.ts
+++ b/src-ui/src/app/utils/query-params.ts
@@ -10,6 +10,7 @@ import {
FILTER_HAS_CUSTOM_FIELDS_ANY,
FILTER_RULE_TYPES,
FilterRuleType,
+ NEGATIVE_NULL_FILTER_VALUE,
} from '../data/filter-rule-type'
import { ListViewState } from '../services/document-list-view.service'
@@ -113,6 +114,10 @@ export function filterRulesFromQueryParams(
rt.isnull_filtervar == filterQueryParamName
)
const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName
+ const nullRuleValue =
+ queryParams.get(filterQueryParamName) == '1'
+ ? null
+ : NEGATIVE_NULL_FILTER_VALUE.toString()
const valueURIComponent: string = queryParams.get(filterQueryParamName)
const filterQueryParamValues: string[] = rule_type.multi
? valueURIComponent.split(',')
@@ -125,7 +130,7 @@ export function filterRulesFromQueryParams(
val = val.replace('1', 'true').replace('0', 'false')
return {
rule_type: rule_type.id,
- value: isNullRuleType ? null : val,
+ value: isNullRuleType ? nullRuleValue : val,
}
})
)
@@ -143,6 +148,11 @@ export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params {
let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
if (ruleType.isnull_filtervar && rule.value == null) {
params[ruleType.isnull_filtervar] = 1
+ } else if (
+ ruleType.isnull_filtervar &&
+ rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
+ ) {
+ params[ruleType.isnull_filtervar] = 0
} else if (ruleType.multi) {
params[ruleType.filtervar] = params[ruleType.filtervar]
? params[ruleType.filtervar] + ',' + rule.value