mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-03-31 13:35:08 -05:00
Merge branch 'dev' into beta
This commit is contained in:
commit
1d9482acc3
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@ import {
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
@ -44,6 +45,11 @@ const nullItem = {
|
||||
name: 'Not assigned',
|
||||
}
|
||||
|
||||
const negativeNullItem = {
|
||||
id: NEGATIVE_NULL_FILTER_VALUE,
|
||||
name: 'Not assigned',
|
||||
}
|
||||
|
||||
let selectionModel: FilterableDropdownSelectionModel
|
||||
|
||||
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
|
||||
@ -64,6 +70,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
hotkeyService = TestBed.inject(HotKeyService)
|
||||
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
component.selectionModel = new FilterableDropdownSelectionModel()
|
||||
selectionModel = new FilterableDropdownSelectionModel()
|
||||
})
|
||||
|
||||
@ -74,7 +81,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
expect(selectionModel.getSelectedItems()).toHaveLength(1)
|
||||
@ -96,7 +103,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should emit change when items selected', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@ -110,11 +117,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
expect(newModel.getSelectedItems()).toEqual([])
|
||||
|
||||
expect(component.items).toEqual([nullItem, ...items])
|
||||
expect(component.selectionModel.items).toEqual([nullItem, ...items])
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@ -124,7 +131,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@ -139,8 +146,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should exclude items when excluded and not editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
component.excludeClicked(items[0].id)
|
||||
@ -149,8 +156,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should toggle when items excluded and editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
@ -160,8 +167,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should hide count for item if adding will increase size of set', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.hideCount(items[0])).toBeFalsy()
|
||||
selectionModel.logicalOperator = LogicalOperator.Or
|
||||
@ -170,7 +177,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should enforce single select when editing', () => {
|
||||
component.editing = true
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@ -182,11 +189,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support manyToOne selecting', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
selectionModel.manyToOne = false
|
||||
component.selectionModel = selectionModel
|
||||
component.manyToOne = true
|
||||
expect(component.manyToOne).toBeTruthy()
|
||||
component.selectionModel.manyToOne = true
|
||||
expect(component.selectionModel.manyToOne).toBeTruthy()
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
|
||||
@ -197,12 +204,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should dynamically enable / disable modifier toggle', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
selectionModel.toggle(null)
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
selectionModel.toggle(items[0].id)
|
||||
selectionModel.toggle(items[1].id)
|
||||
@ -210,7 +215,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should apply changes and close when apply button clicked', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
@ -232,7 +237,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should apply on close if enabled', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.applyOnClose = true
|
||||
@ -250,7 +255,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@ -277,7 +282,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
fixture.nativeElement
|
||||
@ -297,7 +302,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
let applyResult: ChangedItems
|
||||
@ -319,7 +324,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@ -364,7 +369,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@ -400,7 +405,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after click', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@ -425,9 +430,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle logical operator', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
component.selectionModel = selectionModel
|
||||
@ -454,7 +459,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle intersection include / exclude', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
@ -483,22 +488,55 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
expect(changedResult.getExcludedItems()).toEqual(items)
|
||||
}))
|
||||
|
||||
it('selection model should sort items by state', () => {
|
||||
component.items = items.concat([{ id: null, name: 'Null B' }])
|
||||
it('should update null item selection on toggleIntersection', () => {
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
console.log(component.selectionModel.items[0])
|
||||
component.selectionModel.set(null, ToggleableItemState.Selected)
|
||||
component.selectionModel.intersection = Intersection.Exclude
|
||||
component.selectionModel.toggleIntersection()
|
||||
console.log(component.selectionModel)
|
||||
expect(component.selectionModel.getExcludedItems()).toEqual([
|
||||
negativeNullItem,
|
||||
])
|
||||
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel.toggleIntersection()
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([nullItem])
|
||||
})
|
||||
|
||||
it('selection model should sort items by state', () => {
|
||||
component.selectionModel = selectionModel
|
||||
component.selectionModel.items = items.concat([{ id: 3, name: 'Item3' }])
|
||||
selectionModel.toggle(items[1].id)
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.items.length).toEqual(4)
|
||||
expect(selectionModel.items).toEqual([
|
||||
nullItem,
|
||||
{ id: null, name: 'Null B' },
|
||||
items[1],
|
||||
{ id: 3, name: 'Item3' },
|
||||
items[0],
|
||||
])
|
||||
|
||||
selectionModel.intersection = Intersection.Exclude
|
||||
selectionModel.toggleIntersection()
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.items).toEqual([
|
||||
negativeNullItem,
|
||||
items[1],
|
||||
{ id: 3, name: 'Item3' },
|
||||
items[0],
|
||||
])
|
||||
|
||||
// coverage
|
||||
selectionModel.items = selectionModel.items.reverse()
|
||||
selectionModel.apply()
|
||||
})
|
||||
|
||||
it('selection model should sort items by state and document counts = 0, if set', () => {
|
||||
const tagA = { id: 4, name: 'Tag A' }
|
||||
component.items = items.concat([tagA])
|
||||
component.selectionModel.items = items.concat([tagA])
|
||||
component.selectionModel = selectionModel
|
||||
component.documentCounts = [
|
||||
{ id: 1, document_count: 0 }, // Tag1
|
||||
@ -529,7 +567,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should set support create, keep open model and call createRef method', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.selectionModel = selectionModel
|
||||
fixture.nativeElement
|
||||
@ -549,7 +587,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.createRef = jest.fn()
|
||||
@ -569,7 +607,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
const id = 1
|
||||
const state = ToggleableItemState.Selected
|
||||
component.selectionModel = selectionModel
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel.singleSelect = true
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel['temporarySelectionStates'].set(id, state)
|
||||
@ -596,7 +634,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support shortcut keys', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.shortcutKey = 't'
|
||||
fixture.detectChanges()
|
||||
@ -606,7 +644,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support an extra button and not apply changes when clicked', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.extraButtonTitle = 'Extra'
|
||||
component.selectionModel = selectionModel
|
||||
|
@ -12,6 +12,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject, filter, takeUntil } from 'rxjs'
|
||||
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
|
||||
import { MatchingModel } from 'src/app/data/matching-model'
|
||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||
@ -61,15 +62,56 @@ export class FilterableDropdownSelectionModel {
|
||||
}
|
||||
|
||||
set items(items: MatchingModel[]) {
|
||||
this._items = items
|
||||
this.sortItems()
|
||||
if (items) {
|
||||
this._items = Array.from(items)
|
||||
this.sortItems()
|
||||
this.setNullItem()
|
||||
}
|
||||
}
|
||||
|
||||
private setNullItem() {
|
||||
if (this.manyToOne && this.logicalOperator === LogicalOperator.Or) {
|
||||
if (this._items[0]?.id === null) {
|
||||
this._items.shift()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const item = {
|
||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||
id:
|
||||
this.manyToOne || this.intersection === Intersection.Include
|
||||
? null
|
||||
: NEGATIVE_NULL_FILTER_VALUE,
|
||||
}
|
||||
|
||||
if (
|
||||
this._items[0]?.id === null ||
|
||||
this._items[0]?.id === NEGATIVE_NULL_FILTER_VALUE
|
||||
) {
|
||||
this._items[0] = item
|
||||
} else if (this._items) {
|
||||
this._items.unshift(item)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(manyToOne: boolean = false) {
|
||||
this.manyToOne = manyToOne
|
||||
}
|
||||
|
||||
private sortItems() {
|
||||
this._items.sort((a, b) => {
|
||||
if (a.id == null && b.id != null) {
|
||||
if (
|
||||
(a.id == null && b.id != null) ||
|
||||
(a.id == NEGATIVE_NULL_FILTER_VALUE &&
|
||||
b.id != NEGATIVE_NULL_FILTER_VALUE)
|
||||
) {
|
||||
return -1
|
||||
} else if (a.id != null && b.id == null) {
|
||||
} else if (
|
||||
(a.id != null && b.id == null) ||
|
||||
(a.id != NEGATIVE_NULL_FILTER_VALUE &&
|
||||
b.id == NEGATIVE_NULL_FILTER_VALUE)
|
||||
) {
|
||||
return 1
|
||||
} else if (
|
||||
this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
|
||||
@ -230,6 +272,7 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
set logicalOperator(operator: LogicalOperator) {
|
||||
this.temporaryLogicalOperator = operator
|
||||
this.setNullItem()
|
||||
}
|
||||
|
||||
toggleOperator() {
|
||||
@ -242,6 +285,7 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
set intersection(intersection: Intersection) {
|
||||
this.temporaryIntersection = intersection
|
||||
this.setNullItem()
|
||||
}
|
||||
|
||||
toggleIntersection() {
|
||||
@ -250,9 +294,20 @@ export class FilterableDropdownSelectionModel {
|
||||
this.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded
|
||||
|
||||
this.temporarySelectionStates.forEach((state, key) => {
|
||||
this.temporarySelectionStates.set(key, newState)
|
||||
if (key === null && this.intersection === Intersection.Exclude) {
|
||||
this.temporarySelectionStates.set(NEGATIVE_NULL_FILTER_VALUE, newState)
|
||||
} else if (
|
||||
key === NEGATIVE_NULL_FILTER_VALUE &&
|
||||
this.intersection === Intersection.Include
|
||||
) {
|
||||
this.temporarySelectionStates.set(null, newState)
|
||||
} else {
|
||||
this.temporarySelectionStates.set(key, newState)
|
||||
}
|
||||
})
|
||||
|
||||
this.changed.next(this)
|
||||
}
|
||||
|
||||
@ -274,6 +329,7 @@ export class FilterableDropdownSelectionModel {
|
||||
this.temporarySelectionStates.clear()
|
||||
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
|
||||
this.temporaryIntersection = this._intersection = Intersection.Include
|
||||
this.setNullItem()
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
@ -305,8 +361,10 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
isNoneSelected() {
|
||||
return (
|
||||
this.selectionSize() == 1 &&
|
||||
this.get(null) == ToggleableItemState.Selected
|
||||
(this.selectionSize() == 1 &&
|
||||
this.get(null) == ToggleableItemState.Selected) ||
|
||||
(this.intersection == Intersection.Exclude &&
|
||||
this.get(NEGATIVE_NULL_FILTER_VALUE) == ToggleableItemState.Excluded)
|
||||
)
|
||||
}
|
||||
|
||||
@ -384,25 +442,13 @@ export class FilterableDropdownComponent
|
||||
|
||||
filterText: string
|
||||
|
||||
@Input()
|
||||
set items(items: MatchingModel[]) {
|
||||
if (items) {
|
||||
this._selectionModel.items = Array.from(items)
|
||||
this._selectionModel.items.unshift({
|
||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||
id: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
_selectionModel: FilterableDropdownSelectionModel
|
||||
|
||||
get items(): MatchingModel[] {
|
||||
return this._selectionModel.items
|
||||
}
|
||||
|
||||
_selectionModel: FilterableDropdownSelectionModel =
|
||||
new FilterableDropdownSelectionModel()
|
||||
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
set selectionModel(model: FilterableDropdownSelectionModel) {
|
||||
if (this.selectionModel) {
|
||||
this.selectionModel.changed.complete()
|
||||
@ -423,11 +469,6 @@ export class FilterableDropdownComponent
|
||||
@Output()
|
||||
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
||||
|
||||
@Input()
|
||||
set manyToOne(manyToOne: boolean) {
|
||||
this.selectionModel.manyToOne = manyToOne
|
||||
}
|
||||
|
||||
get manyToOne() {
|
||||
return this.selectionModel.manyToOne
|
||||
}
|
||||
@ -484,7 +525,7 @@ export class FilterableDropdownComponent
|
||||
return this.manyToOne
|
||||
? this.selectionModel.selectionSize() > 1 &&
|
||||
this.selectionModel.getExcludedItems().length == 0
|
||||
: !this.selectionModel.isNoneSelected()
|
||||
: true
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
|
@ -20,10 +20,8 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
|
||||
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[manyToOne]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createTag.bind(this)"
|
||||
(opened)="openTagsDropdown()"
|
||||
@ -36,7 +34,6 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@ -51,7 +48,6 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@ -66,7 +62,6 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@ -81,10 +76,8 @@
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
|
||||
<pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
|
||||
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
|
||||
[items]="customFields"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[manyToOne]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createCustomField.bind(this)"
|
||||
(opened)="openCustomFieldsDropdown()"
|
||||
|
@ -1150,10 +1150,10 @@ describe('BulkEditorComponent', () => {
|
||||
|
||||
it('should not attempt to retrieve objects if user does not have permissions', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
expect(component.tags).toBeUndefined()
|
||||
expect(component.correspondents).toBeUndefined()
|
||||
expect(component.documentTypes).toBeUndefined()
|
||||
expect(component.storagePaths).toBeUndefined()
|
||||
expect(component.tagSelectionModel.items.length).toEqual(0)
|
||||
expect(component.correspondentSelectionModel.items.length).toEqual(0)
|
||||
expect(component.documentTypeSelectionModel.items.length).toEqual(0)
|
||||
expect(component.storagePathsSelectionModel.items.length).toEqual(0)
|
||||
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/correspondents/`
|
||||
@ -1204,7 +1204,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(tagListAllSpy).toHaveBeenCalled()
|
||||
|
||||
expect(tagSelectionModelToggleSpy).toHaveBeenCalledWith(newTag.id)
|
||||
expect(component.tags).toEqual(tags.results)
|
||||
expect(component.tagSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(tags.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new correspondent', () => {
|
||||
@ -1251,7 +1253,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(correspondentSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newCorrespondent.id
|
||||
)
|
||||
expect(component.correspondents).toEqual(correspondents.results)
|
||||
expect(component.correspondentSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(correspondents.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new document type', () => {
|
||||
@ -1295,7 +1299,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(documentTypeSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newDocumentType.id
|
||||
)
|
||||
expect(component.documentTypes).toEqual(documentTypes.results)
|
||||
expect(component.documentTypeSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(documentTypes.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new storage path', () => {
|
||||
@ -1339,7 +1345,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(storagePathsSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newStoragePath.id
|
||||
)
|
||||
expect(component.storagePaths).toEqual(storagePaths.results)
|
||||
expect(component.storagePathsSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(storagePaths.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new custom field', () => {
|
||||
@ -1391,7 +1399,9 @@ describe('BulkEditorComponent', () => {
|
||||
expect(customFieldsSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newCustomField.id
|
||||
)
|
||||
expect(component.customFields).toEqual(customFields.results)
|
||||
expect(component.customFieldsSelectionModel.items).toEqual(
|
||||
[{ id: null, name: 'Not assigned' }].concat(customFields.results as any)
|
||||
)
|
||||
})
|
||||
|
||||
it('should open the bulk edit custom field values dialog with correct parameters', () => {
|
||||
@ -1416,17 +1426,17 @@ describe('BulkEditorComponent', () => {
|
||||
const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const listReloadSpy = jest.spyOn(documentListViewService, 'reload')
|
||||
|
||||
component.customFields = [
|
||||
component.customFieldsSelectionModel.items = [
|
||||
{ id: 1, name: 'Custom Field 1', data_type: CustomFieldDataType.String },
|
||||
{ id: 2, name: 'Custom Field 2', data_type: CustomFieldDataType.String },
|
||||
]
|
||||
] as any
|
||||
|
||||
component.setCustomFieldValues({
|
||||
itemsToAdd: [{ id: 1 }, { id: 2 }],
|
||||
itemsToRemove: [1],
|
||||
} as any)
|
||||
|
||||
expect(modal.componentInstance.customFields).toEqual(component.customFields)
|
||||
expect(modal.componentInstance.customFields.length).toEqual(2)
|
||||
expect(modal.componentInstance.fieldsToAddIds).toEqual([1, 2])
|
||||
expect(modal.componentInstance.documents).toEqual([3, 4])
|
||||
|
||||
|
@ -14,12 +14,8 @@ import { saveAs } from 'file-saver'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { first, map, Subject, switchMap, takeUntil } from 'rxjs'
|
||||
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { MatchingModel } from 'src/app/data/matching-model'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
@ -75,17 +71,11 @@ export class BulkEditorComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
tags: Tag[]
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
storagePaths: StoragePath[]
|
||||
customFields: CustomField[]
|
||||
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel(true)
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
||||
customFieldsSelectionModel = new FilterableDropdownSelectionModel()
|
||||
customFieldsSelectionModel = new FilterableDropdownSelectionModel(true)
|
||||
tagDocumentCounts: SelectionDataItem[]
|
||||
correspondentDocumentCounts: SelectionDataItem[]
|
||||
documentTypeDocumentCounts: SelectionDataItem[]
|
||||
@ -176,7 +166,7 @@ export class BulkEditorComponent
|
||||
this.tagService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.tags = result.results))
|
||||
.subscribe((result) => (this.tagSelectionModel.items = result.results))
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@ -187,7 +177,9 @@ export class BulkEditorComponent
|
||||
this.correspondentService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.correspondentSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@ -198,7 +190,9 @@ export class BulkEditorComponent
|
||||
this.documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.documentTypeSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@ -209,7 +203,9 @@ export class BulkEditorComponent
|
||||
this.storagePathService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.storagePathsSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
@ -220,7 +216,9 @@ export class BulkEditorComponent
|
||||
this.customFieldService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.customFields = result.results))
|
||||
.subscribe(
|
||||
(result) => (this.customFieldsSelectionModel.items = result.results)
|
||||
)
|
||||
}
|
||||
|
||||
this.downloadForm
|
||||
@ -651,7 +649,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newTag, tags }) => {
|
||||
this.tags = tags.results
|
||||
this.tagSelectionModel.items = tags.results
|
||||
this.tagSelectionModel.toggle(newTag.id)
|
||||
})
|
||||
}
|
||||
@ -674,7 +672,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newCorrespondent, correspondents }) => {
|
||||
this.correspondents = correspondents.results
|
||||
this.correspondentSelectionModel.items = correspondents.results
|
||||
this.correspondentSelectionModel.toggle(newCorrespondent.id)
|
||||
})
|
||||
}
|
||||
@ -695,7 +693,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newDocumentType, documentTypes }) => {
|
||||
this.documentTypes = documentTypes.results
|
||||
this.documentTypeSelectionModel.items = documentTypes.results
|
||||
this.documentTypeSelectionModel.toggle(newDocumentType.id)
|
||||
})
|
||||
}
|
||||
@ -716,7 +714,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newStoragePath, storagePaths }) => {
|
||||
this.storagePaths = storagePaths.results
|
||||
this.storagePathsSelectionModel.items = storagePaths.results
|
||||
this.storagePathsSelectionModel.toggle(newStoragePath.id)
|
||||
})
|
||||
}
|
||||
@ -737,7 +735,7 @@ export class BulkEditorComponent
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newCustomField, customFields }) => {
|
||||
this.customFields = customFields.results
|
||||
this.customFieldsSelectionModel.items = customFields.results
|
||||
this.customFieldsSelectionModel.toggle(newCustomField.id)
|
||||
})
|
||||
}
|
||||
@ -875,7 +873,9 @@ export class BulkEditorComponent
|
||||
})
|
||||
const dialog =
|
||||
modal.componentInstance as CustomFieldsBulkEditDialogComponent
|
||||
dialog.customFields = this.customFields
|
||||
dialog.customFields = (
|
||||
this.customFieldsSelectionModel.items as CustomField[]
|
||||
).filter((f) => f.id !== null)
|
||||
dialog.fieldsToAddIds = changedCustomFields.itemsToAdd.map(
|
||||
(item) => item.id
|
||||
)
|
||||
|
@ -35,11 +35,9 @@
|
||||
<div class="col-auto">
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tags.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tagSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[manyToOne]="true"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onTagsDropdownOpen()"
|
||||
@ -48,10 +46,9 @@
|
||||
[disabled]="disabled"
|
||||
shortcutKey="t"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondents.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondentSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[(selectionModel)]="correspondentSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onCorrespondentDropdownOpen()"
|
||||
@ -60,10 +57,9 @@
|
||||
[disabled]="disabled"
|
||||
shortcutKey="y"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypes.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypeSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onDocumentTypeDropdownOpen()"
|
||||
@ -72,10 +68,9 @@
|
||||
[disabled]="disabled"
|
||||
shortcutKey="u"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePaths.length > 0) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePathSelectionModel.items.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[(selectionModel)]="storagePathSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onStoragePathDropdownOpen()"
|
||||
|
@ -69,6 +69,7 @@ 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'
|
||||
@ -671,9 +672,6 @@ describe('FilterEditorComponent', () => {
|
||||
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(() => {
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-11 13:33-0700\n"
|
||||
"PO-Revision-Date: 2025-03-11 20:35\n"
|
||||
"PO-Revision-Date: 2025-03-18 00:32\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Indonesian\n"
|
||||
"Language: id_ID\n"
|
||||
@ -274,51 +274,51 @@ msgstr "Kartu Besar"
|
||||
|
||||
#: documents/models.py:384
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
msgstr "Judul"
|
||||
|
||||
#: documents/models.py:385 documents/models.py:942
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
msgstr "Dibuat"
|
||||
|
||||
#: documents/models.py:386 documents/models.py:941
|
||||
msgid "Added"
|
||||
msgstr ""
|
||||
msgstr "Ditambahkan"
|
||||
|
||||
#: documents/models.py:387
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
msgstr "Label"
|
||||
|
||||
#: documents/models.py:388
|
||||
msgid "Correspondent"
|
||||
msgstr ""
|
||||
msgstr "Koresponden"
|
||||
|
||||
#: documents/models.py:389
|
||||
msgid "Document Type"
|
||||
msgstr ""
|
||||
msgstr "Tipe Dokumen"
|
||||
|
||||
#: documents/models.py:390
|
||||
msgid "Storage Path"
|
||||
msgstr ""
|
||||
msgstr "Lokasi Penyimpanan"
|
||||
|
||||
#: documents/models.py:391
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
msgstr "Catatan"
|
||||
|
||||
#: documents/models.py:392
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
msgstr "Pemilik"
|
||||
|
||||
#: documents/models.py:393
|
||||
msgid "Shared"
|
||||
msgstr ""
|
||||
msgstr "Dibagikan"
|
||||
|
||||
#: documents/models.py:394
|
||||
msgid "ASN"
|
||||
msgstr ""
|
||||
msgstr "ASN"
|
||||
|
||||
#: documents/models.py:395
|
||||
msgid "Pages"
|
||||
msgstr ""
|
||||
msgstr "Halaman"
|
||||
|
||||
#: documents/models.py:401
|
||||
msgid "show on dashboard"
|
||||
@ -526,7 +526,7 @@ msgstr "tidak memiliki area khusus"
|
||||
|
||||
#: documents/models.py:489
|
||||
msgid "custom fields query"
|
||||
msgstr ""
|
||||
msgstr "kueri bidang khusus"
|
||||
|
||||
#: documents/models.py:490
|
||||
msgid "created to"
|
||||
@ -566,15 +566,15 @@ msgstr "saring aturan"
|
||||
|
||||
#: documents/models.py:534
|
||||
msgid "Auto Task"
|
||||
msgstr ""
|
||||
msgstr "Tugas Otomatis"
|
||||
|
||||
#: documents/models.py:535
|
||||
msgid "Scheduled Task"
|
||||
msgstr ""
|
||||
msgstr "Tugas Terjadwal"
|
||||
|
||||
#: documents/models.py:536
|
||||
msgid "Manual Task"
|
||||
msgstr ""
|
||||
msgstr "Tugas Manual"
|
||||
|
||||
#: documents/models.py:539
|
||||
msgid "Consume File"
|
||||
@ -666,7 +666,7 @@ msgstr "Data yang dikembalikan dari tugas"
|
||||
|
||||
#: documents/models.py:614
|
||||
msgid "Task Type"
|
||||
msgstr ""
|
||||
msgstr "Tipe Tugas"
|
||||
|
||||
#: documents/models.py:615
|
||||
msgid "The type of task that was run"
|
||||
@ -746,7 +746,7 @@ msgstr "Tautan Dokumen"
|
||||
|
||||
#: documents/models.py:736
|
||||
msgid "Select"
|
||||
msgstr ""
|
||||
msgstr "Pilih"
|
||||
|
||||
#: documents/models.py:748
|
||||
msgid "data type"
|
||||
@ -754,7 +754,7 @@ msgstr "jenis data"
|
||||
|
||||
#: documents/models.py:755
|
||||
msgid "extra data"
|
||||
msgstr ""
|
||||
msgstr "data ekstra"
|
||||
|
||||
#: documents/models.py:759
|
||||
msgid "Extra data for the custom field, such as select options"
|
||||
@ -790,7 +790,7 @@ msgstr "Dokumen diperbarui"
|
||||
|
||||
#: documents/models.py:932
|
||||
msgid "Scheduled"
|
||||
msgstr ""
|
||||
msgstr "Dijadwalkan"
|
||||
|
||||
#: documents/models.py:935
|
||||
msgid "Consume Folder"
|
||||
@ -806,15 +806,15 @@ msgstr "Pengambilan Surat"
|
||||
|
||||
#: documents/models.py:938
|
||||
msgid "Web UI"
|
||||
msgstr ""
|
||||
msgstr "Antarmuka Web"
|
||||
|
||||
#: documents/models.py:943
|
||||
msgid "Modified"
|
||||
msgstr ""
|
||||
msgstr "Dimodifikasi"
|
||||
|
||||
#: documents/models.py:944
|
||||
msgid "Custom Field"
|
||||
msgstr ""
|
||||
msgstr "Kolom Khusus"
|
||||
|
||||
#: documents/models.py:947
|
||||
msgid "Workflow Trigger Type"
|
||||
@ -982,7 +982,7 @@ msgstr "Surel"
|
||||
|
||||
#: documents/models.py:1176
|
||||
msgid "Webhook"
|
||||
msgstr ""
|
||||
msgstr "Webhook"
|
||||
|
||||
#: documents/models.py:1180
|
||||
msgid "Workflow Action Type"
|
||||
@ -1114,11 +1114,11 @@ msgstr "hapus semua kolom khusus"
|
||||
|
||||
#: documents/models.py:1395
|
||||
msgid "email"
|
||||
msgstr ""
|
||||
msgstr "surel"
|
||||
|
||||
#: documents/models.py:1404
|
||||
msgid "webhook"
|
||||
msgstr ""
|
||||
msgstr "webhook"
|
||||
|
||||
#: documents/models.py:1408
|
||||
msgid "workflow action"
|
||||
@ -1146,7 +1146,7 @@ msgstr "diaktifkan"
|
||||
|
||||
#: documents/models.py:1445
|
||||
msgid "workflow"
|
||||
msgstr ""
|
||||
msgstr "alur kerja"
|
||||
|
||||
#: documents/models.py:1449
|
||||
msgid "workflow trigger type"
|
||||
@ -1188,15 +1188,15 @@ msgstr ""
|
||||
|
||||
#: documents/templates/account/account_inactive.html:9
|
||||
msgid "Account inactive."
|
||||
msgstr ""
|
||||
msgstr "Akun tidak aktif."
|
||||
|
||||
#: documents/templates/account/account_inactive.html:14
|
||||
msgid "This account is inactive."
|
||||
msgstr ""
|
||||
msgstr "Akun ini tidak aktif."
|
||||
|
||||
#: documents/templates/account/account_inactive.html:16
|
||||
msgid "Return to login"
|
||||
msgstr ""
|
||||
msgstr "Kembali ke halaman masuk"
|
||||
|
||||
#: documents/templates/account/email/base_message.txt:1
|
||||
#, python-format
|
||||
@ -1358,11 +1358,11 @@ msgstr ""
|
||||
|
||||
#: documents/templates/mfa/authenticate.html:17
|
||||
msgid "Code"
|
||||
msgstr ""
|
||||
msgstr "Kode"
|
||||
|
||||
#: documents/templates/mfa/authenticate.html:24
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
msgstr "Batal"
|
||||
|
||||
#: documents/templates/paperless-ngx/base.html:58
|
||||
msgid "Share link was not found."
|
||||
@ -1470,7 +1470,7 @@ msgstr ""
|
||||
|
||||
#: paperless/models.py:62
|
||||
msgid "none"
|
||||
msgstr ""
|
||||
msgstr "tidak ada"
|
||||
|
||||
#: paperless/models.py:70
|
||||
msgid "LeaveColorUnchanged"
|
||||
@ -1486,7 +1486,7 @@ msgstr ""
|
||||
|
||||
#: paperless/models.py:73
|
||||
msgid "Gray"
|
||||
msgstr ""
|
||||
msgstr "Abu-abu"
|
||||
|
||||
#: paperless/models.py:74
|
||||
msgid "CMYK"
|
||||
@ -1566,7 +1566,7 @@ msgstr "Arab"
|
||||
|
||||
#: paperless/settings.py:725
|
||||
msgid "Afrikaans"
|
||||
msgstr ""
|
||||
msgstr "Bahasa Afrika"
|
||||
|
||||
#: paperless/settings.py:726
|
||||
msgid "Belarusian"
|
||||
@ -1574,7 +1574,7 @@ msgstr "Belarusia"
|
||||
|
||||
#: paperless/settings.py:727
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
msgstr "Bahasa Bulgaria"
|
||||
|
||||
#: paperless/settings.py:728
|
||||
msgid "Catalan"
|
||||
@ -1622,11 +1622,11 @@ msgstr "Italia"
|
||||
|
||||
#: paperless/settings.py:739
|
||||
msgid "Japanese"
|
||||
msgstr ""
|
||||
msgstr "Bahasa Jepang"
|
||||
|
||||
#: paperless/settings.py:740
|
||||
msgid "Korean"
|
||||
msgstr ""
|
||||
msgstr "Bahasa Korea"
|
||||
|
||||
#: paperless/settings.py:741
|
||||
msgid "Luxembourgish"
|
||||
|
@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-11 13:33-0700\n"
|
||||
"PO-Revision-Date: 2025-03-11 20:35\n"
|
||||
"PO-Revision-Date: 2025-03-16 12:11\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Vietnamese\n"
|
||||
"Language: vi_VN\n"
|
||||
|
Loading…
x
Reference in New Issue
Block a user