@@ -34,7 +42,7 @@
-
+
Click again to exclude items.
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
index b2352f869..12de82693 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
@@ -18,12 +18,25 @@ export interface ChangedItems {
itemsToRemove: MatchingModel[]
}
+export enum LogicalOperator {
+ And = 'and',
+ Or = 'or',
+}
+
+export enum Intersection {
+ Include = 'include',
+ Exclude = 'exclude',
+}
+
export class FilterableDropdownSelectionModel {
changed = new Subject
()
- multiple = false
- private _logicalOperator = 'and'
- temporaryLogicalOperator = this._logicalOperator
+ manyToOne = false
+ singleSelect = false
+ private _logicalOperator: LogicalOperator = LogicalOperator.And
+ temporaryLogicalOperator: LogicalOperator = this._logicalOperator
+ private _intersection: Intersection = Intersection.Include
+ temporaryIntersection: Intersection = this._intersection
items: MatchingModel[] = []
@@ -86,7 +99,30 @@ export class FilterableDropdownSelectionModel {
(state != ToggleableItemState.Selected &&
state != ToggleableItemState.Excluded)
) {
- this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
+ if (this.manyToOne || this.singleSelect) {
+ this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
+
+ if (this.singleSelect) {
+ for (let key of this.temporarySelectionStates.keys()) {
+ if (key != id) {
+ this.temporarySelectionStates.delete(key)
+ }
+ }
+ }
+ } else {
+ let newState =
+ this.intersection == Intersection.Include
+ ? ToggleableItemState.Selected
+ : ToggleableItemState.Excluded
+ if (!id) newState = ToggleableItemState.Selected
+ if (
+ state == ToggleableItemState.Excluded &&
+ this.intersection == Intersection.Exclude
+ ) {
+ newState = ToggleableItemState.NotSelected
+ }
+ this.temporarySelectionStates.set(id, newState)
+ }
} else if (
state == ToggleableItemState.Selected ||
state == ToggleableItemState.Excluded
@@ -94,14 +130,6 @@ export class FilterableDropdownSelectionModel {
this.temporarySelectionStates.delete(id)
}
- if (!this.multiple) {
- for (let key of this.temporarySelectionStates.keys()) {
- if (key != id) {
- this.temporarySelectionStates.delete(key)
- }
- }
- }
-
if (!id) {
for (let key of this.temporarySelectionStates.keys()) {
if (key) {
@@ -119,19 +147,36 @@ export class FilterableDropdownSelectionModel {
exclude(id: number, fireEvent: boolean = true) {
let state = this.temporarySelectionStates.get(id)
- if (state == null || state != ToggleableItemState.Excluded) {
- this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
- this.temporaryLogicalOperator = this._logicalOperator = 'and'
- } else if (state == ToggleableItemState.Excluded) {
- this.temporarySelectionStates.delete(id)
- }
+ if (id && (state == null || state != ToggleableItemState.Excluded)) {
+ this.temporaryLogicalOperator = this._logicalOperator = this.manyToOne
+ ? LogicalOperator.And
+ : LogicalOperator.Or
- if (!this.multiple) {
- for (let key of this.temporarySelectionStates.keys()) {
- if (key != id) {
- this.temporarySelectionStates.delete(key)
+ if (this.manyToOne || this.singleSelect) {
+ this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
+
+ if (this.singleSelect) {
+ for (let key of this.temporarySelectionStates.keys()) {
+ if (key != id) {
+ this.temporarySelectionStates.delete(key)
+ }
+ }
}
+ } else {
+ let newState =
+ this.intersection == Intersection.Include
+ ? ToggleableItemState.Selected
+ : ToggleableItemState.Excluded
+ if (
+ state == ToggleableItemState.Selected &&
+ this.intersection == Intersection.Include
+ ) {
+ newState = ToggleableItemState.NotSelected
+ }
+ this.temporarySelectionStates.set(id, newState)
}
+ } else if (!id || state == ToggleableItemState.Excluded) {
+ this.temporarySelectionStates.delete(id)
}
if (fireEvent) {
@@ -143,11 +188,11 @@ export class FilterableDropdownSelectionModel {
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
}
- get logicalOperator(): string {
+ get logicalOperator(): LogicalOperator {
return this.temporaryLogicalOperator
}
- set logicalOperator(operator: string) {
+ set logicalOperator(operator: LogicalOperator) {
this.temporaryLogicalOperator = operator
}
@@ -155,6 +200,26 @@ export class FilterableDropdownSelectionModel {
this.changed.next(this)
}
+ get intersection(): Intersection {
+ return this.temporaryIntersection
+ }
+
+ set intersection(intersection: Intersection) {
+ this.temporaryIntersection = intersection
+ }
+
+ toggleIntersection() {
+ if (this.temporarySelectionStates.size === 0) return
+ let newState =
+ this.intersection == Intersection.Include
+ ? ToggleableItemState.Selected
+ : ToggleableItemState.Excluded
+ this.temporarySelectionStates.forEach((state, key) => {
+ this.temporarySelectionStates.set(key, newState)
+ })
+ this.changed.next(this)
+ }
+
get(id: number) {
return (
this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
@@ -171,7 +236,8 @@ export class FilterableDropdownSelectionModel {
clear(fireEvent = true) {
this.temporarySelectionStates.clear()
- this.temporaryLogicalOperator = this._logicalOperator = 'and'
+ this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
+ this.temporaryIntersection = this._intersection = Intersection.Include
if (fireEvent) {
this.changed.next(this)
}
@@ -194,6 +260,8 @@ export class FilterableDropdownSelectionModel {
return true
} else if (this.temporaryLogicalOperator !== this._logicalOperator) {
return true
+ } else if (this.temporaryIntersection !== this._intersection) {
+ return true
} else {
return false
}
@@ -217,13 +285,18 @@ export class FilterableDropdownSelectionModel {
this.selectionStates.set(key, value)
})
this._logicalOperator = this.temporaryLogicalOperator
+ this._intersection = this.temporaryIntersection
}
- reset() {
+ reset(complete: boolean = false) {
this.temporarySelectionStates.clear()
- this.selectionStates.forEach((value, key) => {
- this.temporarySelectionStates.set(key, value)
- })
+ if (complete) {
+ this.selectionStates.clear()
+ } else {
+ this.selectionStates.forEach((value, key) => {
+ this.temporarySelectionStates.set(key, value)
+ })
+ }
}
diff(): ChangedItems {
@@ -269,14 +342,16 @@ export class FilterableDropdownComponent {
return this._selectionModel.items
}
- _selectionModel = new FilterableDropdownSelectionModel()
+ _selectionModel: FilterableDropdownSelectionModel =
+ new FilterableDropdownSelectionModel()
@Input()
set selectionModel(model: FilterableDropdownSelectionModel) {
if (this.selectionModel) {
this.selectionModel.changed.complete()
model.items = this.selectionModel.items
- model.multiple = this.selectionModel.multiple
+ model.manyToOne = this.selectionModel.manyToOne
+ model.singleSelect = this.editing && !this.selectionModel.manyToOne
}
model.changed.subscribe((updatedModel) => {
this.selectionModelChange.next(updatedModel)
@@ -292,12 +367,12 @@ export class FilterableDropdownComponent {
selectionModelChange = new EventEmitter()
@Input()
- set multiple(value: boolean) {
- this.selectionModel.multiple = value
+ set manyToOne(manyToOne: boolean) {
+ this.selectionModel.manyToOne = manyToOne
}
- get multiple() {
- return this.selectionModel.multiple
+ get manyToOne() {
+ return this.selectionModel.manyToOne
}
@Input()
@@ -327,16 +402,20 @@ export class FilterableDropdownComponent {
@Output()
opened = new EventEmitter()
- get operatorToggleEnabled(): boolean {
- return (
- this.selectionModel.selectionSize() > 1 &&
- this.selectionModel.getExcludedItems().length == 0
- )
+ get modifierToggleEnabled(): boolean {
+ return this.manyToOne
+ ? this.selectionModel.selectionSize() > 1 &&
+ this.selectionModel.getExcludedItems().length == 0
+ : !this.selectionModel.isNoneSelected()
}
@Input()
documentCounts: SelectionDataItem[]
+ get name(): string {
+ return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
+ }
+
getUpdatedDocumentCount(id: number) {
if (this.documentCounts) {
return this.documentCounts.find((c) => c.id === id)?.document_count
@@ -346,7 +425,6 @@ export class FilterableDropdownComponent {
modelIsDirty: boolean = false
constructor(private filterPipe: FilterPipe) {
- this.selectionModel = new FilterableDropdownSelectionModel()
this.selectionModelChange.subscribe((updatedModel) => {
this.modelIsDirty = updatedModel.isDirty()
})
@@ -400,7 +478,7 @@ export class FilterableDropdownComponent {
}
reset() {
- this.selectionModel.reset()
+ this.selectionModel.reset(true)
this.selectionModelChange.emit(this.selectionModel)
}
}
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
index b6cc92e40..dc60bceb9 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -30,7 +30,7 @@
[items]="tags"
[disabled]="!userCanEditAll"
[editing]="true"
- [multiple]="true"
+ [manyToOne]="true"
[applyOnClose]="applyOnClose"
(opened)="openTagsDropdown()"
[(selectionModel)]="tagSelectionModel"
diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html
index 081c013bb..25a75c1b6 100644
--- a/src-ui/src/app/components/document-list/document-list.component.html
+++ b/src-ui/src/app/components/document-list/document-list.component.html
@@ -14,19 +14,19 @@