Compare commits

..

4 Commits

Author SHA1 Message Date
Trenton H
12dc3c57b2 Set Content-Length directly, Django isn't guessing it 2025-03-14 09:45:23 -07:00
Trenton H
a173f05d89 Account for the streaming nature 2025-03-14 08:57:38 -07:00
Trenton H
979af82989 Use right args and call 2025-03-14 08:57:38 -07:00
Trenton H
1b4e44d965 Investigate using more precise response 2025-03-14 08:57:37 -07:00
103 changed files with 59443 additions and 90209 deletions

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@
</svg>
<div class="ms-2 ms-md-3 d-inline-block" [class.d-md-none]="slimSidebarEnabled">
@if (customAppTitle?.length) {
<div class="d-flex flex-column align-items-start custom-title">
<div class="d-flex flex-column align-items-start">
<span class="title">{{customAppTitle}}</span>
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
</div>

View File

@ -244,7 +244,7 @@ main {
}
}
@media screen and (min-width: 366px) and (max-width: 768px) {
@media screen and (max-width: 768px) {
.navbar-toggler {
// compensate for 2 buttons on the right
margin-right: 45px;
@ -257,13 +257,6 @@ main {
}
}
@media screen and (max-width: 345px) {
.custom-title {
max-width: 110px;
overflow: hidden;
}
}
:host ::ng-deep .dropdown.show .dropdown-toggle,
:host ::ng-deep .dropdown-toggle:hover {
opacity: 0.7;

View File

@ -62,7 +62,6 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
this.emailAddress = ''
this.emailSubject = ''
this.emailMessage = ''
this.close()
this.toastService.showInfo($localize`Email sent`)
},
error: (e) => {

View File

@ -7,7 +7,6 @@ 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,
@ -45,11 +44,6 @@ const nullItem = {
name: 'Not assigned',
}
const negativeNullItem = {
id: NEGATIVE_NULL_FILTER_VALUE,
name: 'Not assigned',
}
let selectionModel: FilterableDropdownSelectionModel
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
@ -70,7 +64,6 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
hotkeyService = TestBed.inject(HotKeyService)
fixture = TestBed.createComponent(FilterableDropdownComponent)
component = fixture.componentInstance
component.selectionModel = new FilterableDropdownSelectionModel()
selectionModel = new FilterableDropdownSelectionModel()
})
@ -81,7 +74,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should support reset', () => {
component.selectionModel.items = items
component.items = items
component.selectionModel = selectionModel
selectionModel.set(items[0].id, ToggleableItemState.Selected)
expect(selectionModel.getSelectedItems()).toHaveLength(1)
@ -103,7 +96,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should emit change when items selected', () => {
component.selectionModel.items = items
component.items = items
component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model))
@ -117,11 +110,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
expect(newModel.getSelectedItems()).toEqual([])
expect(component.selectionModel.items).toEqual([nullItem, ...items])
expect(component.items).toEqual([nullItem, ...items])
})
it('should emit change when items excluded', () => {
component.selectionModel.items = items
component.items = items
component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model))
@ -131,7 +124,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should emit change when items excluded', () => {
component.selectionModel.items = items
component.items = items
component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model))
@ -146,8 +139,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should exclude items when excluded and not editing', () => {
component.selectionModel.items = items
component.selectionModel.manyToOne = true
component.items = items
component.manyToOne = true
component.selectionModel = selectionModel
selectionModel.set(items[0].id, ToggleableItemState.Selected)
component.excludeClicked(items[0].id)
@ -156,8 +149,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should toggle when items excluded and editing', () => {
component.selectionModel.items = items
component.selectionModel.manyToOne = true
component.items = items
component.manyToOne = true
component.editing = true
component.selectionModel = selectionModel
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
@ -167,8 +160,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should hide count for item if adding will increase size of set', () => {
component.selectionModel.items = items
component.selectionModel.manyToOne = true
component.items = items
component.manyToOne = true
component.selectionModel = selectionModel
expect(component.hideCount(items[0])).toBeFalsy()
selectionModel.logicalOperator = LogicalOperator.Or
@ -177,7 +170,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should enforce single select when editing', () => {
component.editing = true
component.selectionModel.items = items
component.items = items
component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model))
@ -189,11 +182,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should support manyToOne selecting', () => {
component.selectionModel.items = items
component.items = items
selectionModel.manyToOne = false
component.selectionModel = selectionModel
component.selectionModel.manyToOne = true
expect(component.selectionModel.manyToOne).toBeTruthy()
component.manyToOne = true
expect(component.manyToOne).toBeTruthy()
let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model))
@ -204,10 +197,12 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should dynamically enable / disable modifier toggle', () => {
component.selectionModel.items = items
component.items = items
component.selectionModel = selectionModel
expect(component.modifierToggleEnabled).toBeTruthy()
component.selectionModel.manyToOne = true
selectionModel.toggle(null)
expect(component.modifierToggleEnabled).toBeFalsy()
component.manyToOne = true
expect(component.modifierToggleEnabled).toBeFalsy()
selectionModel.toggle(items[0].id)
selectionModel.toggle(items[1].id)
@ -215,7 +210,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should apply changes and close when apply button clicked', () => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
component.editing = true
component.selectionModel = selectionModel
@ -237,7 +232,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should apply on close if enabled', () => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
component.editing = true
component.applyOnClose = true
@ -255,7 +250,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
fixture.nativeElement
.querySelector('button')
@ -282,7 +277,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}))
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
expect(component.selectionModel.getSelectedItems()).toEqual([])
fixture.nativeElement
@ -302,7 +297,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}))
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
component.editing = true
let applyResult: ChangedItems
@ -324,7 +319,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}))
it('should support arrow keyboard navigation', fakeAsync(() => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
fixture.nativeElement
.querySelector('button')
@ -369,7 +364,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}))
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
fixture.nativeElement
.querySelector('button')
@ -405,7 +400,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}))
it('should support arrow keyboard navigation after click', fakeAsync(() => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
fixture.nativeElement
.querySelector('button')
@ -430,9 +425,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}))
it('should toggle logical operator', fakeAsync(() => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
component.selectionModel.manyToOne = true
component.manyToOne = true
selectionModel.set(items[0].id, ToggleableItemState.Selected)
selectionModel.set(items[1].id, ToggleableItemState.Selected)
component.selectionModel = selectionModel
@ -459,7 +454,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}))
it('should toggle intersection include / exclude', fakeAsync(() => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
selectionModel.set(items[0].id, ToggleableItemState.Selected)
selectionModel.set(items[1].id, ToggleableItemState.Selected)
@ -488,53 +483,22 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
expect(changedResult.getExcludedItems()).toEqual(items)
}))
it('should update null item selection on toggleIntersection', () => {
component.selectionModel.items = items
component.selectionModel = selectionModel
component.selectionModel.intersection = Intersection.Include
component.selectionModel.set(null, ToggleableItemState.Selected)
component.selectionModel.intersection = Intersection.Exclude
component.selectionModel.toggleIntersection()
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.items = items.concat([{ id: null, name: 'Null B' }])
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.selectionModel.items = items.concat([tagA])
component.items = items.concat([tagA])
component.selectionModel = selectionModel
component.documentCounts = [
{ id: 1, document_count: 0 }, // Tag1
@ -565,7 +529,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should set support create, keep open model and call createRef method', fakeAsync(() => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
component.selectionModel = selectionModel
fixture.nativeElement
@ -585,7 +549,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}))
it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
component.editing = true
component.createRef = jest.fn()
@ -605,7 +569,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
const id = 1
const state = ToggleableItemState.Selected
component.selectionModel = selectionModel
component.selectionModel.manyToOne = true
component.manyToOne = true
component.selectionModel.singleSelect = true
component.selectionModel.intersection = Intersection.Include
component.selectionModel['temporarySelectionStates'].set(id, state)
@ -632,7 +596,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should support shortcut keys', () => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
component.shortcutKey = 't'
fixture.detectChanges()
@ -642,7 +606,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})
it('should support an extra button and not apply changes when clicked', () => {
component.selectionModel.items = items
component.items = items
component.icon = 'tag-fill'
component.extraButtonTitle = 'Extra'
component.selectionModel = selectionModel

View File

@ -12,7 +12,6 @@ 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'
@ -62,56 +61,15 @@ export class FilterableDropdownSelectionModel {
}
set items(items: MatchingModel[]) {
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
this._items = items
this.sortItems()
}
private sortItems() {
this._items.sort((a, b) => {
if (
(a.id == null && b.id != null) ||
(a.id == NEGATIVE_NULL_FILTER_VALUE &&
b.id != NEGATIVE_NULL_FILTER_VALUE)
) {
if (a.id == null && b.id != null) {
return -1
} else if (
(a.id != null && b.id == null) ||
(a.id != NEGATIVE_NULL_FILTER_VALUE &&
b.id == NEGATIVE_NULL_FILTER_VALUE)
) {
} else if (a.id != null && b.id == null) {
return 1
} else if (
this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
@ -272,7 +230,6 @@ export class FilterableDropdownSelectionModel {
set logicalOperator(operator: LogicalOperator) {
this.temporaryLogicalOperator = operator
this.setNullItem()
}
toggleOperator() {
@ -285,7 +242,6 @@ export class FilterableDropdownSelectionModel {
set intersection(intersection: Intersection) {
this.temporaryIntersection = intersection
this.setNullItem()
}
toggleIntersection() {
@ -294,20 +250,9 @@ export class FilterableDropdownSelectionModel {
this.intersection == Intersection.Include
? ToggleableItemState.Selected
: ToggleableItemState.Excluded
this.temporarySelectionStates.forEach((state, key) => {
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.temporarySelectionStates.set(key, newState)
})
this.changed.next(this)
}
@ -329,7 +274,6 @@ 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)
}
@ -361,10 +305,8 @@ export class FilterableDropdownSelectionModel {
isNoneSelected() {
return (
(this.selectionSize() == 1 &&
this.get(null) == ToggleableItemState.Selected) ||
(this.intersection == Intersection.Exclude &&
this.get(NEGATIVE_NULL_FILTER_VALUE) == ToggleableItemState.Excluded)
this.selectionSize() == 1 &&
this.get(null) == ToggleableItemState.Selected
)
}
@ -442,13 +384,25 @@ export class FilterableDropdownComponent
filterText: string
_selectionModel: FilterableDropdownSelectionModel
@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,
})
}
}
get items(): MatchingModel[] {
return this._selectionModel.items
}
@Input({ required: true })
_selectionModel: FilterableDropdownSelectionModel =
new FilterableDropdownSelectionModel()
@Input()
set selectionModel(model: FilterableDropdownSelectionModel) {
if (this.selectionModel) {
this.selectionModel.changed.complete()
@ -469,6 +423,11 @@ export class FilterableDropdownComponent
@Output()
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
@Input()
set manyToOne(manyToOne: boolean) {
this.selectionModel.manyToOne = manyToOne
}
get manyToOne() {
return this.selectionModel.manyToOne
}
@ -525,7 +484,7 @@ export class FilterableDropdownComponent
return this.manyToOne
? this.selectionModel.selectionSize() > 1 &&
this.selectionModel.getExcludedItems().length == 0
: true
: !this.selectionModel.isNoneSelected()
}
get name(): string {

View File

@ -824,18 +824,11 @@ export class DocumentDetailComponent
},
error: (error) => {
this.networkActive = false
const canEdit =
this.permissionsService.currentUserHasObjectPermissions(
PermissionAction.Change,
this.document
)
if (!canEdit) {
// document was 'given away'
this.openDocumentService.setDirty(this.document, false)
if (!this.userCanEdit) {
this.toastService.showInfo(
$localize`Document "${this.document.title}" saved successfully.`
)
this.close()
close && this.close()
} else {
this.error = error.error
this.toastService.showError(

View File

@ -20,8 +20,10 @@
@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()"
@ -34,6 +36,7 @@
@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"
@ -48,6 +51,7 @@
@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"
@ -62,6 +66,7 @@
@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"
@ -76,8 +81,10 @@
@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()"

View File

@ -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.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)
expect(component.tags).toBeUndefined()
expect(component.correspondents).toBeUndefined()
expect(component.documentTypes).toBeUndefined()
expect(component.storagePaths).toBeUndefined()
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
httpTestingController.expectNone(
`${environment.apiBaseUrl}documents/correspondents/`
@ -1204,9 +1204,7 @@ describe('BulkEditorComponent', () => {
expect(tagListAllSpy).toHaveBeenCalled()
expect(tagSelectionModelToggleSpy).toHaveBeenCalledWith(newTag.id)
expect(component.tagSelectionModel.items).toEqual(
[{ id: null, name: 'Not assigned' }].concat(tags.results as any)
)
expect(component.tags).toEqual(tags.results)
})
it('should support create new correspondent', () => {
@ -1253,9 +1251,7 @@ describe('BulkEditorComponent', () => {
expect(correspondentSelectionModelToggleSpy).toHaveBeenCalledWith(
newCorrespondent.id
)
expect(component.correspondentSelectionModel.items).toEqual(
[{ id: null, name: 'Not assigned' }].concat(correspondents.results as any)
)
expect(component.correspondents).toEqual(correspondents.results)
})
it('should support create new document type', () => {
@ -1299,9 +1295,7 @@ describe('BulkEditorComponent', () => {
expect(documentTypeSelectionModelToggleSpy).toHaveBeenCalledWith(
newDocumentType.id
)
expect(component.documentTypeSelectionModel.items).toEqual(
[{ id: null, name: 'Not assigned' }].concat(documentTypes.results as any)
)
expect(component.documentTypes).toEqual(documentTypes.results)
})
it('should support create new storage path', () => {
@ -1345,9 +1339,7 @@ describe('BulkEditorComponent', () => {
expect(storagePathsSelectionModelToggleSpy).toHaveBeenCalledWith(
newStoragePath.id
)
expect(component.storagePathsSelectionModel.items).toEqual(
[{ id: null, name: 'Not assigned' }].concat(storagePaths.results as any)
)
expect(component.storagePaths).toEqual(storagePaths.results)
})
it('should support create new custom field', () => {
@ -1399,9 +1391,7 @@ describe('BulkEditorComponent', () => {
expect(customFieldsSelectionModelToggleSpy).toHaveBeenCalledWith(
newCustomField.id
)
expect(component.customFieldsSelectionModel.items).toEqual(
[{ id: null, name: 'Not assigned' }].concat(customFields.results as any)
)
expect(component.customFields).toEqual(customFields.results)
})
it('should open the bulk edit custom field values dialog with correct parameters', () => {
@ -1426,17 +1416,17 @@ describe('BulkEditorComponent', () => {
const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError')
const listReloadSpy = jest.spyOn(documentListViewService, 'reload')
component.customFieldsSelectionModel.items = [
component.customFields = [
{ 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.length).toEqual(2)
expect(modal.componentInstance.customFields).toEqual(component.customFields)
expect(modal.componentInstance.fieldsToAddIds).toEqual([1, 2])
expect(modal.componentInstance.documents).toEqual([3, 4])

View File

@ -14,8 +14,12 @@ 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'
@ -71,11 +75,17 @@ export class BulkEditorComponent
extends ComponentWithPermissions
implements OnInit, OnDestroy
{
tagSelectionModel = new FilterableDropdownSelectionModel(true)
tags: Tag[]
correspondents: Correspondent[]
documentTypes: DocumentType[]
storagePaths: StoragePath[]
customFields: CustomField[]
tagSelectionModel = new FilterableDropdownSelectionModel()
correspondentSelectionModel = new FilterableDropdownSelectionModel()
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
customFieldsSelectionModel = new FilterableDropdownSelectionModel(true)
customFieldsSelectionModel = new FilterableDropdownSelectionModel()
tagDocumentCounts: SelectionDataItem[]
correspondentDocumentCounts: SelectionDataItem[]
documentTypeDocumentCounts: SelectionDataItem[]
@ -166,7 +176,7 @@ export class BulkEditorComponent
this.tagService
.listAll()
.pipe(first())
.subscribe((result) => (this.tagSelectionModel.items = result.results))
.subscribe((result) => (this.tags = result.results))
}
if (
this.permissionService.currentUserCan(
@ -177,9 +187,7 @@ export class BulkEditorComponent
this.correspondentService
.listAll()
.pipe(first())
.subscribe(
(result) => (this.correspondentSelectionModel.items = result.results)
)
.subscribe((result) => (this.correspondents = result.results))
}
if (
this.permissionService.currentUserCan(
@ -190,9 +198,7 @@ export class BulkEditorComponent
this.documentTypeService
.listAll()
.pipe(first())
.subscribe(
(result) => (this.documentTypeSelectionModel.items = result.results)
)
.subscribe((result) => (this.documentTypes = result.results))
}
if (
this.permissionService.currentUserCan(
@ -203,9 +209,7 @@ export class BulkEditorComponent
this.storagePathService
.listAll()
.pipe(first())
.subscribe(
(result) => (this.storagePathsSelectionModel.items = result.results)
)
.subscribe((result) => (this.storagePaths = result.results))
}
if (
this.permissionService.currentUserCan(
@ -216,9 +220,7 @@ export class BulkEditorComponent
this.customFieldService
.listAll()
.pipe(first())
.subscribe(
(result) => (this.customFieldsSelectionModel.items = result.results)
)
.subscribe((result) => (this.customFields = result.results))
}
this.downloadForm
@ -649,7 +651,7 @@ export class BulkEditorComponent
)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ newTag, tags }) => {
this.tagSelectionModel.items = tags.results
this.tags = tags.results
this.tagSelectionModel.toggle(newTag.id)
})
}
@ -672,7 +674,7 @@ export class BulkEditorComponent
)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ newCorrespondent, correspondents }) => {
this.correspondentSelectionModel.items = correspondents.results
this.correspondents = correspondents.results
this.correspondentSelectionModel.toggle(newCorrespondent.id)
})
}
@ -693,7 +695,7 @@ export class BulkEditorComponent
)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ newDocumentType, documentTypes }) => {
this.documentTypeSelectionModel.items = documentTypes.results
this.documentTypes = documentTypes.results
this.documentTypeSelectionModel.toggle(newDocumentType.id)
})
}
@ -714,7 +716,7 @@ export class BulkEditorComponent
)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ newStoragePath, storagePaths }) => {
this.storagePathsSelectionModel.items = storagePaths.results
this.storagePaths = storagePaths.results
this.storagePathsSelectionModel.toggle(newStoragePath.id)
})
}
@ -735,7 +737,7 @@ export class BulkEditorComponent
)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ newCustomField, customFields }) => {
this.customFieldsSelectionModel.items = customFields.results
this.customFields = customFields.results
this.customFieldsSelectionModel.toggle(newCustomField.id)
})
}
@ -873,9 +875,7 @@ export class BulkEditorComponent
})
const dialog =
modal.componentInstance as CustomFieldsBulkEditDialogComponent
dialog.customFields = (
this.customFieldsSelectionModel.items as CustomField[]
).filter((f) => f.id !== null)
dialog.customFields = this.customFields
dialog.fieldsToAddIds = changedCustomFields.itemsToAdd.map(
(item) => item.id
)

View File

@ -35,9 +35,11 @@
<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) && tagSelectionModel.items.length > 0) {
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tags.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()"
@ -46,9 +48,10 @@
[disabled]="disabled"
shortcutKey="t"></pngx-filterable-dropdown>
}
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondentSelectionModel.items.length > 0) {
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondents.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()"
@ -57,9 +60,10 @@
[disabled]="disabled"
shortcutKey="y"></pngx-filterable-dropdown>
}
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypeSelectionModel.items.length > 0) {
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypes.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()"
@ -68,9 +72,10 @@
[disabled]="disabled"
shortcutKey="u"></pngx-filterable-dropdown>
}
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePathSelectionModel.items.length > 0) {
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePaths.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()"

View File

@ -69,7 +69,6 @@ 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'
@ -672,6 +671,9 @@ describe('FilterEditorComponent', () => {
value: '12',
},
]
expect(component.correspondentSelectionModel.logicalOperator).toEqual(
LogicalOperator.Or
)
expect(component.correspondentSelectionModel.intersection).toEqual(
Intersection.Include
)
@ -679,19 +681,6 @@ 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(() => {
@ -765,6 +754,9 @@ describe('FilterEditorComponent', () => {
value: '22',
},
]
expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
LogicalOperator.Or
)
expect(component.documentTypeSelectionModel.intersection).toEqual(
Intersection.Include
)
@ -772,19 +764,6 @@ 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(() => {
@ -801,6 +780,9 @@ describe('FilterEditorComponent', () => {
value: '23',
},
]
expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
LogicalOperator.Or
)
expect(component.documentTypeSelectionModel.intersection).toEqual(
Intersection.Include
)
@ -855,6 +837,9 @@ describe('FilterEditorComponent', () => {
value: '32',
},
]
expect(component.storagePathSelectionModel.logicalOperator).toEqual(
LogicalOperator.Or
)
expect(component.storagePathSelectionModel.intersection).toEqual(
Intersection.Include
)
@ -862,19 +847,6 @@ 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(() => {
@ -1426,19 +1398,6 @@ 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(() => {
@ -1496,19 +1455,6 @@ 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(() => {
@ -1566,19 +1512,6 @@ 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(() => {

View File

@ -26,12 +26,14 @@ 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,
@ -73,8 +75,9 @@ 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,
@ -248,9 +251,7 @@ export class FilterEditorComponent
case FILTER_HAS_CORRESPONDENT_ANY:
if (rule.value) {
return $localize`Correspondent: ${
this.correspondentSelectionModel.items.find(
(c) => c.id == +rule.value
)?.name
this.correspondents.find((c) => c.id == +rule.value)?.name
}`
} else {
return $localize`Without correspondent`
@ -260,9 +261,7 @@ export class FilterEditorComponent
case FILTER_HAS_DOCUMENT_TYPE_ANY:
if (rule.value) {
return $localize`Document type: ${
this.documentTypeSelectionModel.items.find(
(dt) => dt.id == +rule.value
)?.name
this.documentTypes.find((dt) => dt.id == +rule.value)?.name
}`
} else {
return $localize`Without document type`
@ -272,9 +271,7 @@ export class FilterEditorComponent
case FILTER_HAS_STORAGE_PATH_ANY:
if (rule.value) {
return $localize`Storage path: ${
this.storagePathSelectionModel.items.find(
(sp) => sp.id == +rule.value
)?.name
this.storagePaths.find((sp) => sp.id == +rule.value)?.name
}`
} else {
return $localize`Without storage path`
@ -282,7 +279,7 @@ export class FilterEditorComponent
case FILTER_HAS_TAGS_ALL:
return $localize`Tag: ${
this.tagSelectionModel.items.find((t) => t.id == +rule.value)?.name
this.tags.find((t) => t.id == +rule.value)?.name
}`
case FILTER_HAS_ANY_TAG:
@ -329,6 +326,10 @@ export class FilterEditorComponent
@ViewChild('textFilterInput')
textFilterInput: ElementRef
tags: Tag[] = []
correspondents: Correspondent[] = []
documentTypes: DocumentType[] = []
storagePaths: StoragePath[] = []
customFields: CustomField[] = []
tagDocumentCounts: SelectionDataItem[]
@ -369,7 +370,7 @@ export class FilterEditorComponent
)
}
tagSelectionModel = new FilterableDropdownSelectionModel(true)
tagSelectionModel = new FilterableDropdownSelectionModel()
correspondentSelectionModel = new FilterableDropdownSelectionModel()
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
storagePathSelectionModel = new FilterableDropdownSelectionModel()
@ -550,19 +551,6 @@ 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
@ -581,18 +569,6 @@ 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
@ -611,18 +587,6 @@ 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
@ -845,21 +809,9 @@ export class FilterEditorComponent
})
})
}
if (
this.correspondentSelectionModel.isNoneSelected() &&
this.correspondentSelectionModel.intersection == Intersection.Include
) {
if (this.correspondentSelectionModel.isNoneSelected()) {
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) => {
@ -870,7 +822,6 @@ export class FilterEditorComponent
})
this.correspondentSelectionModel
.getExcludedItems()
.filter((correspondent) => correspondent.id > 0)
.forEach((correspondent) => {
filterRules.push({
rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
@ -878,21 +829,9 @@ export class FilterEditorComponent
})
})
}
if (
this.documentTypeSelectionModel.isNoneSelected() &&
this.documentTypeSelectionModel.intersection === Intersection.Include
) {
if (this.documentTypeSelectionModel.isNoneSelected()) {
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) => {
@ -903,7 +842,6 @@ export class FilterEditorComponent
})
this.documentTypeSelectionModel
.getExcludedItems()
.filter((documentType) => documentType.id > 0)
.forEach((documentType) => {
filterRules.push({
rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
@ -911,21 +849,9 @@ export class FilterEditorComponent
})
})
}
if (
this.storagePathSelectionModel.isNoneSelected() &&
this.storagePathSelectionModel.intersection == Intersection.Include
) {
if (this.storagePathSelectionModel.isNoneSelected()) {
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) => {
@ -936,7 +862,6 @@ export class FilterEditorComponent
})
this.storagePathSelectionModel
.getExcludedItems()
.filter((storagePath) => storagePath.id > 0)
.forEach((storagePath) => {
filterRules.push({
rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
@ -1137,7 +1062,7 @@ export class FilterEditorComponent
) {
this.loadingCountTotal++
this.tagService.listAll().subscribe((result) => {
this.tagSelectionModel.items = result.results
this.tags = result.results
this.maybeCompleteLoading()
})
}
@ -1149,7 +1074,7 @@ export class FilterEditorComponent
) {
this.loadingCountTotal++
this.correspondentService.listAll().subscribe((result) => {
this.correspondentSelectionModel.items = result.results
this.correspondents = result.results
this.maybeCompleteLoading()
})
}
@ -1161,7 +1086,7 @@ export class FilterEditorComponent
) {
this.loadingCountTotal++
this.documentTypeService.listAll().subscribe((result) => {
this.documentTypeSelectionModel.items = result.results
this.documentTypes = result.results
this.maybeCompleteLoading()
})
}
@ -1173,7 +1098,7 @@ export class FilterEditorComponent
) {
this.loadingCountTotal++
this.storagePathService.listAll().subscribe((result) => {
this.storagePathSelectionModel.items = result.results
this.storagePaths = result.results
this.maybeCompleteLoading()
})
}

View File

@ -1,7 +1,5 @@
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

View File

@ -602,6 +602,7 @@ export class SettingsService {
)
} catch (error) {
this.toastService.showError(errorMessage)
console.log(error)
}
this.storeSettings()
@ -613,6 +614,7 @@ export class SettingsService {
},
error: (e) => {
this.toastService.showError(errorMessage)
console.log(e)
},
})
}
@ -634,6 +636,7 @@ export class SettingsService {
this.toastService.showError(
'Error migrating update checking setting'
)
console.log(e)
},
})
}

View File

@ -8,7 +8,6 @@ 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,
@ -98,16 +97,6 @@ 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,

View File

@ -10,7 +10,6 @@ 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'
@ -114,10 +113,6 @@ 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(',')
@ -130,7 +125,7 @@ export function filterRulesFromQueryParams(
val = val.replace('1', 'true').replace('0', 'false')
return {
rule_type: rule_type.id,
value: isNullRuleType ? nullRuleValue : val,
value: isNullRuleType ? null : val,
}
})
)
@ -148,11 +143,6 @@ 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

View File

@ -24,7 +24,7 @@
--pngx-bg-alt2: var(--bs-gray-200); // #e9ecef
--pngx-bg-disabled: #f7f7f7;
--pngx-focus-alpha: 0.3;
--pngx-toast-max-width: 340px;
--pngx-toast-max-width: 360px;
--bs-info: var(--pngx-bg-alt2);
--bs-info-rgb: 233, 236, 239;
@media screen and (min-width: 1024px) {

View File

@ -870,7 +870,7 @@ class BasicUserSerializer(serializers.ModelSerializer):
class NotesSerializer(serializers.ModelSerializer):
user = BasicUserSerializer(read_only=True)
user = BasicUserSerializer()
class Meta:
model = Note
@ -893,7 +893,7 @@ class DocumentSerializer(
created_date = serializers.DateField(required=False)
page_count = SerializerMethodField()
notes = NotesSerializer(many=True, required=False, read_only=True)
notes = NotesSerializer(many=True, required=False)
custom_fields = CustomFieldInstanceSerializer(
many=True,

View File

@ -68,7 +68,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "application/zip")
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
with zipfile.ZipFile(io.BytesIO(response.getvalue())) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("2020-03-21 document B.jpg", zipf.namelist())
@ -89,7 +89,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "application/zip")
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
with zipfile.ZipFile(io.BytesIO(response.getvalue())) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("2020-03-21 document B.pdf", zipf.namelist())
@ -110,7 +110,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "application/zip")
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
with zipfile.ZipFile(io.BytesIO(response.getvalue())) as zipf:
self.assertEqual(len(zipf.filelist), 3)
self.assertIn("originals/2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("archive/2020-03-21 document B.pdf", zipf.namelist())
@ -144,7 +144,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "application/zip")
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
with zipfile.ZipFile(io.BytesIO(response.getvalue())) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
@ -203,7 +203,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "application/zip")
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
with zipfile.ZipFile(io.BytesIO(response.getvalue())) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("a space name/Title 2 - Doc 3.jpg", zipf.namelist())
self.assertIn("test/This is Doc 2.pdf", zipf.namelist())
@ -249,7 +249,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "application/zip")
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
with zipfile.ZipFile(io.BytesIO(response.getvalue())) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("somewhere/This is Doc 2.pdf", zipf.namelist())
self.assertIn("somewhere/Title 2 - Doc 3.pdf", zipf.namelist())
@ -298,7 +298,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "application/zip")
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
with zipfile.ZipFile(io.BytesIO(response.getvalue())) as zipf:
self.assertEqual(len(zipf.filelist), 3)
self.assertIn("originals/bill/This is Doc 2.pdf", zipf.namelist())
self.assertIn("archive/statement/Title 2 - Doc 3.pdf", zipf.namelist())

View File

@ -32,6 +32,7 @@ from django.db.models import When
from django.db.models.functions import Length
from django.db.models.functions import Lower
from django.db.models.manager import Manager
from django.http import FileResponse
from django.http import Http404
from django.http import HttpResponse
from django.http import HttpResponseBadRequest
@ -2015,10 +2016,10 @@ class BulkDownloadView(GenericAPIView):
return HttpResponseForbidden("Insufficient permissions")
settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True)
temp = tempfile.NamedTemporaryFile( # noqa: SIM115
dir=settings.SCRATCH_DIR,
suffix="-compressed-archive",
delete=False,
temp_dir = Path(
tempfile.mkdtemp(
dir=settings.SCRATCH_DIR,
),
)
if content == "both":
@ -2028,20 +2029,17 @@ class BulkDownloadView(GenericAPIView):
else:
strategy_class = ArchiveOnlyStrategy
with zipfile.ZipFile(temp.name, "w", compression) as zipf:
zip_file = temp_dir / "documents.zip"
with zipfile.ZipFile(zip_file, "w", compression) as zipf:
strategy = strategy_class(zipf, follow_formatting=follow_filename_format)
for document in documents:
strategy.add_document(document)
# TODO(stumpylog): Investigate using FileResponse here
with open(temp.name, "rb") as f:
response = HttpResponse(f, content_type="application/zip")
response["Content-Disposition"] = '{}; filename="{}"'.format(
"attachment",
"documents.zip",
)
response = FileResponse(zip_file.open("rb"), as_attachment=True)
response["Content-Length"] = zip_file.stat().st_size
return response
return response
@extend_schema_view(**generate_object_with_permissions_schema(StoragePathSerializer))

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

Some files were not shown because too many files have changed in this diff Show More