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 492b2a4e4..a5e8897f7 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 @@ -10,7 +10,7 @@ } - @if (textFilterTarget === 'asn') { + @if (textFilterTarget === 'asn' || textFilterTarget === 'duplicates') { { expect(component.textFilterTarget).toEqual('mime-type') // TEXT_FILTER_TARGET_MIME_TYPE }) + it('should ingest filter rules for documents with duplicates', () => { + component.filterRules = [ + { + rule_type: FILTER_HAS_DUPLICATES, + value: 'true', + }, + ] + fixture.detectChanges() + + expect(component.textFilterTarget).toEqual('duplicates') + expect(component.textFilterModifier).toEqual('has-duplicates') + expect(component.textFilterInputDisabled).toBeTruthy() + }) + + it('should ingest filter rules for documents without duplicates', () => { + component.filterRules = [ + { + rule_type: FILTER_HAS_DUPLICATES, + value: 'false', + }, + ] + + expect(component.textFilterTarget).toEqual('duplicates') + expect(component.textFilterModifier).toEqual('does-not-have-duplicates') + expect(component.filterRules).toEqual([ + { + rule_type: FILTER_HAS_DUPLICATES, + value: 'false', + }, + ]) + }) + it('should ingest text filter rules for fulltext query', () => { expect(component.textFilter).toEqual(null) component.filterRules = [ @@ -1390,6 +1423,33 @@ describe('FilterEditorComponent', () => { ]) }) + it('should convert duplicate target input to the correct filter rule', () => { + const textFieldTargetDropdown = fixture.debugElement.queryAll( + By.directive(NgbDropdownItem) + )[5] + textFieldTargetDropdown.triggerEventHandler('click') + fixture.detectChanges() + + expect(component.textFilterTarget).toEqual('duplicates') + expect(component.filterRules).toEqual([ + { + rule_type: FILTER_HAS_DUPLICATES, + value: 'true', + }, + ]) + + const textFieldModifierSelect = fixture.debugElement.query(By.css('select')) + textFieldModifierSelect.nativeElement.value = 'does-not-have-duplicates' + textFieldModifierSelect.nativeElement.dispatchEvent(new Event('change')) + fixture.detectChanges() + expect(component.filterRules).toEqual([ + { + rule_type: FILTER_HAS_DUPLICATES, + value: 'false', + }, + ]) + }) + it('should convert user input to correct filter rules on full text query', () => { component.textFilterInput.nativeElement.value = 'foo' component.textFilterInput.nativeElement.dispatchEvent(new Event('input')) @@ -2178,6 +2238,22 @@ describe('FilterEditorComponent', () => { ] expect(component.generateFilterName()).toEqual('Without any tag') + component.filterRules = [ + { + rule_type: FILTER_HAS_DUPLICATES, + value: 'true', + }, + ] + expect(component.generateFilterName()).toEqual('With duplicates') + + component.filterRules = [ + { + rule_type: FILTER_HAS_DUPLICATES, + value: 'false', + }, + ] + expect(component.generateFilterName()).toEqual('Without duplicates') + component.filterRules = [ { rule_type: FILTER_CUSTOM_FIELDS_QUERY, 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 16bf3cccb..ed08cee1d 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 @@ -65,6 +65,7 @@ import { FILTER_HAS_CUSTOM_FIELDS_ALL, FILTER_HAS_CUSTOM_FIELDS_ANY, FILTER_HAS_DOCUMENT_TYPE_ANY, + FILTER_HAS_DUPLICATES, FILTER_HAS_STORAGE_PATH_ANY, FILTER_HAS_TAGS_ALL, FILTER_HAS_TAGS_ANY, @@ -129,12 +130,15 @@ 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_TARGET_MIME_TYPE = 'mime-type' +const TEXT_FILTER_TARGET_DUPLICATES = 'duplicates' const TEXT_FILTER_MODIFIER_EQUALS = 'equals' const TEXT_FILTER_MODIFIER_NULL = 'is null' const TEXT_FILTER_MODIFIER_NOTNULL = 'not null' const TEXT_FILTER_MODIFIER_GT = 'greater' const TEXT_FILTER_MODIFIER_LT = 'less' +const TEXT_FILTER_MODIFIER_HAS_DUPLICATES = 'has-duplicates' +const TEXT_FILTER_MODIFIER_DOES_NOT_HAVE_DUPLICATES = 'does-not-have-duplicates' const RELATIVE_DATE_QUERY_REGEXP_CREATED = /created:[\["]([^\]]+)[\]"]/g const RELATIVE_DATE_QUERY_REGEXP_ADDED = /added:[\["]([^\]]+)[\]"]/g @@ -205,6 +209,7 @@ const DEFAULT_TEXT_FILTER_TARGET_OPTIONS = [ id: TEXT_FILTER_TARGET_FULLTEXT_QUERY, name: $localize`Advanced search`, }, + { id: TEXT_FILTER_TARGET_DUPLICATES, name: $localize`Duplicates` }, ] const DEPRECATED_CUSTOM_FIELDS_TEXT_FILTER_TARGET_OPTION = { @@ -241,6 +246,17 @@ const DEFAULT_TEXT_FILTER_MODIFIER_OPTIONS = [ }, ] +const DUPLICATES_FILTER_MODIFIER_OPTIONS = [ + { + id: TEXT_FILTER_MODIFIER_HAS_DUPLICATES, + label: $localize`exist`, + }, + { + id: TEXT_FILTER_MODIFIER_DOES_NOT_HAVE_DUPLICATES, + label: $localize`do not exist`, + }, +] + @Component({ selector: 'pngx-filter-editor', templateUrl: './filter-editor.component.html', @@ -320,6 +336,12 @@ export class FilterEditorComponent if (rule.value == 'false') { return $localize`Without any tag` } + break + + case FILTER_HAS_DUPLICATES: + return rule.value == 'false' + ? $localize`Without duplicates` + : $localize`With duplicates` case FILTER_CUSTOM_FIELDS_QUERY: return $localize`Custom fields query` @@ -390,7 +412,9 @@ export class FilterEditorComponent public textFilterModifier: string get textFilterModifiers() { - return DEFAULT_TEXT_FILTER_MODIFIER_OPTIONS + return this.textFilterTarget === TEXT_FILTER_TARGET_DUPLICATES + ? DUPLICATES_FILTER_MODIFIER_OPTIONS + : DEFAULT_TEXT_FILTER_MODIFIER_OPTIONS } get textFilterModifierIsNull(): boolean { @@ -399,6 +423,13 @@ export class FilterEditorComponent ) } + get textFilterInputDisabled(): boolean { + return ( + this.textFilterModifierIsNull || + this.textFilterTarget === TEXT_FILTER_TARGET_DUPLICATES + ) + } + tagSelectionModel = new FilterableDropdownSelectionModel(true) correspondentSelectionModel = new FilterableDropdownSelectionModel() documentTypeSelectionModel = new FilterableDropdownSelectionModel() @@ -444,6 +475,7 @@ export class FilterEditorComponent this.customFieldQueriesModel.clear(false) this._textFilter = null this._moreLikeId = null + this.textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT this.dateAddedTo = null this.dateAddedFrom = null this.dateCreatedTo = null @@ -477,6 +509,13 @@ export class FilterEditorComponent this.textFilterTarget = TEXT_FILTER_TARGET_MIME_TYPE this._textFilter = rule.value break + case FILTER_HAS_DUPLICATES: + this.textFilterTarget = TEXT_FILTER_TARGET_DUPLICATES + this.textFilterModifier = + rule.value == 'false' || rule.value == '0' + ? TEXT_FILTER_MODIFIER_DOES_NOT_HAVE_DUPLICATES + : TEXT_FILTER_MODIFIER_HAS_DUPLICATES + break case FILTER_FULLTEXT_QUERY: let allQueryArgs = rule.value.split(',') let textQueryArgs = [] @@ -800,6 +839,14 @@ export class FilterEditorComponent value: this._textFilter.trim(), }) } + if (this.textFilterTarget == TEXT_FILTER_TARGET_DUPLICATES) { + filterRules.push({ + rule_type: FILTER_HAS_DUPLICATES, + value: ( + this.textFilterModifier == TEXT_FILTER_MODIFIER_HAS_DUPLICATES + ).toString(), + }) + } if (this._textFilter && this.textFilterTarget == TEXT_FILTER_TARGET_TITLE) { filterRules.push({ rule_type: FILTER_SIMPLE_TITLE, @@ -1163,7 +1210,7 @@ export class FilterEditorComponent } get textFilter() { - return this.textFilterModifierIsNull ? '' : this._textFilter + return this.textFilterInputDisabled ? '' : this._textFilter } set textFilter(value) { @@ -1363,12 +1410,24 @@ export class FilterEditorComponent this._textFilter = '' } this.textFilterTarget = target + if (target == TEXT_FILTER_TARGET_DUPLICATES) { + this._textFilter = '' + this.textFilterModifier = TEXT_FILTER_MODIFIER_HAS_DUPLICATES + } else if ( + [ + TEXT_FILTER_MODIFIER_HAS_DUPLICATES, + TEXT_FILTER_MODIFIER_DOES_NOT_HAVE_DUPLICATES, + ].includes(this.textFilterModifier) + ) { + this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS + } this.textFilterInput.nativeElement.focus() this.updateRules() } textFilterModifierChange() { if ( + this.textFilterTarget == TEXT_FILTER_TARGET_DUPLICATES || this.textFilterModifierIsNull || ([ TEXT_FILTER_MODIFIER_EQUALS,