diff --git a/src-ui/src/app/components/admin/settings/settings.component.html b/src-ui/src/app/components/admin/settings/settings.component.html
index 52e706f2f..8b239e772 100644
--- a/src-ui/src/app/components/admin/settings/settings.component.html
+++ b/src-ui/src/app/components/admin/settings/settings.component.html
@@ -158,6 +158,14 @@
+ Document editing
+
+
+
Bulk editing
diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts
index 7ce13c675..6e105ed11 100644
--- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts
+++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts
@@ -289,7 +289,7 @@ describe('SettingsComponent', () => {
expect(toastErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
- expect(setSpy).toHaveBeenCalledTimes(24)
+ expect(setSpy).toHaveBeenCalledTimes(25)
// succeed
storeSpy.mockReturnValueOnce(of(true))
diff --git a/src-ui/src/app/components/admin/settings/settings.component.ts b/src-ui/src/app/components/admin/settings/settings.component.ts
index 2bfe5d1c8..a77a556bf 100644
--- a/src-ui/src/app/components/admin/settings/settings.component.ts
+++ b/src-ui/src/app/components/admin/settings/settings.component.ts
@@ -88,6 +88,7 @@ export class SettingsComponent
defaultPermsViewGroups: new FormControl(null),
defaultPermsEditUsers: new FormControl(null),
defaultPermsEditGroups: new FormControl(null),
+ documentEditingRemoveInboxTags: new FormControl(null),
notificationsConsumerNewDocument: new FormControl(null),
notificationsConsumerSuccess: new FormControl(null),
@@ -271,6 +272,9 @@ export class SettingsComponent
defaultPermsEditGroups: this.settings.get(
SETTINGS_KEYS.DEFAULT_PERMS_EDIT_GROUPS
),
+ documentEditingRemoveInboxTags: this.settings.get(
+ SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS
+ ),
savedViews: {},
}
}
@@ -484,6 +488,10 @@ export class SettingsComponent
SETTINGS_KEYS.DEFAULT_PERMS_EDIT_GROUPS,
this.settingsForm.value.defaultPermsEditGroups
)
+ this.settings.set(
+ SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS,
+ this.settingsForm.value.documentEditingRemoveInboxTags
+ )
this.settings.setLanguage(this.settingsForm.value.displayLanguage)
this.settings
.storeSettings()
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts
index 0ca458a21..a1162ab7f 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.ts
@@ -630,7 +630,9 @@ export class DocumentDetailComponent
.update(this.document)
.pipe(first())
.subscribe({
- next: () => {
+ next: (docValues) => {
+ // in case data changed while saving eg removing inbox_tags
+ this.documentForm.patchValue(docValues)
this.store.next(this.documentForm.value)
this.toastService.showInfo($localize`Document saved successfully.`)
close && this.close()
diff --git a/src-ui/src/app/data/document.ts b/src-ui/src/app/data/document.ts
index 2bdb954ce..910666f10 100644
--- a/src-ui/src/app/data/document.ts
+++ b/src-ui/src/app/data/document.ts
@@ -63,4 +63,7 @@ export interface Document extends ObjectWithPermissions {
__search_hit__?: SearchHit
custom_fields?: CustomFieldInstance[]
+
+ // write-only field
+ remove_inbox_tags?: boolean
}
diff --git a/src-ui/src/app/data/ui-settings.ts b/src-ui/src/app/data/ui-settings.ts
index e23e490e9..e55f25278 100644
--- a/src-ui/src/app/data/ui-settings.ts
+++ b/src-ui/src/app/data/ui-settings.ts
@@ -53,6 +53,8 @@ export const SETTINGS_KEYS = {
DEFAULT_PERMS_VIEW_GROUPS: 'general-settings:permissions:default-view-groups',
DEFAULT_PERMS_EDIT_USERS: 'general-settings:permissions:default-edit-users',
DEFAULT_PERMS_EDIT_GROUPS: 'general-settings:permissions:default-edit-groups',
+ DOCUMENT_EDITING_REMOVE_INBOX_TAGS:
+ 'general-settings:document-editing:remove-inbox-tags',
}
export const SETTINGS: UiSetting[] = [
@@ -206,4 +208,9 @@ export const SETTINGS: UiSetting[] = [
type: 'string',
default: '',
},
+ {
+ key: SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS,
+ type: 'boolean',
+ default: false,
+ },
]
diff --git a/src-ui/src/app/services/rest/document.service.spec.ts b/src-ui/src/app/services/rest/document.service.spec.ts
index 8576a2399..1f3ccc0af 100644
--- a/src-ui/src/app/services/rest/document.service.spec.ts
+++ b/src-ui/src/app/services/rest/document.service.spec.ts
@@ -7,10 +7,13 @@ import { TestBed } from '@angular/core/testing'
import { environment } from 'src/environments/environment'
import { DocumentService } from './document.service'
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
+import { SettingsService } from '../settings.service'
+import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
let httpTestingController: HttpTestingController
let service: DocumentService
let subscription: Subscription
+let settingsService: SettingsService
const endpoint = 'documents'
const documents = [
{
@@ -34,6 +37,17 @@ const documents = [
},
]
+beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [DocumentService],
+ imports: [HttpClientTestingModule],
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(DocumentService)
+ settingsService = TestBed.inject(SettingsService)
+})
+
describe(`DocumentService`, () => {
// common tests e.g. commonAbstractPaperlessServiceTests differ slightly
it('should call appropriate api endpoint for list all', () => {
@@ -237,16 +251,21 @@ describe(`DocumentService`, () => {
)
expect(req.request.method).toEqual('GET')
})
-})
-beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [DocumentService],
- imports: [HttpClientTestingModule],
+ it('should pass remove_inbox_tags setting to update', () => {
+ subscription = service.update(documents[0]).subscribe()
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/`
+ )
+ expect(req.request.body.remove_inbox_tags).toEqual(false)
+
+ settingsService.set(SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS, true)
+ subscription = service.update(documents[0]).subscribe()
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/`
+ )
+ expect(req.request.body.remove_inbox_tags).toEqual(true)
})
-
- httpTestingController = TestBed.inject(HttpTestingController)
- service = TestBed.inject(DocumentService)
})
afterEach(() => {
diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts
index 9ff99031f..37147b818 100644
--- a/src-ui/src/app/services/rest/document.service.ts
+++ b/src-ui/src/app/services/rest/document.service.ts
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'
import { Document } from 'src/app/data/document'
import { DocumentMetadata } from 'src/app/data/document-metadata'
import { AbstractPaperlessService } from './abstract-paperless-service'
-import { HttpClient, HttpParams } from '@angular/common/http'
+import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'
import { Results } from 'src/app/data/results'
import { FilterRule } from 'src/app/data/filter-rule'
@@ -18,6 +18,8 @@ import {
PermissionType,
PermissionsService,
} from '../permissions.service'
+import { SettingsService } from '../settings.service'
+import { SETTINGS, SETTINGS_KEYS } from 'src/app/data/ui-settings'
export const DOCUMENT_SORT_FIELDS = [
{ field: 'archive_serial_number', name: $localize`ASN` },
@@ -63,7 +65,8 @@ export class DocumentService extends AbstractPaperlessService {
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private storagePathService: StoragePathService,
- private permissionsService: PermissionsService
+ private permissionsService: PermissionsService,
+ private settingsService: SettingsService
) {
super(http, 'documents')
}
@@ -180,6 +183,9 @@ export class DocumentService extends AbstractPaperlessService {
update(o: Document): Observable {
// we want to only set created_date
o.created = undefined
+ o.remove_inbox_tags = this.settingsService.get(
+ SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS
+ )
return super.update(o)
}
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index 3c65e11d9..0839a14b5 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -638,6 +638,11 @@ class DocumentSerializer(
allow_null=True,
)
+ remove_inbox_tags = serializers.BooleanField(
+ default=False,
+ write_only=True,
+ )
+
def get_original_file_name(self, obj):
return obj.original_filename
@@ -681,12 +686,48 @@ class DocumentSerializer(
custom_field_instance.field,
doc_id,
)
+ if (
+ "remove_inbox_tags" in validated_data
+ and validated_data["remove_inbox_tags"]
+ ):
+ tag_ids_being_added = (
+ [
+ tag.id
+ for tag in validated_data["tags"]
+ if tag not in instance.tags.all()
+ ]
+ if "tags" in validated_data
+ else []
+ )
+ inbox_tags_not_being_added = Tag.objects.filter(is_inbox_tag=True).exclude(
+ id__in=tag_ids_being_added,
+ )
+ if "tags" in validated_data:
+ validated_data["tags"] = [
+ tag
+ for tag in validated_data["tags"]
+ if tag not in inbox_tags_not_being_added
+ ]
+ else:
+ validated_data["tags"] = [
+ tag
+ for tag in instance.tags.all()
+ if tag not in inbox_tags_not_being_added
+ ]
super().update(instance, validated_data)
return instance
def __init__(self, *args, **kwargs):
self.truncate_content = kwargs.pop("truncate_content", False)
+ # return full permissions if we're doing a PATCH or PUT
+ context = kwargs.get("context")
+ if (
+ context.get("request").method == "PATCH"
+ or context.get("request").method == "PUT"
+ ):
+ kwargs["full_perms"] = True
+
super().__init__(*args, **kwargs)
class Meta:
@@ -714,6 +755,7 @@ class DocumentSerializer(
"set_permissions",
"notes",
"custom_fields",
+ "remove_inbox_tags",
)
diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py
index 510e2b1b3..20dd64d82 100644
--- a/src/documents/tests/test_api_documents.py
+++ b/src/documents/tests/test_api_documents.py
@@ -2080,6 +2080,72 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp.content, b"1")
+ def test_remove_inbox_tags(self):
+ """
+ GIVEN:
+ - Existing document with or without inbox tags
+ WHEN:
+ - API request to update document, with or without `remove_inbox_tags` flag
+ THEN:
+ - Inbox tags are removed as long as they are not being added
+ """
+ tag1 = Tag.objects.create(name="tag1", color="#abcdef")
+ inbox_tag1 = Tag.objects.create(
+ name="inbox1",
+ color="#abcdef",
+ is_inbox_tag=True,
+ )
+ inbox_tag2 = Tag.objects.create(
+ name="inbox2",
+ color="#abcdef",
+ is_inbox_tag=True,
+ )
+
+ doc1 = Document.objects.create(
+ title="test",
+ mime_type="application/pdf",
+ content="this is a document 1",
+ checksum="1",
+ )
+ doc1.tags.add(tag1)
+ doc1.tags.add(inbox_tag1)
+ doc1.tags.add(inbox_tag2)
+ doc1.save()
+
+ # Remove inbox tags defaults to false
+ resp = self.client.patch(
+ f"/api/documents/{doc1.pk}/",
+ {
+ "title": "New title",
+ },
+ )
+ doc1.refresh_from_db()
+ self.assertEqual(resp.status_code, status.HTTP_200_OK)
+ self.assertEqual(doc1.tags.count(), 3)
+
+ # Remove inbox tags set to true
+ resp = self.client.patch(
+ f"/api/documents/{doc1.pk}/",
+ {
+ "remove_inbox_tags": True,
+ },
+ )
+ doc1.refresh_from_db()
+ self.assertEqual(resp.status_code, status.HTTP_200_OK)
+ self.assertEqual(doc1.tags.count(), 1)
+
+ # Remove inbox tags set to true but adding a new inbox tag
+ resp = self.client.patch(
+ f"/api/documents/{doc1.pk}/",
+ {
+ "remove_inbox_tags": True,
+ "tags": [inbox_tag1.pk, tag1.pk],
+ },
+ )
+ doc1.refresh_from_db()
+ self.assertEqual(resp.status_code, status.HTTP_200_OK)
+ self.assertEqual(doc1.tags.count(), 2)
+
class TestDocumentApiV2(DirectoriesMixin, APITestCase):
def setUp(self):