mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Fix: deselect and trigger refresh for deleted documents from bulk operations with "delete originals" (#8996)
This commit is contained in:
		@@ -2549,15 +2549,15 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">793</context>
 | 
			
		||||
          <context context-type="linenumber">796</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">826</context>
 | 
			
		||||
          <context context-type="linenumber">829</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">845</context>
 | 
			
		||||
          <context context-type="linenumber">848</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
 | 
			
		||||
@@ -3143,27 +3143,27 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">436</context>
 | 
			
		||||
          <context context-type="linenumber">439</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">476</context>
 | 
			
		||||
          <context context-type="linenumber">479</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">514</context>
 | 
			
		||||
          <context context-type="linenumber">517</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">552</context>
 | 
			
		||||
          <context context-type="linenumber">555</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">614</context>
 | 
			
		||||
          <context context-type="linenumber">617</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">747</context>
 | 
			
		||||
          <context context-type="linenumber">750</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1407560924967345762" datatype="html">
 | 
			
		||||
@@ -6143,7 +6143,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">381</context>
 | 
			
		||||
          <context context-type="linenumber">384</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">this string is used to separate processing, failed and added on the file upload widget</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@@ -6676,7 +6676,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">749</context>
 | 
			
		||||
          <context context-type="linenumber">752</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2048798344356757326" datatype="html">
 | 
			
		||||
@@ -6687,7 +6687,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">751</context>
 | 
			
		||||
          <context context-type="linenumber">754</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7295637485862454066" datatype="html">
 | 
			
		||||
@@ -6705,7 +6705,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">789</context>
 | 
			
		||||
          <context context-type="linenumber">792</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2951161989614003846" datatype="html">
 | 
			
		||||
@@ -6786,7 +6786,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">823</context>
 | 
			
		||||
          <context context-type="linenumber">826</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="857641176955257111" datatype="html">
 | 
			
		||||
@@ -6982,25 +6982,25 @@
 | 
			
		||||
        <source>Error executing bulk operation</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">285</context>
 | 
			
		||||
          <context context-type="linenumber">288</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7894972847287473517" datatype="html">
 | 
			
		||||
        <source>"<x id="PH" equiv-text="items[0].name"/>"</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">373</context>
 | 
			
		||||
          <context context-type="linenumber">376</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">379</context>
 | 
			
		||||
          <context context-type="linenumber">382</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8639884465898458690" datatype="html">
 | 
			
		||||
        <source>"<x id="PH" equiv-text="items[0].name"/>" and "<x id="PH_1" equiv-text="items[1].name"/>"</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">375</context>
 | 
			
		||||
          <context context-type="linenumber">378</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">This is for messages like 'modify "tag1" and "tag2"'</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@@ -7008,7 +7008,7 @@
 | 
			
		||||
        <source><x id="PH" equiv-text="list"/> and "<x id="PH_1" equiv-text="items[items.length - 1].name"/>"</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">383,385</context>
 | 
			
		||||
          <context context-type="linenumber">386,388</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">this is for messages like 'modify "tag1", "tag2" and "tag3"'</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@@ -7016,14 +7016,14 @@
 | 
			
		||||
        <source>Confirm tags assignment</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">400</context>
 | 
			
		||||
          <context context-type="linenumber">403</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6619516195038467207" datatype="html">
 | 
			
		||||
        <source>This operation will add the tag "<x id="PH" equiv-text="tag.name"/>" to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">406</context>
 | 
			
		||||
          <context context-type="linenumber">409</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1894412783609570695" datatype="html">
 | 
			
		||||
@@ -7032,14 +7032,14 @@
 | 
			
		||||
        )"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">411,413</context>
 | 
			
		||||
          <context context-type="linenumber">414,416</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7181166515756808573" datatype="html">
 | 
			
		||||
        <source>This operation will remove the tag "<x id="PH" equiv-text="tag.name"/>" from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">419</context>
 | 
			
		||||
          <context context-type="linenumber">422</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3819792277998068944" datatype="html">
 | 
			
		||||
@@ -7048,7 +7048,7 @@
 | 
			
		||||
        )"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">424,426</context>
 | 
			
		||||
          <context context-type="linenumber">427,429</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2739066218579571288" datatype="html">
 | 
			
		||||
@@ -7059,84 +7059,84 @@
 | 
			
		||||
        )"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">428,432</context>
 | 
			
		||||
          <context context-type="linenumber">431,435</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2996713129519325161" datatype="html">
 | 
			
		||||
        <source>Confirm correspondent assignment</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">469</context>
 | 
			
		||||
          <context context-type="linenumber">472</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6900893559485781849" datatype="html">
 | 
			
		||||
        <source>This operation will assign the correspondent "<x id="PH" equiv-text="correspondent.name"/>" to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">471</context>
 | 
			
		||||
          <context context-type="linenumber">474</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1257522660364398440" datatype="html">
 | 
			
		||||
        <source>This operation will remove the correspondent from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">473</context>
 | 
			
		||||
          <context context-type="linenumber">476</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5393409374423140648" datatype="html">
 | 
			
		||||
        <source>Confirm document type assignment</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">507</context>
 | 
			
		||||
          <context context-type="linenumber">510</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="332180123895325027" datatype="html">
 | 
			
		||||
        <source>This operation will assign the document type "<x id="PH" equiv-text="documentType.name"/>" to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">509</context>
 | 
			
		||||
          <context context-type="linenumber">512</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2236642492594872779" datatype="html">
 | 
			
		||||
        <source>This operation will remove the document type from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">511</context>
 | 
			
		||||
          <context context-type="linenumber">514</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6386555513013840736" datatype="html">
 | 
			
		||||
        <source>Confirm storage path assignment</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">545</context>
 | 
			
		||||
          <context context-type="linenumber">548</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8750527458618415924" datatype="html">
 | 
			
		||||
        <source>This operation will assign the storage path "<x id="PH" equiv-text="storagePath.name"/>" to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">547</context>
 | 
			
		||||
          <context context-type="linenumber">550</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="60728365335056946" datatype="html">
 | 
			
		||||
        <source>This operation will remove the storage path from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">549</context>
 | 
			
		||||
          <context context-type="linenumber">552</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="4187352575310415704" datatype="html">
 | 
			
		||||
        <source>Confirm custom field assignment</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">578</context>
 | 
			
		||||
          <context context-type="linenumber">581</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7966494636326273856" datatype="html">
 | 
			
		||||
        <source>This operation will assign the custom field "<x id="PH" equiv-text="customField.name"/>" to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">584</context>
 | 
			
		||||
          <context context-type="linenumber">587</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5789455969634598553" datatype="html">
 | 
			
		||||
@@ -7145,14 +7145,14 @@
 | 
			
		||||
        )"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">589,591</context>
 | 
			
		||||
          <context context-type="linenumber">592,594</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5648572354333199245" datatype="html">
 | 
			
		||||
        <source>This operation will remove the custom field "<x id="PH" equiv-text="customField.name"/>" from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">597</context>
 | 
			
		||||
          <context context-type="linenumber">600</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6666899594015948817" datatype="html">
 | 
			
		||||
@@ -7161,7 +7161,7 @@
 | 
			
		||||
        )"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">602,604</context>
 | 
			
		||||
          <context context-type="linenumber">605,607</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8050047262594964176" datatype="html">
 | 
			
		||||
@@ -7172,70 +7172,70 @@
 | 
			
		||||
        )"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">606,610</context>
 | 
			
		||||
          <context context-type="linenumber">609,613</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8615059324209654051" datatype="html">
 | 
			
		||||
        <source>Move <x id="PH" equiv-text="this.list.selected.size"/> selected document(s) to the trash?</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">748</context>
 | 
			
		||||
          <context context-type="linenumber">751</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8585195717323764335" datatype="html">
 | 
			
		||||
        <source>This operation will permanently recreate the archive files for <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">790</context>
 | 
			
		||||
          <context context-type="linenumber">793</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7366623494074776040" datatype="html">
 | 
			
		||||
        <source>The archive files will be re-generated with the current settings.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">791</context>
 | 
			
		||||
          <context context-type="linenumber">794</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6390006284731990222" datatype="html">
 | 
			
		||||
        <source>This operation will permanently rotate the original version of <x id="PH" equiv-text="this.list.selected.size"/> document(s).</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">824</context>
 | 
			
		||||
          <context context-type="linenumber">827</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7910756456450124185" datatype="html">
 | 
			
		||||
        <source>Merge confirm</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">843</context>
 | 
			
		||||
          <context context-type="linenumber">846</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7643543647233874431" datatype="html">
 | 
			
		||||
        <source>This operation will merge <x id="PH" equiv-text="this.list.selected.size"/> selected documents into a new document.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">844</context>
 | 
			
		||||
          <context context-type="linenumber">847</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7869008840945899895" datatype="html">
 | 
			
		||||
        <source>Merged document will be queued for consumption.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">860</context>
 | 
			
		||||
          <context context-type="linenumber">863</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="476913782630693351" datatype="html">
 | 
			
		||||
        <source>Custom fields updated.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">882</context>
 | 
			
		||||
          <context context-type="linenumber">885</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3873496751167944011" datatype="html">
 | 
			
		||||
        <source>Error updating custom fields.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">891</context>
 | 
			
		||||
          <context context-type="linenumber">894</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6307402210351946694" datatype="html">
 | 
			
		||||
@@ -7414,7 +7414,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">310</context>
 | 
			
		||||
          <context context-type="linenumber">314</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1494518490116523821" datatype="html">
 | 
			
		||||
@@ -7425,7 +7425,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">303</context>
 | 
			
		||||
          <context context-type="linenumber">307</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8461842260159597706" datatype="html">
 | 
			
		||||
@@ -7668,42 +7668,42 @@
 | 
			
		||||
        <source>Reset filters / selection</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">291</context>
 | 
			
		||||
          <context context-type="linenumber">295</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="4135055128446167640" datatype="html">
 | 
			
		||||
        <source>Open first [selected] document</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">319</context>
 | 
			
		||||
          <context context-type="linenumber">323</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3629960544875360046" datatype="html">
 | 
			
		||||
        <source>Previous page</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">335</context>
 | 
			
		||||
          <context context-type="linenumber">339</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3337301694210287595" datatype="html">
 | 
			
		||||
        <source>Next page</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">347</context>
 | 
			
		||||
          <context context-type="linenumber">351</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2155249406916744630" datatype="html">
 | 
			
		||||
        <source>View "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>" saved successfully.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">379</context>
 | 
			
		||||
          <context context-type="linenumber">383</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6837554170707123455" datatype="html">
 | 
			
		||||
        <source>View "<x id="PH" equiv-text="savedView.name"/>" created successfully.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">422</context>
 | 
			
		||||
          <context context-type="linenumber">426</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="739880801667335279" datatype="html">
 | 
			
		||||
@@ -9233,122 +9233,6 @@
 | 
			
		||||
          <context context-type="linenumber">11</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2119857572761283468" datatype="html">
 | 
			
		||||
        <source>Document already exists.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">17</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5103087968344279314" datatype="html">
 | 
			
		||||
        <source>Document already exists. Note: existing document is in the trash.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">18</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6108404046106249255" datatype="html">
 | 
			
		||||
        <source>Document with ASN already exists.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">19</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="65951081560571094" datatype="html">
 | 
			
		||||
        <source>Document with ASN already exists. Note: existing document is in the trash.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">20</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="148389968432135849" datatype="html">
 | 
			
		||||
        <source>File not found.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">21</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1520671543092565667" datatype="html">
 | 
			
		||||
        <source>Pre-consume script does not exist.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">22</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">Pre-Consume is a term that appears like that in the documentation as well and does not need a specific translation</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7742915911032564889" datatype="html">
 | 
			
		||||
        <source>Error while executing pre-consume script.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">23</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">Pre-Consume is a term that appears like that in the documentation as well and does not need a specific translation</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8995193730018060346" datatype="html">
 | 
			
		||||
        <source>Post-consume script does not exist.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">24</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">Post-Consume is a term that appears like that in the documentation as well and does not need a specific translation</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="256773668518189604" datatype="html">
 | 
			
		||||
        <source>Error while executing post-consume script.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">25</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">Post-Consume is a term that appears like that in the documentation as well and does not need a specific translation</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6252258095055634191" datatype="html">
 | 
			
		||||
        <source>Received new file.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">26</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7337565919209746135" datatype="html">
 | 
			
		||||
        <source>File type not supported.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">27</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5002399167376099234" datatype="html">
 | 
			
		||||
        <source>Processing document...</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">28</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1085975194762600381" datatype="html">
 | 
			
		||||
        <source>Generating thumbnail...</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">29</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3280851677698431426" datatype="html">
 | 
			
		||||
        <source>Retrieving date from document...</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">30</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7162102384876037296" datatype="html">
 | 
			
		||||
        <source>Saving document...</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">31</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="4550450765009165976" datatype="html">
 | 
			
		||||
        <source>Finished.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/consumer-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">32</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5523607037798226031" datatype="html">
 | 
			
		||||
        <source>You have unsaved changes to the document</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
@@ -9664,6 +9548,122 @@
 | 
			
		||||
          <context context-type="linenumber">70</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2119857572761283468" datatype="html">
 | 
			
		||||
        <source>Document already exists.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">23</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5103087968344279314" datatype="html">
 | 
			
		||||
        <source>Document already exists. Note: existing document is in the trash.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">24</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6108404046106249255" datatype="html">
 | 
			
		||||
        <source>Document with ASN already exists.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">25</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="65951081560571094" datatype="html">
 | 
			
		||||
        <source>Document with ASN already exists. Note: existing document is in the trash.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">26</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="148389968432135849" datatype="html">
 | 
			
		||||
        <source>File not found.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">27</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1520671543092565667" datatype="html">
 | 
			
		||||
        <source>Pre-consume script does not exist.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">28</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">Pre-Consume is a term that appears like that in the documentation as well and does not need a specific translation</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7742915911032564889" datatype="html">
 | 
			
		||||
        <source>Error while executing pre-consume script.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">29</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">Pre-Consume is a term that appears like that in the documentation as well and does not need a specific translation</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8995193730018060346" datatype="html">
 | 
			
		||||
        <source>Post-consume script does not exist.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">30</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">Post-Consume is a term that appears like that in the documentation as well and does not need a specific translation</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="256773668518189604" datatype="html">
 | 
			
		||||
        <source>Error while executing post-consume script.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">31</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">Post-Consume is a term that appears like that in the documentation as well and does not need a specific translation</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6252258095055634191" datatype="html">
 | 
			
		||||
        <source>Received new file.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">32</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7337565919209746135" datatype="html">
 | 
			
		||||
        <source>File type not supported.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">33</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5002399167376099234" datatype="html">
 | 
			
		||||
        <source>Processing document...</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">34</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1085975194762600381" datatype="html">
 | 
			
		||||
        <source>Generating thumbnail...</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">35</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3280851677698431426" datatype="html">
 | 
			
		||||
        <source>Retrieving date from document...</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">36</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7162102384876037296" datatype="html">
 | 
			
		||||
        <source>Saving document...</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">37</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="4550450765009165976" datatype="html">
 | 
			
		||||
        <source>Finished.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/services/websocket-status.service.ts</context>
 | 
			
		||||
          <context context-type="linenumber">38</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
    </body>
 | 
			
		||||
  </file>
 | 
			
		||||
</xliff>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,20 +18,20 @@ import { ToastsComponent } from './components/common/toasts/toasts.component'
 | 
			
		||||
import { FileDropComponent } from './components/file-drop/file-drop.component'
 | 
			
		||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
 | 
			
		||||
import { PermissionsGuard } from './guards/permissions.guard'
 | 
			
		||||
import {
 | 
			
		||||
  ConsumerStatusService,
 | 
			
		||||
  FileStatus,
 | 
			
		||||
} from './services/consumer-status.service'
 | 
			
		||||
import { HotKeyService } from './services/hot-key.service'
 | 
			
		||||
import { PermissionsService } from './services/permissions.service'
 | 
			
		||||
import { SettingsService } from './services/settings.service'
 | 
			
		||||
import { Toast, ToastService } from './services/toast.service'
 | 
			
		||||
import {
 | 
			
		||||
  FileStatus,
 | 
			
		||||
  WebsocketStatusService,
 | 
			
		||||
} from './services/websocket-status.service'
 | 
			
		||||
 | 
			
		||||
describe('AppComponent', () => {
 | 
			
		||||
  let component: AppComponent
 | 
			
		||||
  let fixture: ComponentFixture<AppComponent>
 | 
			
		||||
  let tourService: TourService
 | 
			
		||||
  let consumerStatusService: ConsumerStatusService
 | 
			
		||||
  let websocketStatusService: WebsocketStatusService
 | 
			
		||||
  let permissionsService: PermissionsService
 | 
			
		||||
  let toastService: ToastService
 | 
			
		||||
  let router: Router
 | 
			
		||||
@@ -59,7 +59,7 @@ describe('AppComponent', () => {
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    tourService = TestBed.inject(TourService)
 | 
			
		||||
    consumerStatusService = TestBed.inject(ConsumerStatusService)
 | 
			
		||||
    websocketStatusService = TestBed.inject(WebsocketStatusService)
 | 
			
		||||
    permissionsService = TestBed.inject(PermissionsService)
 | 
			
		||||
    settingsService = TestBed.inject(SettingsService)
 | 
			
		||||
    toastService = TestBed.inject(ToastService)
 | 
			
		||||
@@ -90,7 +90,7 @@ describe('AppComponent', () => {
 | 
			
		||||
    const toastSpy = jest.spyOn(toastService, 'show')
 | 
			
		||||
    const fileStatusSubject = new Subject<FileStatus>()
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
 | 
			
		||||
      .spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
 | 
			
		||||
      .mockReturnValue(fileStatusSubject)
 | 
			
		||||
    component.ngOnInit()
 | 
			
		||||
    const status = new FileStatus()
 | 
			
		||||
@@ -109,7 +109,7 @@ describe('AppComponent', () => {
 | 
			
		||||
    const toastSpy = jest.spyOn(toastService, 'show')
 | 
			
		||||
    const fileStatusSubject = new Subject<FileStatus>()
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
 | 
			
		||||
      .spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
 | 
			
		||||
      .mockReturnValue(fileStatusSubject)
 | 
			
		||||
    component.ngOnInit()
 | 
			
		||||
    fileStatusSubject.next(new FileStatus())
 | 
			
		||||
@@ -122,7 +122,7 @@ describe('AppComponent', () => {
 | 
			
		||||
    const toastSpy = jest.spyOn(toastService, 'show')
 | 
			
		||||
    const fileStatusSubject = new Subject<FileStatus>()
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(consumerStatusService, 'onDocumentDetected')
 | 
			
		||||
      .spyOn(websocketStatusService, 'onDocumentDetected')
 | 
			
		||||
      .mockReturnValue(fileStatusSubject)
 | 
			
		||||
    component.ngOnInit()
 | 
			
		||||
    fileStatusSubject.next(new FileStatus())
 | 
			
		||||
@@ -136,7 +136,7 @@ describe('AppComponent', () => {
 | 
			
		||||
    const toastSpy = jest.spyOn(toastService, 'show')
 | 
			
		||||
    const fileStatusSubject = new Subject<FileStatus>()
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(consumerStatusService, 'onDocumentDetected')
 | 
			
		||||
      .spyOn(websocketStatusService, 'onDocumentDetected')
 | 
			
		||||
      .mockReturnValue(fileStatusSubject)
 | 
			
		||||
    component.ngOnInit()
 | 
			
		||||
    fileStatusSubject.next(new FileStatus())
 | 
			
		||||
@@ -148,7 +148,7 @@ describe('AppComponent', () => {
 | 
			
		||||
    const toastSpy = jest.spyOn(toastService, 'showError')
 | 
			
		||||
    const fileStatusSubject = new Subject<FileStatus>()
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(consumerStatusService, 'onDocumentConsumptionFailed')
 | 
			
		||||
      .spyOn(websocketStatusService, 'onDocumentConsumptionFailed')
 | 
			
		||||
      .mockReturnValue(fileStatusSubject)
 | 
			
		||||
    component.ngOnInit()
 | 
			
		||||
    fileStatusSubject.next(new FileStatus())
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import { ToastsComponent } from './components/common/toasts/toasts.component'
 | 
			
		||||
import { FileDropComponent } from './components/file-drop/file-drop.component'
 | 
			
		||||
import { SETTINGS_KEYS } from './data/ui-settings'
 | 
			
		||||
import { ComponentRouterService } from './services/component-router.service'
 | 
			
		||||
import { ConsumerStatusService } from './services/consumer-status.service'
 | 
			
		||||
import { HotKeyService } from './services/hot-key.service'
 | 
			
		||||
import {
 | 
			
		||||
  PermissionAction,
 | 
			
		||||
@@ -16,6 +15,7 @@ import {
 | 
			
		||||
import { SettingsService } from './services/settings.service'
 | 
			
		||||
import { TasksService } from './services/tasks.service'
 | 
			
		||||
import { ToastService } from './services/toast.service'
 | 
			
		||||
import { WebsocketStatusService } from './services/websocket-status.service'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'pngx-root',
 | 
			
		||||
@@ -35,7 +35,7 @@ export class AppComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private settings: SettingsService,
 | 
			
		||||
    private consumerStatusService: ConsumerStatusService,
 | 
			
		||||
    private websocketStatusService: WebsocketStatusService,
 | 
			
		||||
    private toastService: ToastService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private tasksService: TasksService,
 | 
			
		||||
@@ -51,7 +51,7 @@ export class AppComponent implements OnInit, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.consumerStatusService.disconnect()
 | 
			
		||||
    this.websocketStatusService.disconnect()
 | 
			
		||||
    if (this.successSubscription) {
 | 
			
		||||
      this.successSubscription.unsubscribe()
 | 
			
		||||
    }
 | 
			
		||||
@@ -76,9 +76,9 @@ export class AppComponent implements OnInit, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.consumerStatusService.connect()
 | 
			
		||||
    this.websocketStatusService.connect()
 | 
			
		||||
 | 
			
		||||
    this.successSubscription = this.consumerStatusService
 | 
			
		||||
    this.successSubscription = this.websocketStatusService
 | 
			
		||||
      .onDocumentConsumptionFinished()
 | 
			
		||||
      .subscribe((status) => {
 | 
			
		||||
        this.tasksService.reload()
 | 
			
		||||
@@ -108,7 +108,7 @@ export class AppComponent implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    this.failedSubscription = this.consumerStatusService
 | 
			
		||||
    this.failedSubscription = this.websocketStatusService
 | 
			
		||||
      .onDocumentConsumptionFailed()
 | 
			
		||||
      .subscribe((status) => {
 | 
			
		||||
        this.tasksService.reload()
 | 
			
		||||
@@ -121,7 +121,7 @@ export class AppComponent implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    this.newDocumentSubscription = this.consumerStatusService
 | 
			
		||||
    this.newDocumentSubscription = this.websocketStatusService
 | 
			
		||||
      .onDocumentDetected()
 | 
			
		||||
      .subscribe((status) => {
 | 
			
		||||
        this.tasksService.reload()
 | 
			
		||||
 
 | 
			
		||||
@@ -33,14 +33,14 @@ import { PermissionsGuard } from 'src/app/guards/permissions.guard'
 | 
			
		||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
 | 
			
		||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
 | 
			
		||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
 | 
			
		||||
import {
 | 
			
		||||
  ConsumerStatusService,
 | 
			
		||||
  FileStatus,
 | 
			
		||||
} from 'src/app/services/consumer-status.service'
 | 
			
		||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
 | 
			
		||||
import { PermissionsService } from 'src/app/services/permissions.service'
 | 
			
		||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
 | 
			
		||||
import { DocumentService } from 'src/app/services/rest/document.service'
 | 
			
		||||
import {
 | 
			
		||||
  FileStatus,
 | 
			
		||||
  WebsocketStatusService,
 | 
			
		||||
} from 'src/app/services/websocket-status.service'
 | 
			
		||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
 | 
			
		||||
import { SavedViewWidgetComponent } from './saved-view-widget.component'
 | 
			
		||||
 | 
			
		||||
@@ -112,7 +112,7 @@ describe('SavedViewWidgetComponent', () => {
 | 
			
		||||
  let component: SavedViewWidgetComponent
 | 
			
		||||
  let fixture: ComponentFixture<SavedViewWidgetComponent>
 | 
			
		||||
  let documentService: DocumentService
 | 
			
		||||
  let consumerStatusService: ConsumerStatusService
 | 
			
		||||
  let websocketStatusService: WebsocketStatusService
 | 
			
		||||
  let documentListViewService: DocumentListViewService
 | 
			
		||||
  let router: Router
 | 
			
		||||
 | 
			
		||||
@@ -176,7 +176,7 @@ describe('SavedViewWidgetComponent', () => {
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    documentService = TestBed.inject(DocumentService)
 | 
			
		||||
    consumerStatusService = TestBed.inject(ConsumerStatusService)
 | 
			
		||||
    websocketStatusService = TestBed.inject(WebsocketStatusService)
 | 
			
		||||
    documentListViewService = TestBed.inject(DocumentListViewService)
 | 
			
		||||
    router = TestBed.inject(Router)
 | 
			
		||||
    fixture = TestBed.createComponent(SavedViewWidgetComponent)
 | 
			
		||||
@@ -235,7 +235,7 @@ describe('SavedViewWidgetComponent', () => {
 | 
			
		||||
  it('should reload on document consumption finished', () => {
 | 
			
		||||
    const fileStatusSubject = new Subject<FileStatus>()
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
 | 
			
		||||
      .spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
 | 
			
		||||
      .mockReturnValue(fileStatusSubject)
 | 
			
		||||
    const reloadSpy = jest.spyOn(component, 'reload')
 | 
			
		||||
    component.ngOnInit()
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,6 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
 | 
			
		||||
import { DocumentTypeNamePipe } from 'src/app/pipes/document-type-name.pipe'
 | 
			
		||||
import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe'
 | 
			
		||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
 | 
			
		||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
 | 
			
		||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
 | 
			
		||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
 | 
			
		||||
import {
 | 
			
		||||
@@ -53,6 +52,7 @@ import {
 | 
			
		||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
 | 
			
		||||
import { DocumentService } from 'src/app/services/rest/document.service'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
 | 
			
		||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@@ -94,7 +94,7 @@ export class SavedViewWidgetComponent
 | 
			
		||||
    private documentService: DocumentService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private list: DocumentListViewService,
 | 
			
		||||
    private consumerStatusService: ConsumerStatusService,
 | 
			
		||||
    private websocketStatusService: WebsocketStatusService,
 | 
			
		||||
    public openDocumentsService: OpenDocumentsService,
 | 
			
		||||
    public documentListViewService: DocumentListViewService,
 | 
			
		||||
    public permissionsService: PermissionsService,
 | 
			
		||||
@@ -124,7 +124,7 @@ export class SavedViewWidgetComponent
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.reload()
 | 
			
		||||
    this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
 | 
			
		||||
    this.consumerStatusService
 | 
			
		||||
    this.websocketStatusService
 | 
			
		||||
      .onDocumentConsumptionFinished()
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .subscribe(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,9 @@ import { routes } from 'src/app/app-routing.module'
 | 
			
		||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
 | 
			
		||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
 | 
			
		||||
import {
 | 
			
		||||
  ConsumerStatusService,
 | 
			
		||||
  FileStatus,
 | 
			
		||||
} from 'src/app/services/consumer-status.service'
 | 
			
		||||
  WebsocketStatusService,
 | 
			
		||||
} from 'src/app/services/websocket-status.service'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
 | 
			
		||||
import { StatisticsWidgetComponent } from './statistics-widget.component'
 | 
			
		||||
@@ -23,7 +23,7 @@ describe('StatisticsWidgetComponent', () => {
 | 
			
		||||
  let component: StatisticsWidgetComponent
 | 
			
		||||
  let fixture: ComponentFixture<StatisticsWidgetComponent>
 | 
			
		||||
  let httpTestingController: HttpTestingController
 | 
			
		||||
  let consumerStatusService: ConsumerStatusService
 | 
			
		||||
  let websocketStatusService: WebsocketStatusService
 | 
			
		||||
  const fileStatusSubject = new Subject<FileStatus>()
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
@@ -44,9 +44,9 @@ describe('StatisticsWidgetComponent', () => {
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(StatisticsWidgetComponent)
 | 
			
		||||
    consumerStatusService = TestBed.inject(ConsumerStatusService)
 | 
			
		||||
    websocketStatusService = TestBed.inject(WebsocketStatusService)
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
 | 
			
		||||
      .spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
 | 
			
		||||
      .mockReturnValue(fileStatusSubject)
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,8 @@ import { first, Subject, Subscription, takeUntil } from 'rxjs'
 | 
			
		||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
 | 
			
		||||
import { FILTER_HAS_TAGS_ANY } from 'src/app/data/filter-rule-type'
 | 
			
		||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
 | 
			
		||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
 | 
			
		||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
 | 
			
		||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
 | 
			
		||||
 | 
			
		||||
@@ -51,7 +51,7 @@ export class StatisticsWidgetComponent
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private http: HttpClient,
 | 
			
		||||
    private consumerStatusService: ConsumerStatusService,
 | 
			
		||||
    private websocketConnectionService: WebsocketStatusService,
 | 
			
		||||
    private documentListViewService: DocumentListViewService
 | 
			
		||||
  ) {
 | 
			
		||||
    super()
 | 
			
		||||
@@ -109,7 +109,7 @@ export class StatisticsWidgetComponent
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.reload()
 | 
			
		||||
    this.subscription = this.consumerStatusService
 | 
			
		||||
    this.subscription = this.websocketConnectionService
 | 
			
		||||
      .onDocumentConsumptionFinished()
 | 
			
		||||
      .subscribe(() => {
 | 
			
		||||
        this.reload()
 | 
			
		||||
 
 | 
			
		||||
@@ -12,13 +12,13 @@ import { NgbAlert, NgbCollapse } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { routes } from 'src/app/app-routing.module'
 | 
			
		||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
 | 
			
		||||
import {
 | 
			
		||||
  ConsumerStatusService,
 | 
			
		||||
  FileStatus,
 | 
			
		||||
  FileStatusPhase,
 | 
			
		||||
} from 'src/app/services/consumer-status.service'
 | 
			
		||||
import { PermissionsService } from 'src/app/services/permissions.service'
 | 
			
		||||
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
 | 
			
		||||
import {
 | 
			
		||||
  FileStatus,
 | 
			
		||||
  FileStatusPhase,
 | 
			
		||||
  WebsocketStatusService,
 | 
			
		||||
} from 'src/app/services/websocket-status.service'
 | 
			
		||||
import { UploadFileWidgetComponent } from './upload-file-widget.component'
 | 
			
		||||
 | 
			
		||||
const FAILED_STATUSES = [new FileStatus()]
 | 
			
		||||
@@ -42,7 +42,7 @@ const DEFAULT_STATUSES = [
 | 
			
		||||
describe('UploadFileWidgetComponent', () => {
 | 
			
		||||
  let component: UploadFileWidgetComponent
 | 
			
		||||
  let fixture: ComponentFixture<UploadFileWidgetComponent>
 | 
			
		||||
  let consumerStatusService: ConsumerStatusService
 | 
			
		||||
  let websocketStatusService: WebsocketStatusService
 | 
			
		||||
  let uploadDocumentsService: UploadDocumentsService
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
@@ -65,7 +65,7 @@ describe('UploadFileWidgetComponent', () => {
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    consumerStatusService = TestBed.inject(ConsumerStatusService)
 | 
			
		||||
    websocketStatusService = TestBed.inject(WebsocketStatusService)
 | 
			
		||||
    uploadDocumentsService = TestBed.inject(UploadDocumentsService)
 | 
			
		||||
    fixture = TestBed.createComponent(UploadFileWidgetComponent)
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
@@ -91,14 +91,14 @@ describe('UploadFileWidgetComponent', () => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should generate stats summary', () => {
 | 
			
		||||
    mockConsumerStatuses(consumerStatusService)
 | 
			
		||||
    mockConsumerStatuses(websocketStatusService)
 | 
			
		||||
    expect(component.getStatusSummary()).toEqual(
 | 
			
		||||
      'Processing: 6, Failed: 1, Added: 4'
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should report an upload progress summary', () => {
 | 
			
		||||
    mockConsumerStatuses(consumerStatusService)
 | 
			
		||||
    mockConsumerStatuses(websocketStatusService)
 | 
			
		||||
    expect(component.getTotalUploadProgress()).toEqual(0.75)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@@ -117,7 +117,7 @@ describe('UploadFileWidgetComponent', () => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should enforce a maximum number of alerts', () => {
 | 
			
		||||
    mockConsumerStatuses(consumerStatusService)
 | 
			
		||||
    mockConsumerStatuses(websocketStatusService)
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    // 5 total, 1 hidden
 | 
			
		||||
    expect(fixture.debugElement.queryAll(By.directive(NgbAlert))).toHaveLength(
 | 
			
		||||
@@ -131,19 +131,19 @@ describe('UploadFileWidgetComponent', () => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should allow dismissing an alert', () => {
 | 
			
		||||
    const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
 | 
			
		||||
    const dismissSpy = jest.spyOn(websocketStatusService, 'dismiss')
 | 
			
		||||
    component.dismiss(new FileStatus())
 | 
			
		||||
    expect(dismissSpy).toHaveBeenCalled()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should allow dismissing completed alerts', fakeAsync(() => {
 | 
			
		||||
    mockConsumerStatuses(consumerStatusService)
 | 
			
		||||
    mockConsumerStatuses(websocketStatusService)
 | 
			
		||||
    component.alertsExpanded = true
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(component, 'getStatusCompleted')
 | 
			
		||||
      .mockImplementation(() => SUCCESS_STATUSES)
 | 
			
		||||
    const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
 | 
			
		||||
    const dismissSpy = jest.spyOn(websocketStatusService, 'dismiss')
 | 
			
		||||
    component.dismissCompleted()
 | 
			
		||||
    tick(1000)
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
 
 | 
			
		||||
@@ -12,13 +12,13 @@ import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
 | 
			
		||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
 | 
			
		||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
 | 
			
		||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
 | 
			
		||||
import {
 | 
			
		||||
  ConsumerStatusService,
 | 
			
		||||
  FileStatus,
 | 
			
		||||
  FileStatusPhase,
 | 
			
		||||
} from 'src/app/services/consumer-status.service'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
 | 
			
		||||
import {
 | 
			
		||||
  FileStatus,
 | 
			
		||||
  FileStatusPhase,
 | 
			
		||||
  WebsocketStatusService,
 | 
			
		||||
} from 'src/app/services/websocket-status.service'
 | 
			
		||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
 | 
			
		||||
 | 
			
		||||
const MAX_ALERTS = 5
 | 
			
		||||
@@ -46,7 +46,7 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
 | 
			
		||||
  @ViewChildren(NgbAlert) alerts: QueryList<NgbAlert>
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private consumerStatusService: ConsumerStatusService,
 | 
			
		||||
    private websocketStatusService: WebsocketStatusService,
 | 
			
		||||
    private uploadDocumentsService: UploadDocumentsService,
 | 
			
		||||
    public settingsService: SettingsService
 | 
			
		||||
  ) {
 | 
			
		||||
@@ -54,13 +54,13 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStatus() {
 | 
			
		||||
    return this.consumerStatusService.getConsumerStatus().slice(0, MAX_ALERTS)
 | 
			
		||||
    return this.websocketStatusService.getConsumerStatus().slice(0, MAX_ALERTS)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStatusSummary() {
 | 
			
		||||
    let strings = []
 | 
			
		||||
    let countUploadingAndProcessing =
 | 
			
		||||
      this.consumerStatusService.getConsumerStatusNotCompleted().length
 | 
			
		||||
      this.websocketStatusService.getConsumerStatusNotCompleted().length
 | 
			
		||||
    let countFailed = this.getStatusFailed().length
 | 
			
		||||
    let countSuccess = this.getStatusSuccess().length
 | 
			
		||||
    if (countUploadingAndProcessing > 0) {
 | 
			
		||||
@@ -78,27 +78,30 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStatusHidden() {
 | 
			
		||||
    if (this.consumerStatusService.getConsumerStatus().length < MAX_ALERTS)
 | 
			
		||||
    if (this.websocketStatusService.getConsumerStatus().length < MAX_ALERTS)
 | 
			
		||||
      return []
 | 
			
		||||
    else return this.consumerStatusService.getConsumerStatus().slice(MAX_ALERTS)
 | 
			
		||||
    else
 | 
			
		||||
      return this.websocketStatusService.getConsumerStatus().slice(MAX_ALERTS)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStatusUploading() {
 | 
			
		||||
    return this.consumerStatusService.getConsumerStatus(
 | 
			
		||||
    return this.websocketStatusService.getConsumerStatus(
 | 
			
		||||
      FileStatusPhase.UPLOADING
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStatusFailed() {
 | 
			
		||||
    return this.consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
 | 
			
		||||
    return this.websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStatusSuccess() {
 | 
			
		||||
    return this.consumerStatusService.getConsumerStatus(FileStatusPhase.SUCCESS)
 | 
			
		||||
    return this.websocketStatusService.getConsumerStatus(
 | 
			
		||||
      FileStatusPhase.SUCCESS
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStatusCompleted() {
 | 
			
		||||
    return this.consumerStatusService.getConsumerStatusCompleted()
 | 
			
		||||
    return this.websocketStatusService.getConsumerStatusCompleted()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTotalUploadProgress() {
 | 
			
		||||
@@ -134,12 +137,12 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  dismiss(status: FileStatus) {
 | 
			
		||||
    this.consumerStatusService.dismiss(status)
 | 
			
		||||
    this.websocketStatusService.dismiss(status)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  dismissCompleted() {
 | 
			
		||||
    this.getStatusCompleted().forEach((status) =>
 | 
			
		||||
      this.consumerStatusService.dismiss(status)
 | 
			
		||||
      this.websocketStatusService.dismiss(status)
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1039,6 +1039,7 @@ describe('BulkEditorComponent', () => {
 | 
			
		||||
    httpTestingController.match(
 | 
			
		||||
      `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
 | 
			
		||||
    ) // listAllFilteredIds
 | 
			
		||||
    expect(documentListViewService.selected.size).toEqual(0)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support bulk download with archive, originals or both and file formatting', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -268,6 +268,9 @@ export class BulkEditorComponent
 | 
			
		||||
      .pipe(first())
 | 
			
		||||
      .subscribe({
 | 
			
		||||
        next: () => {
 | 
			
		||||
          if (args['delete_originals']) {
 | 
			
		||||
            this.list.selected.clear()
 | 
			
		||||
          }
 | 
			
		||||
          this.list.reload()
 | 
			
		||||
          this.list.reduceSelectionToFilter()
 | 
			
		||||
          this.list.selected.forEach((id) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -38,16 +38,16 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
 | 
			
		||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
 | 
			
		||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
 | 
			
		||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
 | 
			
		||||
import {
 | 
			
		||||
  ConsumerStatusService,
 | 
			
		||||
  FileStatus,
 | 
			
		||||
} from 'src/app/services/consumer-status.service'
 | 
			
		||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
 | 
			
		||||
import { PermissionsService } from 'src/app/services/permissions.service'
 | 
			
		||||
import { DocumentService } from 'src/app/services/rest/document.service'
 | 
			
		||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
import {
 | 
			
		||||
  FileStatus,
 | 
			
		||||
  WebsocketStatusService,
 | 
			
		||||
} from 'src/app/services/websocket-status.service'
 | 
			
		||||
import { DocumentCardLargeComponent } from './document-card-large/document-card-large.component'
 | 
			
		||||
import { DocumentCardSmallComponent } from './document-card-small/document-card-small.component'
 | 
			
		||||
import { DocumentListComponent } from './document-list.component'
 | 
			
		||||
@@ -81,7 +81,7 @@ describe('DocumentListComponent', () => {
 | 
			
		||||
  let fixture: ComponentFixture<DocumentListComponent>
 | 
			
		||||
  let documentListService: DocumentListViewService
 | 
			
		||||
  let documentService: DocumentService
 | 
			
		||||
  let consumerStatusService: ConsumerStatusService
 | 
			
		||||
  let websocketStatusService: WebsocketStatusService
 | 
			
		||||
  let savedViewService: SavedViewService
 | 
			
		||||
  let router: Router
 | 
			
		||||
  let activatedRoute: ActivatedRoute
 | 
			
		||||
@@ -112,7 +112,7 @@ describe('DocumentListComponent', () => {
 | 
			
		||||
 | 
			
		||||
    documentListService = TestBed.inject(DocumentListViewService)
 | 
			
		||||
    documentService = TestBed.inject(DocumentService)
 | 
			
		||||
    consumerStatusService = TestBed.inject(ConsumerStatusService)
 | 
			
		||||
    websocketStatusService = TestBed.inject(WebsocketStatusService)
 | 
			
		||||
    savedViewService = TestBed.inject(SavedViewService)
 | 
			
		||||
    router = TestBed.inject(Router)
 | 
			
		||||
    activatedRoute = TestBed.inject(ActivatedRoute)
 | 
			
		||||
@@ -128,13 +128,24 @@ describe('DocumentListComponent', () => {
 | 
			
		||||
    const reloadSpy = jest.spyOn(documentListService, 'reload')
 | 
			
		||||
    const fileStatusSubject = new Subject<FileStatus>()
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
 | 
			
		||||
      .spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
 | 
			
		||||
      .mockReturnValue(fileStatusSubject)
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    fileStatusSubject.next(new FileStatus())
 | 
			
		||||
    expect(reloadSpy).toHaveBeenCalled()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should reload on document deleted', () => {
 | 
			
		||||
    const reloadSpy = jest.spyOn(documentListService, 'reload')
 | 
			
		||||
    const documentDeletedSubject = new Subject<boolean>()
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(websocketStatusService, 'onDocumentDeleted')
 | 
			
		||||
      .mockReturnValue(documentDeletedSubject)
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    documentDeletedSubject.next(true)
 | 
			
		||||
    expect(reloadSpy).toHaveBeenCalled()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should show score sort fields on fulltext queries', () => {
 | 
			
		||||
    documentListService.filterRules = [
 | 
			
		||||
      {
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,6 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
 | 
			
		||||
import { DocumentTypeNamePipe } from 'src/app/pipes/document-type-name.pipe'
 | 
			
		||||
import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe'
 | 
			
		||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
 | 
			
		||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
 | 
			
		||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
 | 
			
		||||
import { HotKeyService } from 'src/app/services/hot-key.service'
 | 
			
		||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
 | 
			
		||||
@@ -51,6 +50,7 @@ import { PermissionsService } from 'src/app/services/permissions.service'
 | 
			
		||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
 | 
			
		||||
import {
 | 
			
		||||
  filterRulesDiffer,
 | 
			
		||||
  isFullTextFilterRule,
 | 
			
		||||
@@ -113,7 +113,7 @@ export class DocumentListComponent
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private toastService: ToastService,
 | 
			
		||||
    private modalService: NgbModal,
 | 
			
		||||
    private consumerStatusService: ConsumerStatusService,
 | 
			
		||||
    private websocketStatusService: WebsocketStatusService,
 | 
			
		||||
    public openDocumentsService: OpenDocumentsService,
 | 
			
		||||
    public settingsService: SettingsService,
 | 
			
		||||
    private hotKeyService: HotKeyService,
 | 
			
		||||
@@ -234,13 +234,17 @@ export class DocumentListComponent
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.consumerStatusService
 | 
			
		||||
    this.websocketStatusService
 | 
			
		||||
      .onDocumentConsumptionFinished()
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .subscribe(() => {
 | 
			
		||||
        this.list.reload()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    this.websocketStatusService.onDocumentDeleted().subscribe(() => {
 | 
			
		||||
      this.list.reload()
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.route.paramMap
 | 
			
		||||
      .pipe(
 | 
			
		||||
        filter((params) => params.has('id')), // only on saved view e.g. /view/id
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
export interface WebsocketDocumentsDeletedMessage {
 | 
			
		||||
  documents: number[]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
export interface WebsocketConsumerStatusMessage {
 | 
			
		||||
export interface WebsocketProgressMessage {
 | 
			
		||||
  filename?: string
 | 
			
		||||
  task_id?: string
 | 
			
		||||
  current_progress?: number
 | 
			
		||||
@@ -1,326 +0,0 @@
 | 
			
		||||
import {
 | 
			
		||||
  HttpEventType,
 | 
			
		||||
  HttpResponse,
 | 
			
		||||
  provideHttpClient,
 | 
			
		||||
  withInterceptorsFromDi,
 | 
			
		||||
} from '@angular/common/http'
 | 
			
		||||
import {
 | 
			
		||||
  HttpTestingController,
 | 
			
		||||
  provideHttpClientTesting,
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import { TestBed } from '@angular/core/testing'
 | 
			
		||||
import WS from 'jest-websocket-mock'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
import {
 | 
			
		||||
  ConsumerStatusService,
 | 
			
		||||
  FILE_STATUS_MESSAGES,
 | 
			
		||||
  FileStatusPhase,
 | 
			
		||||
} from './consumer-status.service'
 | 
			
		||||
import { DocumentService } from './rest/document.service'
 | 
			
		||||
import { SettingsService } from './settings.service'
 | 
			
		||||
 | 
			
		||||
describe('ConsumerStatusService', () => {
 | 
			
		||||
  let httpTestingController: HttpTestingController
 | 
			
		||||
  let consumerStatusService: ConsumerStatusService
 | 
			
		||||
  let documentService: DocumentService
 | 
			
		||||
  let settingsService: SettingsService
 | 
			
		||||
 | 
			
		||||
  const server = new WS(
 | 
			
		||||
    `${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`,
 | 
			
		||||
    { jsonProtocol: true }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        ConsumerStatusService,
 | 
			
		||||
        DocumentService,
 | 
			
		||||
        SettingsService,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    httpTestingController = TestBed.inject(HttpTestingController)
 | 
			
		||||
    settingsService = TestBed.inject(SettingsService)
 | 
			
		||||
    settingsService.currentUser = {
 | 
			
		||||
      id: 1,
 | 
			
		||||
      username: 'testuser',
 | 
			
		||||
      is_superuser: false,
 | 
			
		||||
    }
 | 
			
		||||
    consumerStatusService = TestBed.inject(ConsumerStatusService)
 | 
			
		||||
    documentService = TestBed.inject(DocumentService)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  afterEach(() => {
 | 
			
		||||
    httpTestingController.verify()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should update status on websocket processing progress', () => {
 | 
			
		||||
    const task_id = '1234'
 | 
			
		||||
    const status = consumerStatusService.newFileUpload('file.pdf')
 | 
			
		||||
    expect(status.getProgress()).toEqual(0)
 | 
			
		||||
 | 
			
		||||
    consumerStatusService.connect()
 | 
			
		||||
 | 
			
		||||
    consumerStatusService
 | 
			
		||||
      .onDocumentConsumptionFinished()
 | 
			
		||||
      .subscribe((filestatus) => {
 | 
			
		||||
        expect(filestatus.phase).toEqual(FileStatusPhase.SUCCESS)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    consumerStatusService.onDocumentDetected().subscribe((filestatus) => {
 | 
			
		||||
      expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    server.send({
 | 
			
		||||
      task_id,
 | 
			
		||||
      filename: 'file.pdf',
 | 
			
		||||
      current_progress: 50,
 | 
			
		||||
      max_progress: 100,
 | 
			
		||||
      document_id: 12,
 | 
			
		||||
      status: 'WORKING',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(status.getProgress()).toBeCloseTo(0.6) // (0.8 * 50/100) + .2
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
 | 
			
		||||
      status,
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    server.send({
 | 
			
		||||
      task_id,
 | 
			
		||||
      filename: 'file.pdf',
 | 
			
		||||
      current_progress: 100,
 | 
			
		||||
      max_progress: 100,
 | 
			
		||||
      document_id: 12,
 | 
			
		||||
      status: 'SUCCESS',
 | 
			
		||||
      message: FILE_STATUS_MESSAGES.finished,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(status.getProgress()).toEqual(1)
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      0
 | 
			
		||||
    )
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
 | 
			
		||||
 | 
			
		||||
    consumerStatusService.disconnect()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should update status on websocket failed progress', () => {
 | 
			
		||||
    const task_id = '1234'
 | 
			
		||||
    const status = consumerStatusService.newFileUpload('file.pdf')
 | 
			
		||||
    status.taskId = task_id
 | 
			
		||||
    consumerStatusService.connect()
 | 
			
		||||
 | 
			
		||||
    consumerStatusService
 | 
			
		||||
      .onDocumentConsumptionFailed()
 | 
			
		||||
      .subscribe((filestatus) => {
 | 
			
		||||
        expect(filestatus.phase).toEqual(FileStatusPhase.FAILED)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    server.send({
 | 
			
		||||
      task_id,
 | 
			
		||||
      filename: 'file.pdf',
 | 
			
		||||
      current_progress: 50,
 | 
			
		||||
      max_progress: 100,
 | 
			
		||||
      document_id: 12,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
 | 
			
		||||
      status,
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    server.send({
 | 
			
		||||
      task_id,
 | 
			
		||||
      filename: 'file.pdf',
 | 
			
		||||
      current_progress: 50,
 | 
			
		||||
      max_progress: 100,
 | 
			
		||||
      document_id: 12,
 | 
			
		||||
      status: 'FAILED',
 | 
			
		||||
      message: FILE_STATUS_MESSAGES.document_already_exists,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      0
 | 
			
		||||
    )
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should update status on upload progress', () => {
 | 
			
		||||
    const task_id = '1234'
 | 
			
		||||
    const status = consumerStatusService.newFileUpload('file.pdf')
 | 
			
		||||
 | 
			
		||||
    documentService.uploadDocument({}).subscribe((event) => {
 | 
			
		||||
      if (event.type === HttpEventType.Response) {
 | 
			
		||||
        status.taskId = event.body['task_id']
 | 
			
		||||
        status.message = $localize`Upload complete, waiting...`
 | 
			
		||||
      } else if (event.type === HttpEventType.UploadProgress) {
 | 
			
		||||
        status.updateProgress(
 | 
			
		||||
          FileStatusPhase.UPLOADING,
 | 
			
		||||
          event.loaded,
 | 
			
		||||
          event.total
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const req = httpTestingController.expectOne(
 | 
			
		||||
      `${environment.apiBaseUrl}documents/post_document/`
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    req.event(
 | 
			
		||||
      new HttpResponse({
 | 
			
		||||
        body: {
 | 
			
		||||
          task_id,
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    req.event({
 | 
			
		||||
      type: HttpEventType.UploadProgress,
 | 
			
		||||
      loaded: 100,
 | 
			
		||||
      total: 300,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
 | 
			
		||||
    ).toEqual([status])
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatus()).toEqual([status])
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
 | 
			
		||||
      status,
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    req.event({
 | 
			
		||||
      type: HttpEventType.UploadProgress,
 | 
			
		||||
      loaded: 300,
 | 
			
		||||
      total: 300,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(status.getProgress()).toEqual(0.2) // 0.2 * 300/300
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support dismiss completed', () => {
 | 
			
		||||
    consumerStatusService.connect()
 | 
			
		||||
    server.send({
 | 
			
		||||
      task_id: '1234',
 | 
			
		||||
      filename: 'file.pdf',
 | 
			
		||||
      current_progress: 100,
 | 
			
		||||
      max_progress: 100,
 | 
			
		||||
      document_id: 12,
 | 
			
		||||
      status: 'SUCCESS',
 | 
			
		||||
      message: 'finished',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
 | 
			
		||||
    consumerStatusService.dismissCompleted()
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(0)
 | 
			
		||||
    consumerStatusService.disconnect()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support dismiss', () => {
 | 
			
		||||
    const task_id = '1234'
 | 
			
		||||
    const status = consumerStatusService.newFileUpload('file.pdf')
 | 
			
		||||
    status.taskId = task_id
 | 
			
		||||
    status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
 | 
			
		||||
 | 
			
		||||
    const status2 = consumerStatusService.newFileUpload('file2.pdf')
 | 
			
		||||
    status2.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
 | 
			
		||||
    ).toEqual([status, status2])
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatus()).toEqual([status, status2])
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
 | 
			
		||||
      status,
 | 
			
		||||
      status2,
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    consumerStatusService.dismiss(status)
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatus()).toEqual([status2])
 | 
			
		||||
 | 
			
		||||
    consumerStatusService.dismiss(status2)
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatus()).toHaveLength(0)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support fail', () => {
 | 
			
		||||
    const task_id = '1234'
 | 
			
		||||
    const status = consumerStatusService.newFileUpload('file.pdf')
 | 
			
		||||
    status.taskId = task_id
 | 
			
		||||
    status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      1
 | 
			
		||||
    )
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(0)
 | 
			
		||||
    consumerStatusService.fail(status, 'fail')
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      0
 | 
			
		||||
    )
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should notify of document created on status message without upload', () => {
 | 
			
		||||
    let detected = false
 | 
			
		||||
    consumerStatusService.onDocumentDetected().subscribe((filestatus) => {
 | 
			
		||||
      expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
 | 
			
		||||
      detected = true
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    consumerStatusService.connect()
 | 
			
		||||
    server.send({
 | 
			
		||||
      task_id: '1234',
 | 
			
		||||
      filename: 'file.pdf',
 | 
			
		||||
      current_progress: 0,
 | 
			
		||||
      max_progress: 100,
 | 
			
		||||
      message: 'new_file',
 | 
			
		||||
      status: 'STARTED',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    consumerStatusService.disconnect()
 | 
			
		||||
    expect(detected).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should notify of document in progress without upload', () => {
 | 
			
		||||
    consumerStatusService.connect()
 | 
			
		||||
    server.send({
 | 
			
		||||
      task_id: '1234',
 | 
			
		||||
      filename: 'file.pdf',
 | 
			
		||||
      current_progress: 50,
 | 
			
		||||
      max_progress: 100,
 | 
			
		||||
      docuement_id: 12,
 | 
			
		||||
      status: 'WORKING',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    consumerStatusService.disconnect()
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      1
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should not notify current user if document has different expected owner', () => {
 | 
			
		||||
    consumerStatusService.connect()
 | 
			
		||||
    server.send({
 | 
			
		||||
      task_id: '1234',
 | 
			
		||||
      filename: 'file1.pdf',
 | 
			
		||||
      current_progress: 50,
 | 
			
		||||
      max_progress: 100,
 | 
			
		||||
      docuement_id: 12,
 | 
			
		||||
      owner_id: 1,
 | 
			
		||||
      status: 'WORKING',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    server.send({
 | 
			
		||||
      task_id: '5678',
 | 
			
		||||
      filename: 'file2.pdf',
 | 
			
		||||
      current_progress: 50,
 | 
			
		||||
      max_progress: 100,
 | 
			
		||||
      docuement_id: 13,
 | 
			
		||||
      owner_id: 2,
 | 
			
		||||
      status: 'WORKING',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    consumerStatusService.disconnect()
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      1
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -9,11 +9,11 @@ import {
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import { TestBed } from '@angular/core/testing'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
import {
 | 
			
		||||
  ConsumerStatusService,
 | 
			
		||||
  FileStatusPhase,
 | 
			
		||||
} from './consumer-status.service'
 | 
			
		||||
import { UploadDocumentsService } from './upload-documents.service'
 | 
			
		||||
import {
 | 
			
		||||
  FileStatusPhase,
 | 
			
		||||
  WebsocketStatusService,
 | 
			
		||||
} from './websocket-status.service'
 | 
			
		||||
 | 
			
		||||
const files = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -45,14 +45,14 @@ const fileList = {
 | 
			
		||||
describe('UploadDocumentsService', () => {
 | 
			
		||||
  let httpTestingController: HttpTestingController
 | 
			
		||||
  let uploadDocumentsService: UploadDocumentsService
 | 
			
		||||
  let consumerStatusService: ConsumerStatusService
 | 
			
		||||
  let websocketStatusService: WebsocketStatusService
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        UploadDocumentsService,
 | 
			
		||||
        ConsumerStatusService,
 | 
			
		||||
        WebsocketStatusService,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
@@ -60,7 +60,7 @@ describe('UploadDocumentsService', () => {
 | 
			
		||||
 | 
			
		||||
    httpTestingController = TestBed.inject(HttpTestingController)
 | 
			
		||||
    uploadDocumentsService = TestBed.inject(UploadDocumentsService)
 | 
			
		||||
    consumerStatusService = TestBed.inject(ConsumerStatusService)
 | 
			
		||||
    websocketStatusService = TestBed.inject(WebsocketStatusService)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  afterEach(() => {
 | 
			
		||||
@@ -80,11 +80,11 @@ describe('UploadDocumentsService', () => {
 | 
			
		||||
  it('updates progress during upload and failure', () => {
 | 
			
		||||
    uploadDocumentsService.uploadFiles(fileList)
 | 
			
		||||
 | 
			
		||||
    expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      2
 | 
			
		||||
    )
 | 
			
		||||
    expect(
 | 
			
		||||
      consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
 | 
			
		||||
      websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
 | 
			
		||||
    ).toHaveLength(0)
 | 
			
		||||
 | 
			
		||||
    const req = httpTestingController.match(
 | 
			
		||||
@@ -98,7 +98,7 @@ describe('UploadDocumentsService', () => {
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
 | 
			
		||||
      websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
 | 
			
		||||
    ).toHaveLength(1)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@@ -110,7 +110,7 @@ describe('UploadDocumentsService', () => {
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
 | 
			
		||||
      websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
 | 
			
		||||
    ).toHaveLength(0)
 | 
			
		||||
 | 
			
		||||
    req[0].flush(
 | 
			
		||||
@@ -122,7 +122,7 @@ describe('UploadDocumentsService', () => {
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
 | 
			
		||||
      websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
 | 
			
		||||
    ).toHaveLength(1)
 | 
			
		||||
 | 
			
		||||
    uploadDocumentsService.uploadFiles(fileList)
 | 
			
		||||
@@ -140,7 +140,7 @@ describe('UploadDocumentsService', () => {
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
 | 
			
		||||
      websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
 | 
			
		||||
    ).toHaveLength(2)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,11 @@ import { HttpEventType } from '@angular/common/http'
 | 
			
		||||
import { Injectable } from '@angular/core'
 | 
			
		||||
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'
 | 
			
		||||
import { Subscription } from 'rxjs'
 | 
			
		||||
import {
 | 
			
		||||
  ConsumerStatusService,
 | 
			
		||||
  FileStatusPhase,
 | 
			
		||||
} from './consumer-status.service'
 | 
			
		||||
import { DocumentService } from './rest/document.service'
 | 
			
		||||
import {
 | 
			
		||||
  FileStatusPhase,
 | 
			
		||||
  WebsocketStatusService,
 | 
			
		||||
} from './websocket-status.service'
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root',
 | 
			
		||||
@@ -16,7 +16,7 @@ export class UploadDocumentsService {
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private documentService: DocumentService,
 | 
			
		||||
    private consumerStatusService: ConsumerStatusService
 | 
			
		||||
    private websocketStatusService: WebsocketStatusService
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  onNgxFileDrop(files: NgxFileDropEntry[]) {
 | 
			
		||||
@@ -37,7 +37,7 @@ export class UploadDocumentsService {
 | 
			
		||||
  private uploadFile(file: File) {
 | 
			
		||||
    let formData = new FormData()
 | 
			
		||||
    formData.append('document', file, file.name)
 | 
			
		||||
    let status = this.consumerStatusService.newFileUpload(file.name)
 | 
			
		||||
    let status = this.websocketStatusService.newFileUpload(file.name)
 | 
			
		||||
 | 
			
		||||
    status.message = $localize`Connecting...`
 | 
			
		||||
 | 
			
		||||
@@ -61,11 +61,11 @@ export class UploadDocumentsService {
 | 
			
		||||
        error: (error) => {
 | 
			
		||||
          switch (error.status) {
 | 
			
		||||
            case 400: {
 | 
			
		||||
              this.consumerStatusService.fail(status, error.error.document)
 | 
			
		||||
              this.websocketStatusService.fail(status, error.error.document)
 | 
			
		||||
              break
 | 
			
		||||
            }
 | 
			
		||||
            default: {
 | 
			
		||||
              this.consumerStatusService.fail(
 | 
			
		||||
              this.websocketStatusService.fail(
 | 
			
		||||
                status,
 | 
			
		||||
                $localize`HTTP error: ${error.status} ${error.statusText}`
 | 
			
		||||
              )
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										375
									
								
								src-ui/src/app/services/websocket-status.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										375
									
								
								src-ui/src/app/services/websocket-status.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,375 @@
 | 
			
		||||
import {
 | 
			
		||||
  HttpEventType,
 | 
			
		||||
  HttpResponse,
 | 
			
		||||
  provideHttpClient,
 | 
			
		||||
  withInterceptorsFromDi,
 | 
			
		||||
} from '@angular/common/http'
 | 
			
		||||
import {
 | 
			
		||||
  HttpTestingController,
 | 
			
		||||
  provideHttpClientTesting,
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import { TestBed } from '@angular/core/testing'
 | 
			
		||||
import WS from 'jest-websocket-mock'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
import { DocumentService } from './rest/document.service'
 | 
			
		||||
import { SettingsService } from './settings.service'
 | 
			
		||||
import {
 | 
			
		||||
  FILE_STATUS_MESSAGES,
 | 
			
		||||
  FileStatusPhase,
 | 
			
		||||
  WebsocketStatusService,
 | 
			
		||||
  WebsocketStatusType,
 | 
			
		||||
} from './websocket-status.service'
 | 
			
		||||
 | 
			
		||||
describe('ConsumerStatusService', () => {
 | 
			
		||||
  let httpTestingController: HttpTestingController
 | 
			
		||||
  let websocketStatusService: WebsocketStatusService
 | 
			
		||||
  let documentService: DocumentService
 | 
			
		||||
  let settingsService: SettingsService
 | 
			
		||||
 | 
			
		||||
  const server = new WS(
 | 
			
		||||
    `${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`,
 | 
			
		||||
    { jsonProtocol: true }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        WebsocketStatusService,
 | 
			
		||||
        DocumentService,
 | 
			
		||||
        SettingsService,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    httpTestingController = TestBed.inject(HttpTestingController)
 | 
			
		||||
    settingsService = TestBed.inject(SettingsService)
 | 
			
		||||
    settingsService.currentUser = {
 | 
			
		||||
      id: 1,
 | 
			
		||||
      username: 'testuser',
 | 
			
		||||
      is_superuser: false,
 | 
			
		||||
    }
 | 
			
		||||
    websocketStatusService = TestBed.inject(WebsocketStatusService)
 | 
			
		||||
    documentService = TestBed.inject(DocumentService)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  afterEach(() => {
 | 
			
		||||
    httpTestingController.verify()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should update status on websocket processing progress', () => {
 | 
			
		||||
    const task_id = '1234'
 | 
			
		||||
    const status = websocketStatusService.newFileUpload('file.pdf')
 | 
			
		||||
    expect(status.getProgress()).toEqual(0)
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.connect()
 | 
			
		||||
 | 
			
		||||
    websocketStatusService
 | 
			
		||||
      .onDocumentConsumptionFinished()
 | 
			
		||||
      .subscribe((filestatus) => {
 | 
			
		||||
        expect(filestatus.phase).toEqual(FileStatusPhase.SUCCESS)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.onDocumentDetected().subscribe((filestatus) => {
 | 
			
		||||
      expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    server.send({
 | 
			
		||||
      type: WebsocketStatusType.STATUS_UPDATE,
 | 
			
		||||
      data: {
 | 
			
		||||
        task_id,
 | 
			
		||||
        filename: 'file.pdf',
 | 
			
		||||
        current_progress: 50,
 | 
			
		||||
        max_progress: 100,
 | 
			
		||||
        document_id: 12,
 | 
			
		||||
        status: 'WORKING',
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(status.getProgress()).toBeCloseTo(0.6) // (0.8 * 50/100) + .2
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([
 | 
			
		||||
      status,
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    server.send({
 | 
			
		||||
      type: WebsocketStatusType.STATUS_UPDATE,
 | 
			
		||||
      data: {
 | 
			
		||||
        task_id,
 | 
			
		||||
        filename: 'file.pdf',
 | 
			
		||||
        current_progress: 100,
 | 
			
		||||
        max_progress: 100,
 | 
			
		||||
        document_id: 12,
 | 
			
		||||
        status: 'SUCCESS',
 | 
			
		||||
        message: FILE_STATUS_MESSAGES.finished,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(status.getProgress()).toEqual(1)
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      0
 | 
			
		||||
    )
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1)
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.disconnect()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should update status on websocket failed progress', () => {
 | 
			
		||||
    const task_id = '1234'
 | 
			
		||||
    const status = websocketStatusService.newFileUpload('file.pdf')
 | 
			
		||||
    status.taskId = task_id
 | 
			
		||||
    websocketStatusService.connect()
 | 
			
		||||
 | 
			
		||||
    websocketStatusService
 | 
			
		||||
      .onDocumentConsumptionFailed()
 | 
			
		||||
      .subscribe((filestatus) => {
 | 
			
		||||
        expect(filestatus.phase).toEqual(FileStatusPhase.FAILED)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    server.send({
 | 
			
		||||
      type: WebsocketStatusType.STATUS_UPDATE,
 | 
			
		||||
      data: {
 | 
			
		||||
        task_id,
 | 
			
		||||
        filename: 'file.pdf',
 | 
			
		||||
        current_progress: 50,
 | 
			
		||||
        max_progress: 100,
 | 
			
		||||
        document_id: 12,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([
 | 
			
		||||
      status,
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    server.send({
 | 
			
		||||
      type: WebsocketStatusType.STATUS_UPDATE,
 | 
			
		||||
      data: {
 | 
			
		||||
        task_id,
 | 
			
		||||
        filename: 'file.pdf',
 | 
			
		||||
        current_progress: 50,
 | 
			
		||||
        max_progress: 100,
 | 
			
		||||
        document_id: 12,
 | 
			
		||||
        status: 'FAILED',
 | 
			
		||||
        message: FILE_STATUS_MESSAGES.document_already_exists,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      0
 | 
			
		||||
    )
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should update status on upload progress', () => {
 | 
			
		||||
    const task_id = '1234'
 | 
			
		||||
    const status = websocketStatusService.newFileUpload('file.pdf')
 | 
			
		||||
 | 
			
		||||
    documentService.uploadDocument({}).subscribe((event) => {
 | 
			
		||||
      if (event.type === HttpEventType.Response) {
 | 
			
		||||
        status.taskId = event.body['task_id']
 | 
			
		||||
        status.message = $localize`Upload complete, waiting...`
 | 
			
		||||
      } else if (event.type === HttpEventType.UploadProgress) {
 | 
			
		||||
        status.updateProgress(
 | 
			
		||||
          FileStatusPhase.UPLOADING,
 | 
			
		||||
          event.loaded,
 | 
			
		||||
          event.total
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const req = httpTestingController.expectOne(
 | 
			
		||||
      `${environment.apiBaseUrl}documents/post_document/`
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    req.event(
 | 
			
		||||
      new HttpResponse({
 | 
			
		||||
        body: {
 | 
			
		||||
          task_id,
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    req.event({
 | 
			
		||||
      type: HttpEventType.UploadProgress,
 | 
			
		||||
      loaded: 100,
 | 
			
		||||
      total: 300,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
 | 
			
		||||
    ).toEqual([status])
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatus()).toEqual([status])
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([
 | 
			
		||||
      status,
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    req.event({
 | 
			
		||||
      type: HttpEventType.UploadProgress,
 | 
			
		||||
      loaded: 300,
 | 
			
		||||
      total: 300,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(status.getProgress()).toEqual(0.2) // 0.2 * 300/300
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support dismiss completed', () => {
 | 
			
		||||
    websocketStatusService.connect()
 | 
			
		||||
    server.send({
 | 
			
		||||
      type: WebsocketStatusType.STATUS_UPDATE,
 | 
			
		||||
      data: {
 | 
			
		||||
        task_id: '1234',
 | 
			
		||||
        filename: 'file.pdf',
 | 
			
		||||
        current_progress: 100,
 | 
			
		||||
        max_progress: 100,
 | 
			
		||||
        document_id: 12,
 | 
			
		||||
        status: 'SUCCESS',
 | 
			
		||||
        message: 'finished',
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1)
 | 
			
		||||
    websocketStatusService.dismissCompleted()
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(0)
 | 
			
		||||
    websocketStatusService.disconnect()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support dismiss', () => {
 | 
			
		||||
    const task_id = '1234'
 | 
			
		||||
    const status = websocketStatusService.newFileUpload('file.pdf')
 | 
			
		||||
    status.taskId = task_id
 | 
			
		||||
    status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
 | 
			
		||||
 | 
			
		||||
    const status2 = websocketStatusService.newFileUpload('file2.pdf')
 | 
			
		||||
    status2.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
 | 
			
		||||
    ).toEqual([status, status2])
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatus()).toEqual([
 | 
			
		||||
      status,
 | 
			
		||||
      status2,
 | 
			
		||||
    ])
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([
 | 
			
		||||
      status,
 | 
			
		||||
      status2,
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.dismiss(status)
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatus()).toEqual([status2])
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.dismiss(status2)
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatus()).toHaveLength(0)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support fail', () => {
 | 
			
		||||
    const task_id = '1234'
 | 
			
		||||
    const status = websocketStatusService.newFileUpload('file.pdf')
 | 
			
		||||
    status.taskId = task_id
 | 
			
		||||
    status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      1
 | 
			
		||||
    )
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(0)
 | 
			
		||||
    websocketStatusService.fail(status, 'fail')
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      0
 | 
			
		||||
    )
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should notify of document created on status message without upload', () => {
 | 
			
		||||
    let detected = false
 | 
			
		||||
    websocketStatusService.onDocumentDetected().subscribe((filestatus) => {
 | 
			
		||||
      expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
 | 
			
		||||
      detected = true
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.connect()
 | 
			
		||||
    server.send({
 | 
			
		||||
      type: WebsocketStatusType.STATUS_UPDATE,
 | 
			
		||||
      data: {
 | 
			
		||||
        task_id: '1234',
 | 
			
		||||
        filename: 'file.pdf',
 | 
			
		||||
        current_progress: 0,
 | 
			
		||||
        max_progress: 100,
 | 
			
		||||
        message: 'new_file',
 | 
			
		||||
        status: 'STARTED',
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.disconnect()
 | 
			
		||||
    expect(detected).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should notify of document in progress without upload', () => {
 | 
			
		||||
    websocketStatusService.connect()
 | 
			
		||||
    server.send({
 | 
			
		||||
      type: WebsocketStatusType.STATUS_UPDATE,
 | 
			
		||||
      data: {
 | 
			
		||||
        task_id: '1234',
 | 
			
		||||
        filename: 'file.pdf',
 | 
			
		||||
        current_progress: 50,
 | 
			
		||||
        max_progress: 100,
 | 
			
		||||
        docuement_id: 12,
 | 
			
		||||
        status: 'WORKING',
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.disconnect()
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      1
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should not notify current user if document has different expected owner', () => {
 | 
			
		||||
    websocketStatusService.connect()
 | 
			
		||||
    server.send({
 | 
			
		||||
      type: WebsocketStatusType.STATUS_UPDATE,
 | 
			
		||||
      data: {
 | 
			
		||||
        task_id: '1234',
 | 
			
		||||
        filename: 'file1.pdf',
 | 
			
		||||
        current_progress: 50,
 | 
			
		||||
        max_progress: 100,
 | 
			
		||||
        docuement_id: 12,
 | 
			
		||||
        owner_id: 1,
 | 
			
		||||
        status: 'WORKING',
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    server.send({
 | 
			
		||||
      type: WebsocketStatusType.STATUS_UPDATE,
 | 
			
		||||
      data: {
 | 
			
		||||
        task_id: '5678',
 | 
			
		||||
        filename: 'file2.pdf',
 | 
			
		||||
        current_progress: 50,
 | 
			
		||||
        max_progress: 100,
 | 
			
		||||
        docuement_id: 13,
 | 
			
		||||
        owner_id: 2,
 | 
			
		||||
        status: 'WORKING',
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.disconnect()
 | 
			
		||||
    expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
 | 
			
		||||
      1
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should trigger deleted subject on document deleted', () => {
 | 
			
		||||
    let deleted = false
 | 
			
		||||
    websocketStatusService.onDocumentDeleted().subscribe(() => {
 | 
			
		||||
      deleted = true
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.connect()
 | 
			
		||||
    server.send({
 | 
			
		||||
      type: WebsocketStatusType.DOCUMENTS_DELETED,
 | 
			
		||||
      data: {
 | 
			
		||||
        documents: [1, 2, 3],
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    websocketStatusService.disconnect()
 | 
			
		||||
    expect(deleted).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,9 +1,15 @@
 | 
			
		||||
import { Injectable } from '@angular/core'
 | 
			
		||||
import { Subject } from 'rxjs'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
import { WebsocketConsumerStatusMessage } from '../data/websocket-consumer-status-message'
 | 
			
		||||
import { WebsocketDocumentsDeletedMessage } from '../data/websocket-documents-deleted-message'
 | 
			
		||||
import { WebsocketProgressMessage } from '../data/websocket-progress-message'
 | 
			
		||||
import { SettingsService } from './settings.service'
 | 
			
		||||
 | 
			
		||||
export enum WebsocketStatusType {
 | 
			
		||||
  STATUS_UPDATE = 'status_update',
 | 
			
		||||
  DOCUMENTS_DELETED = 'documents_deleted',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// see ProgressStatusOptions in src/documents/plugins/helpers.py
 | 
			
		||||
export enum FileStatusPhase {
 | 
			
		||||
  STARTED = 0,
 | 
			
		||||
@@ -85,7 +91,7 @@ export class FileStatus {
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root',
 | 
			
		||||
})
 | 
			
		||||
export class ConsumerStatusService {
 | 
			
		||||
export class WebsocketStatusService {
 | 
			
		||||
  constructor(private settingsService: SettingsService) {}
 | 
			
		||||
 | 
			
		||||
  private statusWebSocket: WebSocket
 | 
			
		||||
@@ -95,6 +101,7 @@ export class ConsumerStatusService {
 | 
			
		||||
  private documentDetectedSubject = new Subject<FileStatus>()
 | 
			
		||||
  private documentConsumptionFinishedSubject = new Subject<FileStatus>()
 | 
			
		||||
  private documentConsumptionFailedSubject = new Subject<FileStatus>()
 | 
			
		||||
  private documentDeletedSubject = new Subject<boolean>()
 | 
			
		||||
 | 
			
		||||
  private get(taskId: string, filename?: string) {
 | 
			
		||||
    let status =
 | 
			
		||||
@@ -145,63 +152,75 @@ export class ConsumerStatusService {
 | 
			
		||||
    this.statusWebSocket = new WebSocket(
 | 
			
		||||
      `${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`
 | 
			
		||||
    )
 | 
			
		||||
    this.statusWebSocket.onmessage = (ev) => {
 | 
			
		||||
      let statusMessage: WebsocketConsumerStatusMessage = JSON.parse(ev['data'])
 | 
			
		||||
    this.statusWebSocket.onmessage = (ev: MessageEvent) => {
 | 
			
		||||
      const {
 | 
			
		||||
        type,
 | 
			
		||||
        data: messageData,
 | 
			
		||||
      }: {
 | 
			
		||||
        type: WebsocketStatusType
 | 
			
		||||
        data: WebsocketProgressMessage | WebsocketDocumentsDeletedMessage
 | 
			
		||||
      } = JSON.parse(ev.data)
 | 
			
		||||
 | 
			
		||||
      // fallback if backend didn't restrict message
 | 
			
		||||
      if (
 | 
			
		||||
        statusMessage.owner_id &&
 | 
			
		||||
        statusMessage.owner_id !== this.settingsService.currentUser?.id &&
 | 
			
		||||
        !this.settingsService.currentUser?.is_superuser
 | 
			
		||||
      ) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let statusMessageGet = this.get(
 | 
			
		||||
        statusMessage.task_id,
 | 
			
		||||
        statusMessage.filename
 | 
			
		||||
      )
 | 
			
		||||
      let status = statusMessageGet.status
 | 
			
		||||
      let created = statusMessageGet.created
 | 
			
		||||
 | 
			
		||||
      status.updateProgress(
 | 
			
		||||
        FileStatusPhase.WORKING,
 | 
			
		||||
        statusMessage.current_progress,
 | 
			
		||||
        statusMessage.max_progress
 | 
			
		||||
      )
 | 
			
		||||
      if (
 | 
			
		||||
        statusMessage.message &&
 | 
			
		||||
        statusMessage.message in FILE_STATUS_MESSAGES
 | 
			
		||||
      ) {
 | 
			
		||||
        status.message = FILE_STATUS_MESSAGES[statusMessage.message]
 | 
			
		||||
      } else if (statusMessage.message) {
 | 
			
		||||
        status.message = statusMessage.message
 | 
			
		||||
      }
 | 
			
		||||
      status.documentId = statusMessage.document_id
 | 
			
		||||
 | 
			
		||||
      if (statusMessage.status in FileStatusPhase) {
 | 
			
		||||
        status.phase = FileStatusPhase[statusMessage.status]
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      switch (status.phase) {
 | 
			
		||||
        case FileStatusPhase.STARTED:
 | 
			
		||||
          if (created) this.documentDetectedSubject.next(status)
 | 
			
		||||
      switch (type) {
 | 
			
		||||
        case WebsocketStatusType.DOCUMENTS_DELETED:
 | 
			
		||||
          this.documentDeletedSubject.next(true)
 | 
			
		||||
          break
 | 
			
		||||
 | 
			
		||||
        case FileStatusPhase.SUCCESS:
 | 
			
		||||
          this.documentConsumptionFinishedSubject.next(status)
 | 
			
		||||
          break
 | 
			
		||||
 | 
			
		||||
        case FileStatusPhase.FAILED:
 | 
			
		||||
          this.documentConsumptionFailedSubject.next(status)
 | 
			
		||||
          break
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
        case WebsocketStatusType.STATUS_UPDATE:
 | 
			
		||||
          this.handleProgressUpdate(messageData as WebsocketProgressMessage)
 | 
			
		||||
          break
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleProgressUpdate(messageData: WebsocketProgressMessage) {
 | 
			
		||||
    // fallback if backend didn't restrict message
 | 
			
		||||
    if (
 | 
			
		||||
      messageData.owner_id &&
 | 
			
		||||
      messageData.owner_id !== this.settingsService.currentUser?.id &&
 | 
			
		||||
      !this.settingsService.currentUser?.is_superuser
 | 
			
		||||
    ) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let statusMessageGet = this.get(messageData.task_id, messageData.filename)
 | 
			
		||||
    let status = statusMessageGet.status
 | 
			
		||||
    let created = statusMessageGet.created
 | 
			
		||||
 | 
			
		||||
    status.updateProgress(
 | 
			
		||||
      FileStatusPhase.WORKING,
 | 
			
		||||
      messageData.current_progress,
 | 
			
		||||
      messageData.max_progress
 | 
			
		||||
    )
 | 
			
		||||
    if (messageData.message && messageData.message in FILE_STATUS_MESSAGES) {
 | 
			
		||||
      status.message = FILE_STATUS_MESSAGES[messageData.message]
 | 
			
		||||
    } else if (messageData.message) {
 | 
			
		||||
      status.message = messageData.message
 | 
			
		||||
    }
 | 
			
		||||
    status.documentId = messageData.document_id
 | 
			
		||||
 | 
			
		||||
    if (messageData.status in FileStatusPhase) {
 | 
			
		||||
      status.phase = FileStatusPhase[messageData.status]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    switch (status.phase) {
 | 
			
		||||
      case FileStatusPhase.STARTED:
 | 
			
		||||
        if (created) this.documentDetectedSubject.next(status)
 | 
			
		||||
        break
 | 
			
		||||
 | 
			
		||||
      case FileStatusPhase.SUCCESS:
 | 
			
		||||
        this.documentConsumptionFinishedSubject.next(status)
 | 
			
		||||
        break
 | 
			
		||||
 | 
			
		||||
      case FileStatusPhase.FAILED:
 | 
			
		||||
        this.documentConsumptionFailedSubject.next(status)
 | 
			
		||||
        break
 | 
			
		||||
 | 
			
		||||
      default:
 | 
			
		||||
        break
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fail(status: FileStatus, message: string) {
 | 
			
		||||
    status.message = message
 | 
			
		||||
    status.phase = FileStatusPhase.FAILED
 | 
			
		||||
@@ -250,4 +269,8 @@ export class ConsumerStatusService {
 | 
			
		||||
  onDocumentDetected() {
 | 
			
		||||
    return this.documentDetectedSubject
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onDocumentDeleted() {
 | 
			
		||||
    return this.documentDeletedSubject
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -24,6 +24,7 @@ from documents.models import Document
 | 
			
		||||
from documents.models import DocumentType
 | 
			
		||||
from documents.models import StoragePath
 | 
			
		||||
from documents.permissions import set_permissions_for_object
 | 
			
		||||
from documents.plugins.helpers import DocumentsStatusManager
 | 
			
		||||
from documents.tasks import bulk_update_documents
 | 
			
		||||
from documents.tasks import consume_file
 | 
			
		||||
from documents.tasks import update_document_content_maybe_archive_file
 | 
			
		||||
@@ -219,6 +220,9 @@ def delete(doc_ids: list[int]) -> Literal["OK"]:
 | 
			
		||||
        with index.open_index_writer() as writer:
 | 
			
		||||
            for id in doc_ids:
 | 
			
		||||
                index.remove_document_by_id(writer, id)
 | 
			
		||||
 | 
			
		||||
        status_mgr = DocumentsStatusManager()
 | 
			
		||||
        status_mgr.send_documents_deleted(doc_ids)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        if "Data too long for column" in str(e):
 | 
			
		||||
            logger.warning(
 | 
			
		||||
 
 | 
			
		||||
@@ -15,16 +15,14 @@ class ProgressStatusOptions(str, enum.Enum):
 | 
			
		||||
    FAILED = "FAILED"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgressManager:
 | 
			
		||||
class BaseStatusManager:
 | 
			
		||||
    """
 | 
			
		||||
    Handles sending of progress information via the channel layer, with proper management
 | 
			
		||||
    of the open/close of the layer to ensure messages go out and everything is cleaned up
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, filename: str, task_id: str | None = None) -> None:
 | 
			
		||||
        self.filename = filename
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        self._channel: RedisPubSubChannelLayer | None = None
 | 
			
		||||
        self.task_id = task_id
 | 
			
		||||
 | 
			
		||||
    def __enter__(self):
 | 
			
		||||
        self.open()
 | 
			
		||||
@@ -49,6 +47,24 @@ class ProgressManager:
 | 
			
		||||
            async_to_sync(self._channel.flush)
 | 
			
		||||
            self._channel = None
 | 
			
		||||
 | 
			
		||||
    def send(self, payload: dict[str, str | int | None]) -> None:
 | 
			
		||||
        # Ensure the layer is open
 | 
			
		||||
        self.open()
 | 
			
		||||
 | 
			
		||||
        # Just for IDEs
 | 
			
		||||
        if TYPE_CHECKING:
 | 
			
		||||
            assert self._channel is not None
 | 
			
		||||
 | 
			
		||||
        # Construct and send the update
 | 
			
		||||
        async_to_sync(self._channel.group_send)("status_updates", payload)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgressManager(BaseStatusManager):
 | 
			
		||||
    def __init__(self, filename: str | None = None, task_id: str | None = None) -> None:
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.filename = filename
 | 
			
		||||
        self.task_id = task_id
 | 
			
		||||
 | 
			
		||||
    def send_progress(
 | 
			
		||||
        self,
 | 
			
		||||
        status: ProgressStatusOptions,
 | 
			
		||||
@@ -57,13 +73,6 @@ class ProgressManager:
 | 
			
		||||
        max_progress: int,
 | 
			
		||||
        extra_args: dict[str, str | int | None] | None = None,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        # Ensure the layer is open
 | 
			
		||||
        self.open()
 | 
			
		||||
 | 
			
		||||
        # Just for IDEs
 | 
			
		||||
        if TYPE_CHECKING:
 | 
			
		||||
            assert self._channel is not None
 | 
			
		||||
 | 
			
		||||
        payload = {
 | 
			
		||||
            "type": "status_update",
 | 
			
		||||
            "data": {
 | 
			
		||||
@@ -78,5 +87,16 @@ class ProgressManager:
 | 
			
		||||
        if extra_args is not None:
 | 
			
		||||
            payload["data"].update(extra_args)
 | 
			
		||||
 | 
			
		||||
        # Construct and send the update
 | 
			
		||||
        async_to_sync(self._channel.group_send)("status_updates", payload)
 | 
			
		||||
        self.send(payload)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DocumentsStatusManager(BaseStatusManager):
 | 
			
		||||
    def send_documents_deleted(self, documents: list[int]) -> None:
 | 
			
		||||
        payload = {
 | 
			
		||||
            "type": "documents_deleted",
 | 
			
		||||
            "data": {
 | 
			
		||||
                "documents": documents,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.send(payload)
 | 
			
		||||
 
 | 
			
		||||
@@ -41,4 +41,10 @@ class StatusConsumer(WebsocketConsumer):
 | 
			
		||||
            self.close()
 | 
			
		||||
        else:
 | 
			
		||||
            if self._is_owner_or_unowned(event["data"]):
 | 
			
		||||
                self.send(json.dumps(event["data"]))
 | 
			
		||||
                self.send(json.dumps(event))
 | 
			
		||||
 | 
			
		||||
    def documents_deleted(self, event):
 | 
			
		||||
        if not self._authenticated():
 | 
			
		||||
            self.close()
 | 
			
		||||
        else:
 | 
			
		||||
            self.send(json.dumps(event))
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,9 @@ from channels.testing import WebsocketCommunicator
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.test import override_settings
 | 
			
		||||
 | 
			
		||||
from documents.plugins.helpers import DocumentsStatusManager
 | 
			
		||||
from documents.plugins.helpers import ProgressManager
 | 
			
		||||
from documents.plugins.helpers import ProgressStatusOptions
 | 
			
		||||
from paperless.asgi import application
 | 
			
		||||
 | 
			
		||||
TEST_CHANNEL_LAYERS = {
 | 
			
		||||
@@ -22,6 +25,39 @@ class TestWebSockets(TestCase):
 | 
			
		||||
        self.assertFalse(connected)
 | 
			
		||||
        await communicator.disconnect()
 | 
			
		||||
 | 
			
		||||
    @mock.patch("paperless.consumers.StatusConsumer.close")
 | 
			
		||||
    @mock.patch("paperless.consumers.StatusConsumer._authenticated")
 | 
			
		||||
    async def test_close_on_no_auth(self, _authenticated, mock_close):
 | 
			
		||||
        _authenticated.return_value = True
 | 
			
		||||
 | 
			
		||||
        communicator = WebsocketCommunicator(application, "/ws/status/")
 | 
			
		||||
        connected, subprotocol = await communicator.connect()
 | 
			
		||||
        self.assertTrue(connected)
 | 
			
		||||
 | 
			
		||||
        message = {"type": "status_update", "data": {"task_id": "test"}}
 | 
			
		||||
 | 
			
		||||
        _authenticated.return_value = False
 | 
			
		||||
 | 
			
		||||
        channel_layer = get_channel_layer()
 | 
			
		||||
        await channel_layer.group_send(
 | 
			
		||||
            "status_updates",
 | 
			
		||||
            message,
 | 
			
		||||
        )
 | 
			
		||||
        await communicator.receive_nothing()
 | 
			
		||||
 | 
			
		||||
        mock_close.assert_called_once()
 | 
			
		||||
        mock_close.reset_mock()
 | 
			
		||||
 | 
			
		||||
        message = {"type": "documents_deleted", "data": {"documents": [1, 2, 3]}}
 | 
			
		||||
 | 
			
		||||
        await channel_layer.group_send(
 | 
			
		||||
            "status_updates",
 | 
			
		||||
            message,
 | 
			
		||||
        )
 | 
			
		||||
        await communicator.receive_nothing()
 | 
			
		||||
 | 
			
		||||
        mock_close.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    @mock.patch("paperless.consumers.StatusConsumer._authenticated")
 | 
			
		||||
    async def test_auth(self, _authenticated):
 | 
			
		||||
        _authenticated.return_value = True
 | 
			
		||||
@@ -33,19 +69,19 @@ class TestWebSockets(TestCase):
 | 
			
		||||
        await communicator.disconnect()
 | 
			
		||||
 | 
			
		||||
    @mock.patch("paperless.consumers.StatusConsumer._authenticated")
 | 
			
		||||
    async def test_receive(self, _authenticated):
 | 
			
		||||
    async def test_receive_status_update(self, _authenticated):
 | 
			
		||||
        _authenticated.return_value = True
 | 
			
		||||
 | 
			
		||||
        communicator = WebsocketCommunicator(application, "/ws/status/")
 | 
			
		||||
        connected, subprotocol = await communicator.connect()
 | 
			
		||||
        self.assertTrue(connected)
 | 
			
		||||
 | 
			
		||||
        message = {"task_id": "test"}
 | 
			
		||||
        message = {"type": "status_update", "data": {"task_id": "test"}}
 | 
			
		||||
 | 
			
		||||
        channel_layer = get_channel_layer()
 | 
			
		||||
        await channel_layer.group_send(
 | 
			
		||||
            "status_updates",
 | 
			
		||||
            {"type": "status_update", "data": message},
 | 
			
		||||
            message,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        response = await communicator.receive_json_from()
 | 
			
		||||
@@ -53,3 +89,73 @@ class TestWebSockets(TestCase):
 | 
			
		||||
        self.assertEqual(response, message)
 | 
			
		||||
 | 
			
		||||
        await communicator.disconnect()
 | 
			
		||||
 | 
			
		||||
    @mock.patch("paperless.consumers.StatusConsumer._authenticated")
 | 
			
		||||
    async def test_receive_documents_deleted(self, _authenticated):
 | 
			
		||||
        _authenticated.return_value = True
 | 
			
		||||
 | 
			
		||||
        communicator = WebsocketCommunicator(application, "/ws/status/")
 | 
			
		||||
        connected, subprotocol = await communicator.connect()
 | 
			
		||||
        self.assertTrue(connected)
 | 
			
		||||
 | 
			
		||||
        message = {"type": "documents_deleted", "data": {"documents": [1, 2, 3]}}
 | 
			
		||||
 | 
			
		||||
        channel_layer = get_channel_layer()
 | 
			
		||||
        await channel_layer.group_send(
 | 
			
		||||
            "status_updates",
 | 
			
		||||
            message,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        response = await communicator.receive_json_from()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(response, message)
 | 
			
		||||
 | 
			
		||||
        await communicator.disconnect()
 | 
			
		||||
 | 
			
		||||
    @mock.patch("channels.layers.InMemoryChannelLayer.group_send")
 | 
			
		||||
    def test_manager_send_progress(self, mock_group_send):
 | 
			
		||||
        with ProgressManager(task_id="test") as manager:
 | 
			
		||||
            manager.send_progress(
 | 
			
		||||
                ProgressStatusOptions.STARTED,
 | 
			
		||||
                "Test message",
 | 
			
		||||
                1,
 | 
			
		||||
                10,
 | 
			
		||||
                extra_args={
 | 
			
		||||
                    "foo": "bar",
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        message = mock_group_send.call_args[0][1]
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            message,
 | 
			
		||||
            {
 | 
			
		||||
                "type": "status_update",
 | 
			
		||||
                "data": {
 | 
			
		||||
                    "filename": None,
 | 
			
		||||
                    "task_id": "test",
 | 
			
		||||
                    "current_progress": 1,
 | 
			
		||||
                    "max_progress": 10,
 | 
			
		||||
                    "status": ProgressStatusOptions.STARTED,
 | 
			
		||||
                    "message": "Test message",
 | 
			
		||||
                    "foo": "bar",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @mock.patch("channels.layers.InMemoryChannelLayer.group_send")
 | 
			
		||||
    def test_manager_send_documents_deleted(self, mock_group_send):
 | 
			
		||||
        with DocumentsStatusManager() as manager:
 | 
			
		||||
            manager.send_documents_deleted([1, 2, 3])
 | 
			
		||||
 | 
			
		||||
        message = mock_group_send.call_args[0][1]
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            message,
 | 
			
		||||
            {
 | 
			
		||||
                "type": "documents_deleted",
 | 
			
		||||
                "data": {
 | 
			
		||||
                    "documents": [1, 2, 3],
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user