Compare commits

...

7 Commits

Author SHA1 Message Date
GitHub Actions
ce5d4e9c92 Auto translate strings 2025-05-13 06:07:40 +00:00
shamoon
0ab85b5122
Fix/Chore: replace file drop package (#9926) 2025-05-12 23:05:34 -07:00
shamoon
2c9e690dfb
Chore: resolve dynamic import warnings from pdfjs, again (#9924) 2025-05-12 21:04:18 -07:00
shamoon
344cc70cd5
Enhancement: support negative offset in scheduled workflows (#9746) 2025-05-11 20:04:46 +00:00
shamoon
73f0f1212d
Fixhancement: better handle removed social apps in profile (#9876) 2025-05-11 19:55:33 +00:00
GitHub Actions
a61f5ac64c Auto translate strings 2025-05-11 19:45:45 +00:00
shamoon
6a5be992c0
Enhancement: add barcode frontend config (#9742) 2025-05-11 19:44:06 +00:00
33 changed files with 1196 additions and 764 deletions

View File

@ -406,7 +406,8 @@ Currently, there are three events that correspond to workflow trigger 'types':
3. **Document Updated**: when a document is updated. Similar to 'added' events, triggers can include filtering by content matching, 3. **Document Updated**: when a document is updated. Similar to 'added' events, triggers can include filtering by content matching,
tags, doc type, or correspondent. tags, doc type, or correspondent.
4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document 4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document
added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date. added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive
offsets will trigger before the date, negative offsets will trigger after).
The following flow diagram illustrates the three document trigger types: The following flow diagram illustrates the three document trigger types:

View File

@ -0,0 +1,13 @@
export const getDocument = jest.fn(() => ({
promise: Promise.resolve({ numPages: 3 }),
}))
export const GlobalWorkerOptions = { workerSrc: '' }
export const VerbosityLevel = { ERRORS: 0 }
globalThis.pdfjsLib = {
getDocument,
GlobalWorkerOptions,
VerbosityLevel,
AbortException: class AbortException extends Error {},
}

View File

@ -1284,19 +1284,19 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">201</context> <context context-type="linenumber">209</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">220</context> <context context-type="linenumber">228</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">287</context> <context context-type="linenumber">295</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">306</context> <context context-type="linenumber">314</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context> <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
@ -1319,19 +1319,19 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">209</context> <context context-type="linenumber">217</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">228</context> <context context-type="linenumber">236</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">295</context> <context context-type="linenumber">303</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">314</context> <context context-type="linenumber">322</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context> <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
@ -1357,11 +1357,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">234</context> <context context-type="linenumber">242</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">320</context> <context context-type="linenumber">328</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context> <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
@ -3691,7 +3691,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">163</context> <context context-type="linenumber">171</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6457471243969293847" datatype="html"> <trans-unit id="6457471243969293847" datatype="html">
@ -4115,7 +4115,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">188</context> <context context-type="linenumber">196</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4754802869258527587" datatype="html"> <trans-unit id="4754802869258527587" datatype="html">
@ -4133,7 +4133,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">189</context> <context context-type="linenumber">197</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1519954996184640001" datatype="html"> <trans-unit id="1519954996184640001" datatype="html">
@ -4645,381 +4645,388 @@
<source>Offset days</source> <source>Offset days</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">126</context> <context context-type="linenumber">128</context>
</context-group>
</trans-unit>
<trans-unit id="1488741788768120127" datatype="html">
<source>Positive values will trigger the workflow before the date, negative values after.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">132</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3726450101884717309" datatype="html"> <trans-unit id="3726450101884717309" datatype="html">
<source>Relative to</source> <source>Relative to</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">129</context> <context context-type="linenumber">137</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3878884308536053934" datatype="html"> <trans-unit id="3878884308536053934" datatype="html">
<source>Custom field</source> <source>Custom field</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">133</context> <context context-type="linenumber">141</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1088170562604583291" datatype="html"> <trans-unit id="1088170562604583291" datatype="html">
<source>Custom field to use for date.</source> <source>Custom field to use for date.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">133</context> <context context-type="linenumber">141</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1011433830042635014" datatype="html"> <trans-unit id="1011433830042635014" datatype="html">
<source>Recurring</source> <source>Recurring</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">139</context> <context context-type="linenumber">147</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1421663004162437543" datatype="html"> <trans-unit id="1421663004162437543" datatype="html">
<source>Trigger is recurring.</source> <source>Trigger is recurring.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">139</context> <context context-type="linenumber">147</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5937989815294159481" datatype="html"> <trans-unit id="5937989815294159481" datatype="html">
<source>Recurring interval days</source> <source>Recurring interval days</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">143</context> <context context-type="linenumber">151</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="722765958672682251" datatype="html"> <trans-unit id="722765958672682251" datatype="html">
<source>Repeat the trigger every n days.</source> <source>Repeat the trigger every n days.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">143</context> <context context-type="linenumber">151</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8727727835543352574" datatype="html"> <trans-unit id="8727727835543352574" datatype="html">
<source>Trigger for documents that match <x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;em&gt;"/>all<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;/em&gt;"/> filters specified below.</source> <source>Trigger for documents that match <x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;em&gt;"/>all<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;/em&gt;"/> filters specified below.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">156</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7467799586957602479" datatype="html"> <trans-unit id="7467799586957602479" datatype="html">
<source>Filter filename</source> <source>Filter filename</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">151</context> <context context-type="linenumber">159</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3694878959415278689" datatype="html"> <trans-unit id="3694878959415278689" datatype="html">
<source>Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.</source> <source>Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">151</context> <context context-type="linenumber">159</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1473412958770421458" datatype="html"> <trans-unit id="1473412958770421458" datatype="html">
<source>Filter sources</source> <source>Filter sources</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">153</context> <context context-type="linenumber">161</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6540860478788535250" datatype="html"> <trans-unit id="6540860478788535250" datatype="html">
<source>Filter path</source> <source>Filter path</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">154</context> <context context-type="linenumber">162</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5491897741674893121" datatype="html"> <trans-unit id="5491897741674893121" datatype="html">
<source>Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.&lt;/a&gt;</source> <source>Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.&lt;/a&gt;</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">154</context> <context context-type="linenumber">162</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7468453896129193641" datatype="html"> <trans-unit id="7468453896129193641" datatype="html">
<source>Filter mail rule</source> <source>Filter mail rule</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">155</context> <context context-type="linenumber">163</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8663702115863339485" datatype="html"> <trans-unit id="8663702115863339485" datatype="html">
<source>Apply to documents consumed via this mail rule.</source> <source>Apply to documents consumed via this mail rule.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">155</context> <context context-type="linenumber">163</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6840369584127435743" datatype="html"> <trans-unit id="6840369584127435743" datatype="html">
<source>Content matching algorithm</source> <source>Content matching algorithm</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">158</context> <context context-type="linenumber">166</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="510635115034690805" datatype="html"> <trans-unit id="510635115034690805" datatype="html">
<source>Content matching pattern</source> <source>Content matching pattern</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">160</context> <context context-type="linenumber">168</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3484236514968690689" datatype="html"> <trans-unit id="3484236514968690689" datatype="html">
<source>Has any of tags</source> <source>Has any of tags</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">169</context> <context context-type="linenumber">177</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5281365940563983618" datatype="html"> <trans-unit id="5281365940563983618" datatype="html">
<source>Has correspondent</source> <source>Has correspondent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">170</context> <context context-type="linenumber">178</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4806713133917046341" datatype="html"> <trans-unit id="4806713133917046341" datatype="html">
<source>Has document type</source> <source>Has document type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">171</context> <context context-type="linenumber">179</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6417103744331194518" datatype="html"> <trans-unit id="6417103744331194518" datatype="html">
<source>Action type</source> <source>Action type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">181</context> <context context-type="linenumber">189</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6019822389883736115" datatype="html"> <trans-unit id="6019822389883736115" datatype="html">
<source>Assign title</source> <source>Assign title</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">186</context> <context context-type="linenumber">194</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1098196422099517191" datatype="html"> <trans-unit id="1098196422099517191" datatype="html">
<source>Can include some placeholders, see &lt;a target=&apos;_blank&apos; href=&apos;https://docs.paperless-ngx.com/usage/#workflows&apos;&gt;documentation&lt;/a&gt;.</source> <source>Can include some placeholders, see &lt;a target=&apos;_blank&apos; href=&apos;https://docs.paperless-ngx.com/usage/#workflows&apos;&gt;documentation&lt;/a&gt;.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">186</context> <context context-type="linenumber">194</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6528897010417701530" datatype="html"> <trans-unit id="6528897010417701530" datatype="html">
<source>Assign tags</source> <source>Assign tags</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">187</context> <context context-type="linenumber">195</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7198346314713788799" datatype="html"> <trans-unit id="7198346314713788799" datatype="html">
<source>Assign storage path</source> <source>Assign storage path</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">190</context> <context context-type="linenumber">198</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="475685412372379925" datatype="html"> <trans-unit id="475685412372379925" datatype="html">
<source>Assign custom fields</source> <source>Assign custom fields</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">191</context> <context context-type="linenumber">199</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5057200219587080996" datatype="html"> <trans-unit id="5057200219587080996" datatype="html">
<source>Assign owner</source> <source>Assign owner</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">195</context> <context context-type="linenumber">203</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1749184201773078639" datatype="html"> <trans-unit id="1749184201773078639" datatype="html">
<source>Assign view permissions</source> <source>Assign view permissions</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">197</context> <context context-type="linenumber">205</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1744964187586405039" datatype="html"> <trans-unit id="1744964187586405039" datatype="html">
<source>Assign edit permissions</source> <source>Assign edit permissions</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">216</context> <context context-type="linenumber">224</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6236311670364192011" datatype="html"> <trans-unit id="6236311670364192011" datatype="html">
<source>Remove tags</source> <source>Remove tags</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">243</context> <context context-type="linenumber">251</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7890599006071681081" datatype="html"> <trans-unit id="7890599006071681081" datatype="html">
<source>Remove all</source> <source>Remove all</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">244</context> <context context-type="linenumber">252</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">250</context> <context context-type="linenumber">258</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">256</context> <context context-type="linenumber">264</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">262</context> <context context-type="linenumber">270</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">268</context> <context context-type="linenumber">276</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">275</context> <context context-type="linenumber">283</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">281</context> <context context-type="linenumber">289</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8636414563726517994" datatype="html"> <trans-unit id="8636414563726517994" datatype="html">
<source>Remove correspondents</source> <source>Remove correspondents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">249</context> <context context-type="linenumber">257</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5305293055593064952" datatype="html"> <trans-unit id="5305293055593064952" datatype="html">
<source>Remove document types</source> <source>Remove document types</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">255</context> <context context-type="linenumber">263</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2400388879708187" datatype="html"> <trans-unit id="2400388879708187" datatype="html">
<source>Remove storage paths</source> <source>Remove storage paths</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">261</context> <context context-type="linenumber">269</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4324304327041955720" datatype="html"> <trans-unit id="4324304327041955720" datatype="html">
<source>Remove custom fields</source> <source>Remove custom fields</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">267</context> <context context-type="linenumber">275</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8367536502602515064" datatype="html"> <trans-unit id="8367536502602515064" datatype="html">
<source>Remove owners</source> <source>Remove owners</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">274</context> <context context-type="linenumber">282</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3393772184866313281" datatype="html"> <trans-unit id="3393772184866313281" datatype="html">
<source>Remove permissions</source> <source>Remove permissions</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">280</context> <context context-type="linenumber">288</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3145629643370481114" datatype="html"> <trans-unit id="3145629643370481114" datatype="html">
<source>View permissions</source> <source>View permissions</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">283</context> <context context-type="linenumber">291</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1946660694635960249" datatype="html"> <trans-unit id="1946660694635960249" datatype="html">
<source>Edit permissions</source> <source>Edit permissions</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">302</context> <context context-type="linenumber">310</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8987736563240025468" datatype="html"> <trans-unit id="8987736563240025468" datatype="html">
<source>Email subject</source> <source>Email subject</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">330</context> <context context-type="linenumber">338</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8239445959209739142" datatype="html"> <trans-unit id="8239445959209739142" datatype="html">
<source>Email body</source> <source>Email body</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">331</context> <context context-type="linenumber">339</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1222152280703048012" datatype="html"> <trans-unit id="1222152280703048012" datatype="html">
<source>Email recipients</source> <source>Email recipients</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">332</context> <context context-type="linenumber">340</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7916910101279824329" datatype="html"> <trans-unit id="7916910101279824329" datatype="html">
<source>Attach document</source> <source>Attach document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">333</context> <context context-type="linenumber">341</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5028001922785731600" datatype="html"> <trans-unit id="5028001922785731600" datatype="html">
<source>Webhook url</source> <source>Webhook url</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">341</context> <context context-type="linenumber">349</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7491983459027245019" datatype="html"> <trans-unit id="7491983459027245019" datatype="html">
<source>Use parameters for webhook body</source> <source>Use parameters for webhook body</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">343</context> <context context-type="linenumber">351</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4078214298308732810" datatype="html"> <trans-unit id="4078214298308732810" datatype="html">
<source>Send webhook payload as JSON</source> <source>Send webhook payload as JSON</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">344</context> <context context-type="linenumber">352</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6806149889743731985" datatype="html"> <trans-unit id="6806149889743731985" datatype="html">
<source>Webhook params</source> <source>Webhook params</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">347</context> <context context-type="linenumber">355</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7089924379374330" datatype="html"> <trans-unit id="7089924379374330" datatype="html">
<source>Webhook body</source> <source>Webhook body</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">349</context> <context context-type="linenumber">357</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3829826512656746316" datatype="html"> <trans-unit id="3829826512656746316" datatype="html">
<source>Webhook headers</source> <source>Webhook headers</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">351</context> <context context-type="linenumber">359</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2114525789021600887" datatype="html"> <trans-unit id="2114525789021600887" datatype="html">
<source>Include document</source> <source>Include document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">352</context> <context context-type="linenumber">360</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4626030417479279989" datatype="html"> <trans-unit id="4626030417479279989" datatype="html">
@ -7901,7 +7908,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">90</context> <context context-type="linenumber">91</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="329406837759048287" datatype="html"> <trans-unit id="329406837759048287" datatype="html">
@ -8248,7 +8255,18 @@
<source>Initiating upload...</source> <source>Initiating upload...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/file-drop/file-drop.component.ts</context> <context context-type="sourcefile">src/app/components/file-drop/file-drop.component.ts</context>
<context context-type="linenumber">93</context> <context context-type="linenumber">139</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/file-drop/file-drop.component.ts</context>
<context context-type="linenumber">148</context>
</context-group>
</trans-unit>
<trans-unit id="146335022760474171" datatype="html">
<source>Failed to read dropped items: <x id="PH" equiv-text="e.message"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/file-drop/file-drop.component.ts</context>
<context context-type="linenumber">144</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6316128875819022658" datatype="html"> <trans-unit id="6316128875819022658" datatype="html">
@ -9260,102 +9278,186 @@
<context context-type="linenumber">51</context> <context context-type="linenumber">51</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7943546022633063836" datatype="html">
<source>Barcode Settings</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">52</context>
</context-group>
</trans-unit>
<trans-unit id="1313137480169642057" datatype="html"> <trans-unit id="1313137480169642057" datatype="html">
<source>Output Type</source> <source>Output Type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">75</context> <context context-type="linenumber">76</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2826581353496868063" datatype="html"> <trans-unit id="2826581353496868063" datatype="html">
<source>Language</source> <source>Language</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">83</context> <context context-type="linenumber">84</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1713271461473302108" datatype="html"> <trans-unit id="1713271461473302108" datatype="html">
<source>Mode</source> <source>Mode</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">97</context> <context context-type="linenumber">98</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6114528299376689399" datatype="html"> <trans-unit id="6114528299376689399" datatype="html">
<source>Skip Archive File</source> <source>Skip Archive File</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">105</context> <context context-type="linenumber">106</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1115402553541327390" datatype="html"> <trans-unit id="1115402553541327390" datatype="html">
<source>Image DPI</source> <source>Image DPI</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">113</context> <context context-type="linenumber">114</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6352596107300820129" datatype="html"> <trans-unit id="6352596107300820129" datatype="html">
<source>Clean</source> <source>Clean</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">120</context> <context context-type="linenumber">121</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="725308589819024010" datatype="html"> <trans-unit id="725308589819024010" datatype="html">
<source>Deskew</source> <source>Deskew</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">128</context> <context context-type="linenumber">129</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6256076128297775802" datatype="html"> <trans-unit id="6256076128297775802" datatype="html">
<source>Rotate Pages</source> <source>Rotate Pages</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">135</context> <context context-type="linenumber">136</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8527188778859256947" datatype="html"> <trans-unit id="8527188778859256947" datatype="html">
<source>Rotate Pages Threshold</source> <source>Rotate Pages Threshold</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">142</context> <context context-type="linenumber">143</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3762131309176747817" datatype="html"> <trans-unit id="3762131309176747817" datatype="html">
<source>Max Image Pixels</source> <source>Max Image Pixels</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">149</context> <context context-type="linenumber">150</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7846583355792281769" datatype="html"> <trans-unit id="7846583355792281769" datatype="html">
<source>Color Conversion Strategy</source> <source>Color Conversion Strategy</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">156</context> <context context-type="linenumber">157</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4696480417479207939" datatype="html"> <trans-unit id="4696480417479207939" datatype="html">
<source>OCR Arguments</source> <source>OCR Arguments</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">164</context> <context context-type="linenumber">165</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7106327322456204362" datatype="html"> <trans-unit id="7106327322456204362" datatype="html">
<source>Application Logo</source> <source>Application Logo</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">171</context> <context context-type="linenumber">172</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2684743776608068095" datatype="html"> <trans-unit id="2684743776608068095" datatype="html">
<source>Application Title</source> <source>Application Title</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context> <context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">178</context> <context context-type="linenumber">179</context>
</context-group>
</trans-unit>
<trans-unit id="4763207540517250026" datatype="html">
<source>Enable Barcodes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit id="5111693440737450705" datatype="html">
<source>Enable TIFF Support</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">193</context>
</context-group>
</trans-unit>
<trans-unit id="7024102701648099736" datatype="html">
<source>Barcode String</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">200</context>
</context-group>
</trans-unit>
<trans-unit id="5496493538285104278" datatype="html">
<source>Retain Split Pages</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">207</context>
</context-group>
</trans-unit>
<trans-unit id="3585266363073659539" datatype="html">
<source>Enable ASN</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">214</context>
</context-group>
</trans-unit>
<trans-unit id="2563883192247717052" datatype="html">
<source>ASN Prefix</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">221</context>
</context-group>
</trans-unit>
<trans-unit id="876335624277968161" datatype="html">
<source>Upscale</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">228</context>
</context-group>
</trans-unit>
<trans-unit id="3330040801415354394" datatype="html">
<source>DPI</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">235</context>
</context-group>
</trans-unit>
<trans-unit id="2056636654483201493" datatype="html">
<source>Max Pages</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">242</context>
</context-group>
</trans-unit>
<trans-unit id="7410804727457548947" datatype="html">
<source>Enable Tag Detection</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">249</context>
</context-group>
</trans-unit>
<trans-unit id="3723784143052004117" datatype="html">
<source>Tag Mapping</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">256</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5948496158474272829" datatype="html"> <trans-unit id="5948496158474272829" datatype="html">
@ -9826,28 +9928,28 @@
<source>Connecting...</source> <source>Connecting...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/services/upload-documents.service.ts</context> <context context-type="sourcefile">src/app/services/upload-documents.service.ts</context>
<context context-type="linenumber">43</context> <context context-type="linenumber">27</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1245343823699368872" datatype="html"> <trans-unit id="1245343823699368872" datatype="html">
<source>Uploading...</source> <source>Uploading...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/services/upload-documents.service.ts</context> <context context-type="sourcefile">src/app/services/upload-documents.service.ts</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">39</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7446520539098045935" datatype="html"> <trans-unit id="7446520539098045935" datatype="html">
<source>Upload complete, waiting...</source> <source>Upload complete, waiting...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/services/upload-documents.service.ts</context> <context context-type="sourcefile">src/app/services/upload-documents.service.ts</context>
<context context-type="linenumber">58</context> <context context-type="linenumber">42</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1405142710727603568" datatype="html"> <trans-unit id="1405142710727603568" datatype="html">
<source>HTTP error: <x id="PH" equiv-text="error.status"/> <x id="PH_1" equiv-text="error.statusText"/></source> <source>HTTP error: <x id="PH" equiv-text="error.status"/> <x id="PH_1" equiv-text="error.statusText"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/services/upload-documents.service.ts</context> <context context-type="sourcefile">src/app/services/upload-documents.service.ts</context>
<context context-type="linenumber">71</context> <context context-type="linenumber">55</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2119857572761283468" datatype="html"> <trans-unit id="2119857572761283468" datatype="html">

View File

@ -7,8 +7,7 @@
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"test": "ng test --no-watch --coverage", "test": "ng test --no-watch --coverage",
"lint": "ng lint", "lint": "ng lint"
"postinstall": "patch-package"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -33,7 +32,6 @@
"ngx-color": "^10.0.0", "ngx-color": "^10.0.0",
"ngx-cookie-service": "^19.1.2", "ngx-cookie-service": "^19.1.2",
"ngx-device-detector": "^9.0.0", "ngx-device-detector": "^9.0.0",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^16.0.0", "ngx-ui-tour-ng-bootstrap": "^16.0.0",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
@ -67,7 +65,6 @@
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"jest-preset-angular": "^14.5.5", "jest-preset-angular": "^14.5.5",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"patch-package": "^8.0.0",
"prettier-plugin-organize-imports": "^4.1.0", "prettier-plugin-organize-imports": "^4.1.0",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",
"typescript": "^5.5.4" "typescript": "^5.5.4"

File diff suppressed because one or more lines are too long

224
src-ui/pnpm-lock.yaml generated
View File

@ -71,12 +71,9 @@ importers:
ngx-device-detector: ngx-device-detector:
specifier: ^9.0.0 specifier: ^9.0.0
version: 9.0.0(@angular/common@19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0)) version: 9.0.0(@angular/common@19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))
ngx-file-drop:
specifier: ^16.0.0
version: 16.0.0(@angular/common@19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))
ngx-ui-tour-ng-bootstrap: ngx-ui-tour-ng-bootstrap:
specifier: ^16.0.0 specifier: ^16.0.0
version: 16.0.0(nfyq54qpjcnpjhdwpvrgi4nyra) version: 16.0.0(9bfbf45bdfd1029869b23707ee6bb312)
rxjs: rxjs:
specifier: ^7.8.2 specifier: ^7.8.2
version: 7.8.2 version: 7.8.2
@ -98,7 +95,7 @@ importers:
version: 19.0.1(@angular/compiler-cli@19.2.9(@angular/compiler@19.2.9)(typescript@5.5.4))(@angular/compiler@19.2.9)(@angular/localize@19.2.9(@angular/compiler-cli@19.2.9(@angular/compiler@19.2.9)(typescript@5.5.4))(@angular/compiler@19.2.9))(@types/node@22.15.3)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.15.3)(ts-node@10.9.2(@types/node@22.15.3)(typescript@5.5.4)))(jiti@1.21.7)(typescript@5.5.4)(vite@6.2.7(@types/node@22.15.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) version: 19.0.1(@angular/compiler-cli@19.2.9(@angular/compiler@19.2.9)(typescript@5.5.4))(@angular/compiler@19.2.9)(@angular/localize@19.2.9(@angular/compiler-cli@19.2.9(@angular/compiler@19.2.9)(typescript@5.5.4))(@angular/compiler@19.2.9))(@types/node@22.15.3)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.15.3)(ts-node@10.9.2(@types/node@22.15.3)(typescript@5.5.4)))(jiti@1.21.7)(typescript@5.5.4)(vite@6.2.7(@types/node@22.15.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
'@angular-builders/jest': '@angular-builders/jest':
specifier: ^19.0.1 specifier: ^19.0.1
version: 19.0.1(2xzflpchozqzu223xxigbxxvgu) version: 19.0.1(025d537e0c22047bb48e08c4eeeaebf1)
'@angular-devkit/build-angular': '@angular-devkit/build-angular':
specifier: ^19.2.10 specifier: ^19.2.10
version: 19.2.10(@angular/compiler-cli@19.2.9(@angular/compiler@19.2.9)(typescript@5.5.4))(@angular/compiler@19.2.9)(@angular/localize@19.2.9(@angular/compiler-cli@19.2.9(@angular/compiler@19.2.9)(typescript@5.5.4))(@angular/compiler@19.2.9))(@types/node@22.15.3)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.15.3)(ts-node@10.9.2(@types/node@22.15.3)(typescript@5.5.4)))(jiti@1.21.7)(typescript@5.5.4)(vite@6.2.7(@types/node@22.15.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) version: 19.2.10(@angular/compiler-cli@19.2.9(@angular/compiler@19.2.9)(typescript@5.5.4))(@angular/compiler@19.2.9)(@angular/localize@19.2.9(@angular/compiler-cli@19.2.9(@angular/compiler@19.2.9)(typescript@5.5.4))(@angular/compiler@19.2.9))(@types/node@22.15.3)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.15.3)(ts-node@10.9.2(@types/node@22.15.3)(typescript@5.5.4)))(jiti@1.21.7)(typescript@5.5.4)(vite@6.2.7(@types/node@22.15.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
@ -168,9 +165,6 @@ importers:
jest-websocket-mock: jest-websocket-mock:
specifier: ^2.5.0 specifier: ^2.5.0
version: 2.5.0 version: 2.5.0
patch-package:
specifier: ^8.0.0
version: 8.0.0
prettier-plugin-organize-imports: prettier-plugin-organize-imports:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0(prettier@3.4.2)(typescript@5.5.4) version: 4.1.0(prettier@3.4.2)(typescript@5.5.4)
@ -2780,10 +2774,6 @@ packages:
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
at-least-node@1.0.0:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
autoprefixer@10.4.20: autoprefixer@10.4.20:
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@ -2931,10 +2921,6 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'}
call-bound@1.0.4: call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3226,10 +3212,6 @@ packages:
defaults@1.0.4: defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
define-lazy-prop@3.0.0: define-lazy-prop@3.0.0:
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -3607,9 +3589,6 @@ packages:
resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
find-yarn-workspace-root@2.0.0:
resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==}
flat-cache@4.0.1: flat-cache@4.0.1:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'} engines: {node: '>=16'}
@ -3652,10 +3631,6 @@ packages:
fs-constants@1.0.0: fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
fs-extra@9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
fs-minipass@2.1.0: fs-minipass@2.1.0:
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -3692,10 +3667,6 @@ packages:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
get-intrinsic@1.2.7:
resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==}
engines: {node: '>= 0.4'}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3763,9 +3734,6 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
has-symbols@1.1.0: has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3943,11 +3911,6 @@ packages:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
is-docker@3.0.0: is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -4020,10 +3983,6 @@ packages:
is-what@3.14.1: is-what@3.14.1:
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
is-wsl@2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
is-wsl@3.1.0: is-wsl@3.1.0:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
engines: {node: '>=16'} engines: {node: '>=16'}
@ -4031,9 +3990,6 @@ packages:
isarray@1.0.0: isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@ -4310,10 +4266,6 @@ packages:
json-stable-stringify-without-jsonify@1.0.1: json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
json-stable-stringify@1.1.0:
resolution: {integrity: sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==}
engines: {node: '>= 0.4'}
json5@2.2.3: json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -4322,12 +4274,6 @@ packages:
jsonc-parser@3.3.1: jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
jsonify@0.0.1:
resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==}
jsonparse@1.3.1: jsonparse@1.3.1:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0} engines: {'0': node >= 0.2.0}
@ -4342,9 +4288,6 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
klaw-sync@6.0.0:
resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==}
kleur@3.0.3: kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -4708,13 +4651,6 @@ packages:
'@angular/common': ^19.0.0 '@angular/common': ^19.0.0
'@angular/core': ^19.0.0 '@angular/core': ^19.0.0
ngx-file-drop@16.0.0:
resolution: {integrity: sha512-33RPoZBAiMkV110Rzu3iOrzGcG5M20S4sAiwLzNylfJobu9qVw5XR83FhUelSeqJRoaDxXBRKAozYCSnUf2CNw==}
engines: {node: '>= 14.5.0', npm: '>= 6.9.0'}
peerDependencies:
'@angular/common': '>=14.0.0'
'@angular/core': '>=14.0.0'
ngx-ui-tour-core@14.0.0: ngx-ui-tour-core@14.0.0:
resolution: {integrity: sha512-6pzzEwxn/gCS3puEXDqgINBRbhvhzHYjmiA9DTCNEx1dPfYwjZVmPqNvNeZIVHucVnVZViAAKvA6MTc3Gm7aOw==} resolution: {integrity: sha512-6pzzEwxn/gCS3puEXDqgINBRbhvhzHYjmiA9DTCNEx1dPfYwjZVmPqNvNeZIVHucVnVZViAAKvA6MTc3Gm7aOw==}
peerDependencies: peerDependencies:
@ -4814,10 +4750,6 @@ packages:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
obuf@1.1.2: obuf@1.1.2:
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
@ -4844,10 +4776,6 @@ packages:
resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==} resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==}
engines: {node: '>=18'} engines: {node: '>=18'}
open@7.4.2:
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
engines: {node: '>=8'}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -4935,11 +4863,6 @@ packages:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
patch-package@8.0.0:
resolution: {integrity: sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==}
engines: {node: '>=14', npm: '>5'}
hasBin: true
path-exists@4.0.0: path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -5265,11 +5188,6 @@ packages:
rfdc@1.4.1: rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rollup@4.34.8: rollup@4.34.8:
resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==} resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -5369,10 +5287,6 @@ packages:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
set-function-length@1.2.1:
resolution: {integrity: sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==}
engines: {node: '>= 0.4'}
setprototypeof@1.1.0: setprototypeof@1.1.0:
resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
@ -5434,10 +5348,6 @@ packages:
sisteransi@1.0.5: sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slash@2.0.0:
resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
engines: {node: '>=6'}
slash@3.0.0: slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -5825,10 +5735,6 @@ packages:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'} engines: {node: '>= 4.0.0'}
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
unpipe@1.0.0: unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -6233,7 +6139,7 @@ snapshots:
- webpack-cli - webpack-cli
- yaml - yaml
'@angular-builders/jest@19.0.1(2xzflpchozqzu223xxigbxxvgu)': '@angular-builders/jest@19.0.1(025d537e0c22047bb48e08c4eeeaebf1)':
dependencies: dependencies:
'@angular-builders/common': 3.0.1(@types/node@22.15.3)(chokidar@4.0.3)(typescript@5.5.4) '@angular-builders/common': 3.0.1(@types/node@22.15.3)(chokidar@4.0.3)(typescript@5.5.4)
'@angular-devkit/architect': 0.1902.8(chokidar@4.0.3) '@angular-devkit/architect': 0.1902.8(chokidar@4.0.3)
@ -8995,8 +8901,6 @@ snapshots:
asynckit@0.4.0: {} asynckit@0.4.0: {}
at-least-node@1.0.0: {}
autoprefixer@10.4.20(postcss@8.5.2): autoprefixer@10.4.20(postcss@8.5.2):
dependencies: dependencies:
browserslist: 4.24.4 browserslist: 4.24.4
@ -9215,14 +9119,6 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
function-bind: 1.1.2 function-bind: 1.1.2
call-bind@1.0.7:
dependencies:
es-define-property: 1.0.1
es-errors: 1.3.0
function-bind: 1.1.2
get-intrinsic: 1.2.7
set-function-length: 1.2.1
call-bound@1.0.4: call-bound@1.0.4:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@ -9497,12 +9393,6 @@ snapshots:
dependencies: dependencies:
clone: 1.0.4 clone: 1.0.4
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.1
es-errors: 1.3.0
gopd: 1.2.0
define-lazy-prop@3.0.0: {} define-lazy-prop@3.0.0: {}
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
@ -9970,10 +9860,6 @@ snapshots:
locate-path: 7.2.0 locate-path: 7.2.0
path-exists: 5.0.0 path-exists: 5.0.0
find-yarn-workspace-root@2.0.0:
dependencies:
micromatch: 4.0.8
flat-cache@4.0.1: flat-cache@4.0.1:
dependencies: dependencies:
flatted: 3.3.3 flatted: 3.3.3
@ -10007,13 +9893,6 @@ snapshots:
fs-constants@1.0.0: fs-constants@1.0.0:
optional: true optional: true
fs-extra@9.1.0:
dependencies:
at-least-node: 1.0.0
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.1
fs-minipass@2.1.0: fs-minipass@2.1.0:
dependencies: dependencies:
minipass: 3.3.6 minipass: 3.3.6
@ -10038,19 +9917,6 @@ snapshots:
get-east-asian-width@1.3.0: {} get-east-asian-width@1.3.0: {}
get-intrinsic@1.2.7:
dependencies:
call-bind-apply-helpers: 1.0.1
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@ -10127,10 +9993,6 @@ snapshots:
has-flag@4.0.0: {} has-flag@4.0.0: {}
has-property-descriptors@1.0.2:
dependencies:
es-define-property: 1.0.1
has-symbols@1.1.0: {} has-symbols@1.1.0: {}
hasown@2.0.2: hasown@2.0.2:
@ -10320,8 +10182,6 @@ snapshots:
dependencies: dependencies:
hasown: 2.0.2 hasown: 2.0.2
is-docker@2.2.1: {}
is-docker@3.0.0: {} is-docker@3.0.0: {}
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
@ -10366,18 +10226,12 @@ snapshots:
is-what@3.14.1: {} is-what@3.14.1: {}
is-wsl@2.2.0:
dependencies:
is-docker: 2.2.1
is-wsl@3.1.0: is-wsl@3.1.0:
dependencies: dependencies:
is-inside-container: 1.0.0 is-inside-container: 1.0.0
isarray@1.0.0: {} isarray@1.0.0: {}
isarray@2.0.5: {}
isexe@2.0.0: {} isexe@2.0.0: {}
isexe@3.1.1: {} isexe@3.1.1: {}
@ -10896,25 +10750,10 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {} json-stable-stringify-without-jsonify@1.0.1: {}
json-stable-stringify@1.1.0:
dependencies:
call-bind: 1.0.7
isarray: 2.0.5
jsonify: 0.0.1
object-keys: 1.1.1
json5@2.2.3: {} json5@2.2.3: {}
jsonc-parser@3.3.1: {} jsonc-parser@3.3.1: {}
jsonfile@6.1.0:
dependencies:
universalify: 2.0.1
optionalDependencies:
graceful-fs: 4.2.11
jsonify@0.0.1: {}
jsonparse@1.3.1: {} jsonparse@1.3.1: {}
karma-source-map-support@1.4.0: karma-source-map-support@1.4.0:
@ -10927,10 +10766,6 @@ snapshots:
kind-of@6.0.3: {} kind-of@6.0.3: {}
klaw-sync@6.0.0:
dependencies:
graceful-fs: 4.2.11
kleur@3.0.3: {} kleur@3.0.3: {}
launch-editor@2.10.0: launch-editor@2.10.0:
@ -11289,12 +11124,6 @@ snapshots:
'@angular/core': 19.2.9(rxjs@7.8.2)(zone.js@0.15.0) '@angular/core': 19.2.9(rxjs@7.8.2)(zone.js@0.15.0)
tslib: 2.8.1 tslib: 2.8.1
ngx-file-drop@16.0.0(@angular/common@19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0)):
dependencies:
'@angular/common': 19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
'@angular/core': 19.2.9(rxjs@7.8.2)(zone.js@0.15.0)
tslib: 2.8.1
ngx-ui-tour-core@14.0.0(@angular/common@19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(@angular/router@19.2.9(@angular/common@19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@19.2.9(@angular/common@19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))(rxjs@7.8.2): ngx-ui-tour-core@14.0.0(@angular/common@19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(@angular/router@19.2.9(@angular/common@19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@19.2.9(@angular/common@19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))(rxjs@7.8.2):
dependencies: dependencies:
'@angular/common': 19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/common': 19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
@ -11303,7 +11132,7 @@ snapshots:
rxjs: 7.8.2 rxjs: 7.8.2
tslib: 2.8.1 tslib: 2.8.1
ngx-ui-tour-ng-bootstrap@16.0.0(nfyq54qpjcnpjhdwpvrgi4nyra): ngx-ui-tour-ng-bootstrap@16.0.0(9bfbf45bdfd1029869b23707ee6bb312):
dependencies: dependencies:
'@angular/common': 19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/common': 19.2.9(@angular/core@19.2.9(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
'@angular/core': 19.2.9(rxjs@7.8.2)(zone.js@0.15.0) '@angular/core': 19.2.9(rxjs@7.8.2)(zone.js@0.15.0)
@ -11412,8 +11241,6 @@ snapshots:
object-inspect@1.13.4: {} object-inspect@1.13.4: {}
object-keys@1.1.1: {}
obuf@1.1.2: {} obuf@1.1.2: {}
on-finished@2.4.1: on-finished@2.4.1:
@ -11441,11 +11268,6 @@ snapshots:
is-inside-container: 1.0.0 is-inside-container: 1.0.0
is-wsl: 3.1.0 is-wsl: 3.1.0
open@7.4.2:
dependencies:
is-docker: 2.2.1
is-wsl: 2.2.0
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@ -11561,24 +11383,6 @@ snapshots:
parseurl@1.3.3: {} parseurl@1.3.3: {}
patch-package@8.0.0:
dependencies:
'@yarnpkg/lockfile': 1.1.0
chalk: 4.1.2
ci-info: 3.9.0
cross-spawn: 7.0.6
find-yarn-workspace-root: 2.0.0
fs-extra: 9.1.0
json-stable-stringify: 1.1.0
klaw-sync: 6.0.0
minimist: 1.2.8
open: 7.4.2
rimraf: 2.7.1
semver: 7.7.1
slash: 2.0.0
tmp: 0.0.33
yaml: 2.7.0
path-exists@4.0.0: {} path-exists@4.0.0: {}
path-exists@5.0.0: {} path-exists@5.0.0: {}
@ -11883,10 +11687,6 @@ snapshots:
rfdc@1.4.1: {} rfdc@1.4.1: {}
rimraf@2.7.1:
dependencies:
glob: 7.2.3
rollup@4.34.8: rollup@4.34.8:
dependencies: dependencies:
'@types/estree': 1.0.6 '@types/estree': 1.0.6
@ -12018,15 +11818,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
set-function-length@1.2.1:
dependencies:
define-data-property: 1.1.4
es-errors: 1.3.0
function-bind: 1.1.2
get-intrinsic: 1.2.7
gopd: 1.2.0
has-property-descriptors: 1.0.2
setprototypeof@1.1.0: {} setprototypeof@1.1.0: {}
setprototypeof@1.2.0: {} setprototypeof@1.2.0: {}
@ -12105,8 +11896,6 @@ snapshots:
sisteransi@1.0.5: {} sisteransi@1.0.5: {}
slash@2.0.0: {}
slash@3.0.0: {} slash@3.0.0: {}
slash@5.1.0: {} slash@5.1.0: {}
@ -12517,8 +12306,6 @@ snapshots:
universalify@0.2.0: {} universalify@0.2.0: {}
universalify@2.0.1: {}
unpipe@1.0.0: {} unpipe@1.0.0: {}
unplugin@1.16.1: unplugin@1.16.1:
@ -12783,7 +12570,8 @@ snapshots:
yallist@5.0.0: {} yallist@5.0.0: {}
yaml@2.7.0: {} yaml@2.7.0:
optional: true
yargs-parser@21.1.1: {} yargs-parser@21.1.1: {}

View File

@ -121,19 +121,4 @@ HTMLCanvasElement.prototype.getContext = <
typeof HTMLCanvasElement.prototype.getContext typeof HTMLCanvasElement.prototype.getContext
>jest.fn() >jest.fn()
// pdfjs jest.mock('pdfjs-dist')
jest.mock('pdfjs-dist', () => ({
getDocument: jest.fn(() => ({
promise: Promise.resolve({ numPages: 3 }),
})),
GlobalWorkerOptions: { workerSrc: '' },
VerbosityLevel: { ERRORS: 0 },
globalThis: {
pdfjsLib: {
GlobalWorkerOptions: {
workerSrc: '',
},
},
},
}))
jest.mock('pdfjs-dist/web/pdf_viewer', () => ({}))

View File

@ -9,7 +9,6 @@ import {
import { Router, RouterModule } from '@angular/router' import { Router, RouterModule } from '@angular/router'
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { NgxFileDropModule } from 'ngx-file-drop'
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { routes } from './app-routing.module' import { routes } from './app-routing.module'
@ -43,7 +42,6 @@ describe('AppComponent', () => {
imports: [ imports: [
TourNgBootstrapModule, TourNgBootstrapModule,
RouterModule.forRoot(routes), RouterModule.forRoot(routes),
NgxFileDropModule,
NgbModalModule, NgbModalModule,
AppComponent, AppComponent,
ToastsComponent, ToastsComponent,

View File

@ -105,9 +105,9 @@ describe('ConfigComponent', () => {
it('should support JSON validation for e.g. user_args', () => { it('should support JSON validation for e.g. user_args', () => {
component.configForm.patchValue({ user_args: '{ foo bar }' }) component.configForm.patchValue({ user_args: '{ foo bar }' })
expect(component.errors).toEqual({ user_args: 'Invalid JSON' }) expect(component.errors['user_args']).toEqual('Invalid JSON')
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' }) component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
expect(component.errors).toEqual({ user_args: null }) expect(component.errors['user_args']).toBeNull()
}) })
it('should upload file, show error if necessary', () => { it('should upload file, show error if necessary', () => {

View File

@ -123,7 +123,15 @@
<p class="small" i18n>Set scheduled trigger offset and which date field to use.</p> <p class="small" i18n>Set scheduled trigger offset and which date field to use.</p>
<div class="row"> <div class="row">
<div class="col-4"> <div class="col-4">
<pngx-input-number i18n-title title="Offset days" formControlName="schedule_offset_days" [showAdd]="false" [error]="error?.schedule_offset_days"></pngx-input-number> <pngx-input-number
i18n-title
title="Offset days"
formControlName="schedule_offset_days"
[showAdd]="false"
[error]="error?.schedule_offset_days"
hint="Positive values will trigger the workflow before the date, negative values after."
i18n-hint
></pngx-input-number>
</div> </div>
<div class="col-4"> <div class="col-4">
<pngx-input-select i18n-title title="Relative to" formControlName="schedule_date_field" [items]="scheduleDateFieldOptions" [error]="error?.schedule_date_field"></pngx-input-select> <pngx-input-select i18n-title title="Relative to" formControlName="schedule_date_field" [items]="scheduleDateFieldOptions" [error]="error?.schedule_date_field"></pngx-input-select>

View File

@ -82,10 +82,20 @@ describe('UploadFileWidgetComponent', () => {
}) })
it('should upload files', () => { it('should upload files', () => {
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles') const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
fixture.debugElement const file = new File(
.query(By.css('input')) [new Blob(['testing'], { type: 'application/pdf' })],
.nativeElement.dispatchEvent(new Event('change')) 'file.pdf'
)
const fileInput = fixture.debugElement.query(By.css('input'))
jest.spyOn(fileInput.nativeElement, 'files', 'get').mockReturnValue({
item: () => file,
length: 1,
[Symbol.iterator]: () => ({
next: () => ({ done: false, value: file }),
}),
} as any)
fileInput.nativeElement.dispatchEvent(new Event('change'))
expect(uploadSpy).toHaveBeenCalled() expect(uploadSpy).toHaveBeenCalled()
}) })

View File

@ -134,9 +134,11 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
} }
public onFileSelected(event: Event) { public onFileSelected(event: Event) {
this.uploadDocumentsService.uploadFiles( const files = (event.target as HTMLInputElement).files
(event.target as HTMLInputElement).files for (let i = 0; i < files?.length; i++) {
) const file = files.item(i)
file && this.uploadDocumentsService.uploadFile(file)
}
} }
get slimSidebarEnabled(): boolean { get slimSidebarEnabled(): boolean {

View File

@ -2,13 +2,6 @@
<ng-content select="[content]"></ng-content> <ng-content select="[content]"></ng-content>
</div> </div>
<div class="global-dropzone-overlay position-fixed top-0 start-0 bottom-0 end-0 text-center pe-none fade" [class.show]="fileIsOver" [class.hide]="hidden"> <div class="global-dropzone-overlay position-fixed top-0 start-0 bottom-0 end-0 text-center pe-none" [class.active]="fileIsOver && !hidden">
<h2 class="pe-none position-absolute top-50 start-50 translate-middle" i18n>Drop files to begin upload</h2> <h2 class="pe-none position-absolute top-50 start-50 translate-middle" i18n>Drop files to begin upload</h2>
</div> </div>
<ngx-file-drop
dropZoneClassName="visually-hidden"
contentClassName="visually-hidden"
(onFileDrop)="dropped($event)"
#ngxFileDrop>
</ngx-file-drop>

View File

@ -1,8 +1,14 @@
.global-dropzone-overlay { .global-dropzone-overlay {
opacity: 0;
transition: opacity 0.25s ease-in-out;
background-color: hsla(var(--pngx-primary), var(--pngx-primary-lightness), .8); background-color: hsla(var(--pngx-primary), var(--pngx-primary-lightness), .8);
z-index: 1200; z-index: 1200;
h2 { h2 {
color: var(--pngx-primary-text-contrast) color: var(--pngx-primary-text-contrast)
} }
&.active {
opacity: 1;
}
} }

View File

@ -9,7 +9,6 @@ import {
tick, tick,
} from '@angular/core/testing' } from '@angular/core/testing'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop'
import { PermissionsService } from 'src/app/services/permissions.service' import { PermissionsService } from 'src/app/services/permissions.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
@ -27,7 +26,7 @@ describe('FileDropComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [NgxFileDropModule, FileDropComponent, ToastsComponent], imports: [FileDropComponent, ToastsComponent],
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
@ -66,12 +65,12 @@ describe('FileDropComponent', () => {
const dropzone = fixture.debugElement.query( const dropzone = fixture.debugElement.query(
By.css('.global-dropzone-overlay') By.css('.global-dropzone-overlay')
) )
expect(dropzone.classes['hide']).toBeTruthy() expect(dropzone.classes['active']).toBeFalsy()
component.onDragLeave(new Event('dragleave') as DragEvent) component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700) tick(700)
fixture.detectChanges() fixture.detectChanges()
// drop // drop
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles') const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const dragEvent = new Event('drop') const dragEvent = new Event('drop')
dragEvent['dataTransfer'] = { dragEvent['dataTransfer'] = {
files: { files: {
@ -93,53 +92,209 @@ describe('FileDropComponent', () => {
tick(1) tick(1)
fixture.detectChanges() fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy() expect(component.fileIsOver).toBeTruthy()
const dropzone = fixture.debugElement.query(
By.css('.global-dropzone-overlay')
)
component.onDragLeave(new Event('dragleave') as DragEvent) component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700) tick(700)
fixture.detectChanges() fixture.detectChanges()
expect(dropzone.classes['hide']).toBeTruthy()
// drop // drop
const toastSpy = jest.spyOn(toastService, 'show') const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn( const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
UploadDocumentsService.prototype as any, const file = new File(
'uploadFile' [new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
) )
const dragEvent = new Event('drop') const dragEvent = new Event('drop')
dragEvent['dataTransfer'] = { dragEvent['dataTransfer'] = {
files: { items: [
item: () => { {
return new File( kind: 'file',
[new Blob(['testing'], { type: 'application/pdf' })], type: 'application/pdf',
'file.pdf' getAsFile: () => file,
)
}, },
length: 1, ],
} as unknown as FileList,
} }
component.onDrop(dragEvent as DragEvent) component.onDrop(dragEvent as DragEvent)
component.dropped([
{
fileEntry: {
isFile: true,
file: (callback) => {
callback(
new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
)
},
},
} as unknown as NgxFileDropEntry,
])
tick(3000) tick(3000)
expect(toastSpy).toHaveBeenCalled() expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled() expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks() discardPeriodicTasks()
})) }))
it('should support drag drop, initiate upload with webkitGetAsEntry', fakeAsync(() => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.fileIsOver).toBeFalsy()
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const file = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
const dragEvent = new Event('drop')
dragEvent['dataTransfer'] = {
items: [
{
kind: 'file',
type: 'application/pdf',
webkitGetAsEntry: () => ({
isFile: true,
isDirectory: false,
file: (cb: (file: File) => void) => cb(file),
}),
},
],
files: [],
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
it('should show an error on traverseFileTree error', fakeAsync(() => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
const toastSpy = jest.spyOn(toastService, 'showError')
const traverseSpy = jest
.spyOn(component as any, 'traverseFileTree')
.mockReturnValue(Promise.reject(new Error('Error traversing file tree')))
fixture.detectChanges()
// Simulate a drop with a directory entry
const mockEntry = {
isDirectory: true,
isFile: false,
createReader: () => ({ readEntries: jest.fn() }),
} as unknown as FileSystemDirectoryEntry
const event = {
preventDefault: () => {},
stopImmediatePropagation: () => {},
dataTransfer: {
items: [
{
kind: 'file',
webkitGetAsEntry: () => mockEntry,
},
],
},
} as unknown as DragEvent
component.onDrop(event)
tick() // flush microtasks (e.g., Promise.reject)
expect(traverseSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
$localize`Failed to read dropped items: Error traversing file tree`
)
discardPeriodicTasks()
}))
it('should support drag drop, initiate upload without DataTransfer API support', fakeAsync(() => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.fileIsOver).toBeFalsy()
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const file = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
const dragEvent = new Event('drop')
dragEvent['dataTransfer'] = {
items: [],
files: [file],
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
it('should resolve a single file when entry isFile', () => {
const mockFile = new File(['data'], 'test.txt', { type: 'text/plain' })
const mockEntry = {
isFile: true,
isDirectory: false,
file: (cb: (f: File) => void) => cb(mockFile),
} as unknown as FileSystemFileEntry
return (component as any)
.traverseFileTree(mockEntry)
.then((result: File[]) => {
expect(result).toEqual([mockFile])
})
})
it('should resolve all files in a flat directory', async () => {
const file1 = new File(['data'], 'file1.txt')
const file2 = new File(['data'], 'file2.txt')
const mockFileEntry1 = {
isFile: true,
isDirectory: false,
file: (cb: (f: File) => void) => cb(file1),
} as unknown as FileSystemFileEntry
const mockFileEntry2 = {
isFile: true,
isDirectory: false,
file: (cb: (f: File) => void) => cb(file2),
} as unknown as FileSystemFileEntry
let callCount = 0
const mockDirEntry = {
isFile: false,
isDirectory: true,
createReader: () => ({
readEntries: (cb: (batch: FileSystemEntry[]) => void) => {
if (callCount++ === 0) {
cb([mockFileEntry1, mockFileEntry2])
} else {
cb([]) // second call: signal EOF
}
},
}),
} as unknown as FileSystemDirectoryEntry
const result = await (component as any).traverseFileTree(mockDirEntry)
expect(result).toEqual([file1, file2])
})
it('should resolve a non-file non-directory entry as an empty array', () => {
const mockEntry = {
isFile: false,
isDirectory: false,
file: (cb: (f: File) => void) => cb(new File([], '')),
} as unknown as FileSystemEntry
return (component as any)
.traverseFileTree(mockEntry)
.then((result: File[]) => {
expect(result).toEqual([])
})
})
it('should ignore events if disabled', fakeAsync(() => { it('should ignore events if disabled', fakeAsync(() => {
settingsService.globalDropzoneEnabled = false settingsService.globalDropzoneEnabled = false
expect(settingsService.globalDropzoneActive).toBeFalsy() expect(settingsService.globalDropzoneActive).toBeFalsy()

View File

@ -1,9 +1,4 @@
import { Component, HostListener, ViewChild } from '@angular/core' import { Component, HostListener } from '@angular/core'
import {
NgxFileDropComponent,
NgxFileDropEntry,
NgxFileDropModule,
} from 'ngx-file-drop'
import { import {
PermissionAction, PermissionAction,
PermissionsService, PermissionsService,
@ -17,7 +12,7 @@ import { UploadDocumentsService } from 'src/app/services/upload-documents.servic
selector: 'pngx-file-drop', selector: 'pngx-file-drop',
templateUrl: './file-drop.component.html', templateUrl: './file-drop.component.html',
styleUrls: ['./file-drop.component.scss'], styleUrls: ['./file-drop.component.scss'],
imports: [NgxFileDropModule], imports: [],
}) })
export class FileDropComponent { export class FileDropComponent {
private fileLeaveTimeoutID: any private fileLeaveTimeoutID: any
@ -41,8 +36,6 @@ export class FileDropComponent {
) )
} }
@ViewChild('ngxFileDrop') ngxFileDrop: NgxFileDropComponent
@HostListener('document:dragover', ['$event']) onDragOver(event: DragEvent) { @HostListener('document:dragover', ['$event']) onDragOver(event: DragEvent) {
if (!this.dragDropEnabled || !event.dataTransfer?.types?.includes('Files')) if (!this.dragDropEnabled || !event.dataTransfer?.types?.includes('Files'))
return return
@ -78,19 +71,85 @@ export class FileDropComponent {
}, ms) }, ms)
} }
private traverseFileTree(entry: FileSystemEntry): Promise<File[]> {
if (entry.isFile) {
return new Promise((resolve, reject) => {
;(entry as FileSystemFileEntry).file(resolve, reject)
}).then((file: File) => [file])
}
if (entry.isDirectory) {
return new Promise<File[]>((resolve, reject) => {
const dirReader = (entry as FileSystemDirectoryEntry).createReader()
const allEntries: FileSystemEntry[] = []
const readEntries = () => {
dirReader.readEntries((batch) => {
if (batch.length === 0) {
const promises = allEntries.map((child) =>
this.traverseFileTree(child)
)
Promise.all(promises)
.then((results) => resolve([].concat(...results)))
.catch(reject)
} else {
allEntries.push(...batch)
readEntries() // keep reading
}
}, reject)
}
readEntries()
})
}
return Promise.resolve([])
}
@HostListener('document:drop', ['$event']) public onDrop(event: DragEvent) { @HostListener('document:drop', ['$event']) public onDrop(event: DragEvent) {
if (!this.dragDropEnabled) return if (!this.dragDropEnabled) return
event.preventDefault() event.preventDefault()
event.stopImmediatePropagation() event.stopImmediatePropagation()
// pass event onto ngx-file-drop to handle files
this.ngxFileDrop.dropFiles(event)
this.onDragLeave(event, true)
}
public dropped(files: NgxFileDropEntry[]) { const files: File[] = []
this.uploadDocumentsService.onNgxFileDrop(files) const entries: FileSystemEntry[] = []
if (files.length > 0) if (event.dataTransfer?.items && event.dataTransfer.items.length) {
for (const item of Array.from(event.dataTransfer.items)) {
if (item.webkitGetAsEntry) {
// webkitGetAsEntry not standard, but is widely supported
const entry = item.webkitGetAsEntry()
if (entry) entries.push(entry)
} else if (item.kind === 'file') {
const file = item.getAsFile()
if (file) files.push(file)
}
}
} else if (event.dataTransfer?.files) {
// Fallback for browsers without DataTransferItem API
for (const file of Array.from(event.dataTransfer.files)) {
files.push(file)
}
}
if (entries.length) {
const promises = entries.map((entry) => this.traverseFileTree(entry))
Promise.all(promises)
.then((results) => {
files.push(...[].concat(...results))
this.toastService.showInfo($localize`Initiating upload...`, 3000)
files.forEach((file) => this.uploadDocumentsService.uploadFile(file))
})
.catch((e) => {
this.toastService.showError(
$localize`Failed to read dropped items: ${e.message}`
)
})
} else if (files.length) {
this.toastService.showInfo($localize`Initiating upload...`, 3000) this.toastService.showInfo($localize`Initiating upload...`, 3000)
files.forEach((file) => this.uploadDocumentsService.uploadFile(file))
}
this.onDragLeave(event, true)
} }
@HostListener('window:blur', ['$event']) public onWindowBlur() { @HostListener('window:blur', ['$event']) public onWindowBlur() {

View File

@ -49,6 +49,7 @@ export enum ConfigOptionType {
export const ConfigCategory = { export const ConfigCategory = {
General: $localize`General Settings`, General: $localize`General Settings`,
OCR: $localize`OCR Settings`, OCR: $localize`OCR Settings`,
Barcode: $localize`Barcode Settings`,
} }
export interface ConfigOption { export interface ConfigOption {
@ -180,6 +181,83 @@ export const PaperlessConfigOptions: ConfigOption[] = [
config_key: 'PAPERLESS_APP_TITLE', config_key: 'PAPERLESS_APP_TITLE',
category: ConfigCategory.General, category: ConfigCategory.General,
}, },
{
key: 'barcodes_enabled',
title: $localize`Enable Barcodes`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_ENABLE_BARCODES',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_enable_tiff_support',
title: $localize`Enable TIFF Support`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_string',
title: $localize`Barcode String`,
type: ConfigOptionType.String,
config_key: 'PAPERLESS_CONSUMER_BARCODE_STRING',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_retain_split_pages',
title: $localize`Retain Split Pages`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_enable_asn',
title: $localize`Enable ASN`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_asn_prefix',
title: $localize`ASN Prefix`,
type: ConfigOptionType.String,
config_key: 'PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_upscale',
title: $localize`Upscale`,
type: ConfigOptionType.Number,
config_key: 'PAPERLESS_CONSUMER_BARCODE_UPSCALE',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_dpi',
title: $localize`DPI`,
type: ConfigOptionType.Number,
config_key: 'PAPERLESS_CONSUMER_BARCODE_DPI',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_max_pages',
title: $localize`Max Pages`,
type: ConfigOptionType.Number,
config_key: 'PAPERLESS_CONSUMER_BARCODE_MAX_PAGES',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_enable_tag',
title: $localize`Enable Tag Detection`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE',
category: ConfigCategory.Barcode,
},
{
key: 'barcode_tag_mapping',
title: $localize`Tag Mapping`,
type: ConfigOptionType.JSON,
config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING',
category: ConfigCategory.Barcode,
},
] ]
export interface PaperlessConfig extends ObjectWithId { export interface PaperlessConfig extends ObjectWithId {
@ -198,4 +276,15 @@ export interface PaperlessConfig extends ObjectWithId {
user_args: object user_args: object
app_logo: string app_logo: string
app_title: string app_title: string
barcodes_enabled: boolean
barcode_enable_tiff_support: boolean
barcode_string: string
barcode_retain_split_pages: boolean
barcode_enable_asn: boolean
barcode_asn_prefix: string
barcode_upscale: number
barcode_dpi: number
barcode_max_pages: number
barcode_enable_tag: boolean
barcode_tag_mapping: object
} }

View File

@ -15,33 +15,6 @@ import {
WebsocketStatusService, WebsocketStatusService,
} from './websocket-status.service' } from './websocket-status.service'
const files = [
{
lastModified: 1693349892540,
lastModifiedDate: new Date(),
name: 'file1.pdf',
size: 386,
type: 'application/pdf',
},
{
lastModified: 1695618533892,
lastModifiedDate: new Date(),
name: 'file2.pdf',
size: 358265,
type: 'application/pdf',
},
]
const fileList = {
item: (x) => {
return new File(
[new Blob(['testing'], { type: files[x].type })],
files[x].name
)
},
length: files.length,
} as unknown as FileList
describe('UploadDocumentsService', () => { describe('UploadDocumentsService', () => {
let httpTestingController: HttpTestingController let httpTestingController: HttpTestingController
let uploadDocumentsService: UploadDocumentsService let uploadDocumentsService: UploadDocumentsService
@ -68,7 +41,11 @@ describe('UploadDocumentsService', () => {
}) })
it('calls post_document api endpoint on upload', () => { it('calls post_document api endpoint on upload', () => {
uploadDocumentsService.uploadFiles(fileList) const file = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
uploadDocumentsService.uploadFile(file)
const req = httpTestingController.match( const req = httpTestingController.match(
`${environment.apiBaseUrl}documents/post_document/` `${environment.apiBaseUrl}documents/post_document/`
) )
@ -78,7 +55,16 @@ describe('UploadDocumentsService', () => {
}) })
it('updates progress during upload and failure', () => { it('updates progress during upload and failure', () => {
uploadDocumentsService.uploadFiles(fileList) const file = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
const file2 = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file2.pdf'
)
uploadDocumentsService.uploadFile(file)
uploadDocumentsService.uploadFile(file2)
expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength( expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
2 2
@ -103,7 +89,11 @@ describe('UploadDocumentsService', () => {
}) })
it('updates progress on failure', () => { it('updates progress on failure', () => {
uploadDocumentsService.uploadFiles(fileList) const file = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
uploadDocumentsService.uploadFile(file)
let req = httpTestingController.match( let req = httpTestingController.match(
`${environment.apiBaseUrl}documents/post_document/` `${environment.apiBaseUrl}documents/post_document/`
@ -125,7 +115,7 @@ describe('UploadDocumentsService', () => {
websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED) websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
).toHaveLength(1) ).toHaveLength(1)
uploadDocumentsService.uploadFiles(fileList) uploadDocumentsService.uploadFile(file)
req = httpTestingController.match( req = httpTestingController.match(
`${environment.apiBaseUrl}documents/post_document/` `${environment.apiBaseUrl}documents/post_document/`
@ -143,35 +133,4 @@ describe('UploadDocumentsService', () => {
websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED) websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
).toHaveLength(2) ).toHaveLength(2)
}) })
it('accepts files via drag and drop', () => {
const uploadSpy = jest.spyOn(
UploadDocumentsService.prototype as any,
'uploadFile'
)
const fileEntry = {
name: 'file.pdf',
isDirectory: false,
isFile: true,
file: (callback) => {
return callback(
new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
)
},
}
uploadDocumentsService.onNgxFileDrop([
{
relativePath: 'path/to/file.pdf',
fileEntry,
},
])
expect(uploadSpy).toHaveBeenCalled()
let req = httpTestingController.match(
`${environment.apiBaseUrl}documents/post_document/`
)
})
}) })

View File

@ -1,6 +1,5 @@
import { HttpEventType } from '@angular/common/http' import { HttpEventType } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { DocumentService } from './rest/document.service' import { DocumentService } from './rest/document.service'
import { import {
@ -19,22 +18,7 @@ export class UploadDocumentsService {
private websocketStatusService: WebsocketStatusService private websocketStatusService: WebsocketStatusService
) {} ) {}
onNgxFileDrop(files: NgxFileDropEntry[]) { public uploadFile(file: File) {
for (const droppedFile of files) {
if (droppedFile.fileEntry.isFile) {
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry
fileEntry.file((file: File) => this.uploadFile(file))
}
}
}
uploadFiles(files: FileList) {
for (let index = 0; index < files.length; index++) {
this.uploadFile(files.item(index))
}
}
private uploadFile(file: File) {
let formData = new FormData() let formData = new FormData()
formData.append('document', file, file.name) formData.append('document', file, file.name)
formData.append('from_webui', 'true') formData.append('from_webui', 'true')

View File

@ -135,7 +135,6 @@ import {
} from 'ngx-bootstrap-icons' } from 'ngx-bootstrap-icons'
import { ColorSliderModule } from 'ngx-color/slider' import { ColorSliderModule } from 'ngx-color/slider'
import { CookieService } from 'ngx-cookie-service' import { CookieService } from 'ngx-cookie-service'
import { NgxFileDropModule } from 'ngx-file-drop'
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { AppRoutingModule } from './app/app-routing.module' import { AppRoutingModule } from './app/app-routing.module'
import { AppComponent } from './app/app.component' import { AppComponent } from './app/app.component'
@ -353,7 +352,6 @@ bootstrapApplication(AppComponent, {
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
PdfViewerModule, PdfViewerModule,
NgxFileDropModule,
NgSelectModule, NgSelectModule,
ColorSliderModule, ColorSliderModule,
TourNgBootstrapModule, TourNgBootstrapModule,

View File

@ -15,13 +15,16 @@ from pikepdf import Pdf
from documents.converters import convert_from_tiff_to_pdf from documents.converters import convert_from_tiff_to_pdf
from documents.data_models import ConsumableDocument from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.models import Tag from documents.models import Tag
from documents.plugins.base import ConsumeTaskPlugin from documents.plugins.base import ConsumeTaskPlugin
from documents.plugins.base import StopConsumeTaskError from documents.plugins.base import StopConsumeTaskError
from documents.plugins.helpers import ProgressManager
from documents.plugins.helpers import ProgressStatusOptions from documents.plugins.helpers import ProgressStatusOptions
from documents.utils import copy_basic_file_stats from documents.utils import copy_basic_file_stats
from documents.utils import copy_file_with_basic_stats from documents.utils import copy_file_with_basic_stats
from documents.utils import maybe_override_pixel_limit from documents.utils import maybe_override_pixel_limit
from paperless.config import BarcodeConfig
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable from collections.abc import Callable
@ -39,6 +42,7 @@ class Barcode:
page: int page: int
value: str value: str
settings: BarcodeConfig
@property @property
def is_separator(self) -> bool: def is_separator(self) -> bool:
@ -46,7 +50,7 @@ class Barcode:
Returns True if the barcode value equals the configured separation value, Returns True if the barcode value equals the configured separation value,
False otherwise False otherwise
""" """
return self.value == settings.CONSUMER_BARCODE_STRING return self.value == self.settings.barcode_string
@property @property
def is_asn(self) -> bool: def is_asn(self) -> bool:
@ -54,7 +58,7 @@ class Barcode:
Returns True if the barcode value matches the configured ASN prefix, Returns True if the barcode value matches the configured ASN prefix,
False otherwise False otherwise
""" """
return self.value.startswith(settings.CONSUMER_ASN_BARCODE_PREFIX) return self.value.startswith(self.settings.barcode_asn_prefix)
class BarcodePlugin(ConsumeTaskPlugin): class BarcodePlugin(ConsumeTaskPlugin):
@ -67,17 +71,41 @@ class BarcodePlugin(ConsumeTaskPlugin):
- ASN from barcode detection is enabled or - ASN from barcode detection is enabled or
- Barcode support is enabled and the mime type is supported - Barcode support is enabled and the mime type is supported
""" """
if settings.CONSUMER_BARCODE_TIFF_SUPPORT: if self.settings.barcode_enable_tiff_support:
supported_mimes: set[str] = {"application/pdf", "image/tiff"} supported_mimes: set[str] = {"application/pdf", "image/tiff"}
else: else:
supported_mimes = {"application/pdf"} supported_mimes = {"application/pdf"}
return ( return (
settings.CONSUMER_ENABLE_ASN_BARCODE self.settings.barcode_enable_asn
or settings.CONSUMER_ENABLE_BARCODES or self.settings.barcodes_enabled
or settings.CONSUMER_ENABLE_TAG_BARCODE or self.settings.barcode_enable_tag
) and self.input_doc.mime_type in supported_mimes ) and self.input_doc.mime_type in supported_mimes
def get_settings(self) -> BarcodeConfig:
"""
Returns the settings for this plugin (Django settings or app config)
"""
return BarcodeConfig()
def __init__(
self,
input_doc: ConsumableDocument,
metadata: DocumentMetadataOverrides,
status_mgr: ProgressManager,
base_tmp_dir: Path,
task_id: str,
) -> None:
super().__init__(
input_doc,
metadata,
status_mgr,
base_tmp_dir,
task_id,
)
# need these for able_to_run
self.settings = self.get_settings()
def setup(self) -> None: def setup(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory( self.temp_dir = tempfile.TemporaryDirectory(
dir=self.base_tmp_dir, dir=self.base_tmp_dir,
@ -99,7 +127,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
# try reading tags from barcodes # try reading tags from barcodes
if ( if (
settings.CONSUMER_ENABLE_TAG_BARCODE self.settings.barcode_enable_tag
and (tags := self.tags) is not None and (tags := self.tags) is not None
and len(tags) > 0 and len(tags) > 0
): ):
@ -110,7 +138,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
logger.info(f"Found tags in barcode: {tags}") logger.info(f"Found tags in barcode: {tags}")
# Lastly attempt to split documents # Lastly attempt to split documents
if settings.CONSUMER_ENABLE_BARCODES and ( if self.settings.barcodes_enabled and (
separator_pages := self.get_separation_pages() separator_pages := self.get_separation_pages()
): ):
# We have pages to split against # We have pages to split against
@ -155,10 +183,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Update/overwrite an ASN if possible # Update/overwrite an ASN if possible
# After splitting, as otherwise each split document gets the same ASN # After splitting, as otherwise each split document gets the same ASN
if ( if self.settings.barcode_enable_asn and (located_asn := self.asn) is not None:
settings.CONSUMER_ENABLE_ASN_BARCODE
and (located_asn := self.asn) is not None
):
logger.info(f"Found ASN in barcode: {located_asn}") logger.info(f"Found ASN in barcode: {located_asn}")
self.metadata.asn = located_asn self.metadata.asn = located_asn
@ -245,8 +270,8 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Get limit from configuration # Get limit from configuration
barcode_max_pages: int = ( barcode_max_pages: int = (
num_of_pages num_of_pages
if settings.CONSUMER_BARCODE_MAX_PAGES == 0 if self.settings.barcode_max_pages == 0
else settings.CONSUMER_BARCODE_MAX_PAGES else self.settings.barcode_max_pages
) )
if barcode_max_pages < num_of_pages: # pragma: no cover if barcode_max_pages < num_of_pages: # pragma: no cover
@ -261,7 +286,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Convert page to image # Convert page to image
page = convert_from_path( page = convert_from_path(
self.pdf_file, self.pdf_file,
dpi=settings.CONSUMER_BARCODE_DPI, dpi=self.settings.barcode_dpi,
output_folder=self.temp_dir.name, output_folder=self.temp_dir.name,
first_page=current_page_number + 1, first_page=current_page_number + 1,
last_page=current_page_number + 1, last_page=current_page_number + 1,
@ -272,7 +297,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
logger.debug(f"Image is at {page_filepath}") logger.debug(f"Image is at {page_filepath}")
# Upscale image if configured # Upscale image if configured
factor = settings.CONSUMER_BARCODE_UPSCALE factor = self.settings.barcode_upscale
if factor > 1.0: if factor > 1.0:
logger.debug( logger.debug(
f"Upscaling image by {factor} for better barcode detection", f"Upscaling image by {factor} for better barcode detection",
@ -285,7 +310,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Detect barcodes # Detect barcodes
for barcode_value in reader(page): for barcode_value in reader(page):
self.barcodes.append( self.barcodes.append(
Barcode(current_page_number, barcode_value), Barcode(current_page_number, barcode_value, self.settings),
) )
# Delete temporary image file # Delete temporary image file
@ -308,7 +333,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
def asn(self) -> int | None: def asn(self) -> int | None:
""" """
Search the parsed barcodes for any ASNs. Search the parsed barcodes for any ASNs.
The first barcode that starts with CONSUMER_ASN_BARCODE_PREFIX The first barcode that starts with barcode_asn_prefix
is considered the ASN to be used. is considered the ASN to be used.
Returns the detected ASN (or None) Returns the detected ASN (or None)
""" """
@ -317,7 +342,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Ensure the barcodes have been read # Ensure the barcodes have been read
self.detect() self.detect()
# get the first barcode that starts with CONSUMER_ASN_BARCODE_PREFIX # get the first barcode that starts with barcode_asn_prefix
asn_text: str | None = next( asn_text: str | None = next(
(x.value for x in self.barcodes if x.is_asn), (x.value for x in self.barcodes if x.is_asn),
None, None,
@ -326,7 +351,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
if asn_text: if asn_text:
logger.debug(f"Found ASN Barcode: {asn_text}") logger.debug(f"Found ASN Barcode: {asn_text}")
# remove the prefix and remove whitespace # remove the prefix and remove whitespace
asn_text = asn_text[len(settings.CONSUMER_ASN_BARCODE_PREFIX) :].strip() asn_text = asn_text[len(self.settings.barcode_asn_prefix) :].strip()
# remove non-numeric parts of the remaining string # remove non-numeric parts of the remaining string
asn_text = re.sub(r"\D", "", asn_text) asn_text = re.sub(r"\D", "", asn_text)
@ -356,9 +381,9 @@ class BarcodePlugin(ConsumeTaskPlugin):
for raw in tag_texts.split(","): for raw in tag_texts.split(","):
try: try:
tag_str: str | None = None tag_str: str | None = None
for regex in settings.CONSUMER_TAG_BARCODE_MAPPING: for regex in self.settings.barcode_tag_mapping:
if re.match(regex, raw, flags=re.IGNORECASE): if re.match(regex, raw, flags=re.IGNORECASE):
sub = settings.CONSUMER_TAG_BARCODE_MAPPING[regex] sub = self.settings.barcode_tag_mapping[regex]
tag_str = ( tag_str = (
re.sub(regex, sub, raw, flags=re.IGNORECASE) re.sub(regex, sub, raw, flags=re.IGNORECASE)
if sub if sub
@ -394,13 +419,13 @@ class BarcodePlugin(ConsumeTaskPlugin):
""" """
# filter all barcodes for the separator string # filter all barcodes for the separator string
# get the page numbers of the separating barcodes # get the page numbers of the separating barcodes
retain = settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES retain = self.settings.barcode_retain_split_pages
separator_pages = { separator_pages = {
bc.page: retain bc.page: retain
for bc in self.barcodes for bc in self.barcodes
if bc.is_separator and (not retain or (retain and bc.page > 0)) if bc.is_separator and (not retain or (retain and bc.page > 0))
} # as below, dont include the first page if retain is enabled } # as below, dont include the first page if retain is enabled
if not settings.CONSUMER_ENABLE_ASN_BARCODE: if not self.settings.barcode_enable_asn:
return separator_pages return separator_pages
# add the page numbers of the ASN barcodes # add the page numbers of the ASN barcodes

View File

@ -0,0 +1,22 @@
# Generated by Django 5.1.7 on 2025-04-15 19:18
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "1065_workflowaction_assign_custom_fields_values"),
]
operations = [
migrations.AlterField(
model_name="workflowtrigger",
name="schedule_offset_days",
field=models.IntegerField(
default=0,
help_text="The number of days to offset the schedule trigger by.",
verbose_name="schedule offset days",
),
),
]

View File

@ -1019,7 +1019,7 @@ class WorkflowTrigger(models.Model):
verbose_name=_("has this correspondent"), verbose_name=_("has this correspondent"),
) )
schedule_offset_days = models.PositiveIntegerField( schedule_offset_days = models.IntegerField(
_("schedule offset days"), _("schedule offset days"),
default=0, default=0,
help_text=_( help_text=_(

View File

@ -1,8 +1,8 @@
import datetime
import hashlib import hashlib
import logging import logging
import shutil import shutil
import uuid import uuid
from datetime import timedelta
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@ -357,7 +357,7 @@ def empty_trash(doc_ids=None):
if doc_ids is not None if doc_ids is not None
else Document.deleted_objects.filter( else Document.deleted_objects.filter(
deleted_at__lt=timezone.localtime(timezone.now()) deleted_at__lt=timezone.localtime(timezone.now())
- timedelta( - datetime.timedelta(
days=settings.EMPTY_TRASH_DELAY, days=settings.EMPTY_TRASH_DELAY,
), ),
) )
@ -397,6 +397,7 @@ def check_scheduled_workflows():
) )
if scheduled_workflows.count() > 0: if scheduled_workflows.count() > 0:
logger.debug(f"Checking {len(scheduled_workflows)} scheduled workflows") logger.debug(f"Checking {len(scheduled_workflows)} scheduled workflows")
now = timezone.now()
for workflow in scheduled_workflows: for workflow in scheduled_workflows:
schedule_triggers = workflow.triggers.filter( schedule_triggers = workflow.triggers.filter(
type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED, type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED,
@ -404,31 +405,60 @@ def check_scheduled_workflows():
trigger: WorkflowTrigger trigger: WorkflowTrigger
for trigger in schedule_triggers: for trigger in schedule_triggers:
documents = Document.objects.none() documents = Document.objects.none()
offset_td = timedelta(days=trigger.schedule_offset_days) offset_td = datetime.timedelta(days=-trigger.schedule_offset_days)
threshold = now - offset_td
logger.debug( logger.debug(
f"Checking trigger {trigger} with offset {offset_td} against field: {trigger.schedule_date_field}", f"Trigger {trigger.id}: checking if (date + {offset_td}) <= now ({now})",
) )
match trigger.schedule_date_field: match trigger.schedule_date_field:
case WorkflowTrigger.ScheduleDateField.ADDED: case WorkflowTrigger.ScheduleDateField.ADDED:
documents = Document.objects.filter( documents = Document.objects.filter(added__lte=threshold)
added__lt=timezone.now() - offset_td,
)
case WorkflowTrigger.ScheduleDateField.CREATED: case WorkflowTrigger.ScheduleDateField.CREATED:
documents = Document.objects.filter( documents = Document.objects.filter(created__lte=threshold)
created__lt=timezone.now() - offset_td,
)
case WorkflowTrigger.ScheduleDateField.MODIFIED: case WorkflowTrigger.ScheduleDateField.MODIFIED:
documents = Document.objects.filter( documents = Document.objects.filter(modified__lte=threshold)
modified__lt=timezone.now() - offset_td,
)
case WorkflowTrigger.ScheduleDateField.CUSTOM_FIELD: case WorkflowTrigger.ScheduleDateField.CUSTOM_FIELD:
cf_instances = CustomFieldInstance.objects.filter( # cap earliest date to avoid massive scans
field=trigger.schedule_date_custom_field, earliest_date = now - datetime.timedelta(days=365)
value_date__lt=timezone.now() - offset_td, if offset_td.days < -365:
) logger.warning(
documents = Document.objects.filter( f"Trigger {trigger.id} has large negative offset ({offset_td.days}), "
id__in=cf_instances.values_list("document", flat=True), f"limiting earliest scan date to {earliest_date}",
)
cf_filter_kwargs = {
"field": trigger.schedule_date_custom_field,
"value_date__isnull": False,
"value_date__lte": threshold,
"value_date__gte": earliest_date,
}
recent_cf_instances = CustomFieldInstance.objects.filter(
**cf_filter_kwargs,
) )
matched_ids = [
cfi.document_id
for cfi in recent_cf_instances
if cfi.value_date
and (
timezone.make_aware(
datetime.datetime.combine(
cfi.value_date,
datetime.time.min,
),
)
+ offset_td
<= now
)
]
documents = Document.objects.filter(id__in=matched_ids)
if documents.count() > 0: if documents.count() > 0:
logger.debug( logger.debug(
f"Found {documents.count()} documents for trigger {trigger}", f"Found {documents.count()} documents for trigger {trigger}",
@ -440,18 +470,18 @@ def check_scheduled_workflows():
workflow=workflow, workflow=workflow,
).order_by("-run_at") ).order_by("-run_at")
if not trigger.schedule_is_recurring and workflow_runs.exists(): if not trigger.schedule_is_recurring and workflow_runs.exists():
# schedule is non-recurring and the workflow has already been run
logger.debug( logger.debug(
f"Skipping document {document} for non-recurring workflow {workflow} as it has already been run", f"Skipping document {document} for non-recurring workflow {workflow} as it has already been run",
) )
continue continue
elif (
if (
trigger.schedule_is_recurring trigger.schedule_is_recurring
and workflow_runs.exists() and workflow_runs.exists()
and ( and (
workflow_runs.last().run_at workflow_runs.last().run_at
> timezone.now() > now
- timedelta( - datetime.timedelta(
days=trigger.schedule_recurring_interval_days, days=trigger.schedule_recurring_interval_days,
) )
) )

View File

@ -32,28 +32,39 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.maxDiff = None
json.dumps(response.data[0]),
json.dumps( self.assertDictEqual(
{ response.data[0],
"id": 1, {
"user_args": None, "id": 1,
"output_type": None, "output_type": None,
"pages": None, "pages": None,
"language": None, "language": None,
"mode": None, "mode": None,
"skip_archive_file": None, "skip_archive_file": None,
"image_dpi": None, "image_dpi": None,
"unpaper_clean": None, "unpaper_clean": None,
"deskew": None, "deskew": None,
"rotate_pages": None, "rotate_pages": None,
"rotate_pages_threshold": None, "rotate_pages_threshold": None,
"max_image_pixels": None, "max_image_pixels": None,
"color_conversion_strategy": None, "color_conversion_strategy": None,
"app_title": None, "user_args": None,
"app_logo": None, "app_title": None,
}, "app_logo": None,
), "barcodes_enabled": None,
"barcode_enable_tiff_support": None,
"barcode_string": None,
"barcode_retain_split_pages": None,
"barcode_enable_asn": None,
"barcode_asn_prefix": None,
"barcode_upscale": None,
"barcode_dpi": None,
"barcode_max_pages": None,
"barcode_enable_tag": None,
"barcode_tag_mapping": None,
},
) )
def test_api_get_ui_settings_with_config(self): def test_api_get_ui_settings_with_config(self):
@ -118,6 +129,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
{ {
"user_args": "", "user_args": "",
"language": "", "language": "",
"barcode_tag_mapping": "",
}, },
), ),
content_type="application/json", content_type="application/json",
@ -126,6 +138,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
config = ApplicationConfiguration.objects.first() config = ApplicationConfiguration.objects.first()
self.assertEqual(config.user_args, None) self.assertEqual(config.user_args, None)
self.assertEqual(config.language, None) self.assertEqual(config.language, None)
self.assertEqual(config.barcode_tag_mapping, None)
def test_api_replace_app_logo(self): def test_api_replace_app_logo(self):
""" """

View File

@ -136,6 +136,36 @@ class TestApiProfile(DirectoriesMixin, APITestCase):
], ],
) )
def test_profile_w_social_removed_app(self):
"""
GIVEN:
- Configured user and setup social account
- Social app has been removed
WHEN:
- API call is made to get profile
THEN:
- Profile is returned with "Unknown App" as name
"""
self.setupSocialAccount()
# Remove the social app
SocialApp.objects.get(provider_id="keycloak-test").delete()
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data["social_accounts"],
[
{
"id": 1,
"provider": "keycloak-test",
"name": "Unknown App",
},
],
)
def test_update_profile(self): def test_update_profile(self):
""" """
GIVEN: GIVEN:

View File

@ -22,6 +22,7 @@ from documents.tests.utils import DocumentConsumeDelayMixin
from documents.tests.utils import DummyProgressManager from documents.tests.utils import DummyProgressManager
from documents.tests.utils import FileSystemAssertsMixin from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import SampleDirMixin from documents.tests.utils import SampleDirMixin
from paperless.models import ApplicationConfiguration
try: try:
import zxingcpp # noqa: F401 import zxingcpp # noqa: F401
@ -547,6 +548,27 @@ class TestBarcode(
}, },
) )
def test_barcode_config(self):
"""
GIVEN:
- Barcode app config is set (settings are not)
WHEN:
- Document with barcode is processed
THEN:
- The barcode config is used
"""
app_config = ApplicationConfiguration.objects.first()
app_config.barcodes_enabled = True
app_config.barcode_string = "CUSTOM BARCODE"
app_config.save()
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-custom.pdf"
with self.get_reader(test_file) as reader:
reader.detect()
separator_page_numbers = reader.get_separation_pages()
self.assertEqual(reader.pdf_file, test_file)
self.assertDictEqual(separator_page_numbers, {0: False})
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR") @override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
class TestBarcodeNewConsume( class TestBarcodeNewConsume(

View File

@ -1336,6 +1336,8 @@ class TestWorkflows(
GIVEN: GIVEN:
- Existing workflow with SCHEDULED trigger against the created field and action that assigns owner - Existing workflow with SCHEDULED trigger against the created field and action that assigns owner
- Existing doc that matches the trigger - Existing doc that matches the trigger
- Workflow set to trigger at (now - offset) = now - 1 day
- Document created date is 2 days ago trigger condition met
WHEN: WHEN:
- Scheduled workflows are checked - Scheduled workflows are checked
THEN: THEN:
@ -1359,7 +1361,7 @@ class TestWorkflows(
w.save() w.save()
now = timezone.localtime(timezone.now()) now = timezone.localtime(timezone.now())
created = now - timedelta(weeks=520) created = now - timedelta(days=2)
doc = Document.objects.create( doc = Document.objects.create(
title="sample test", title="sample test",
correspondent=self.c, correspondent=self.c,
@ -1377,6 +1379,8 @@ class TestWorkflows(
GIVEN: GIVEN:
- Existing workflow with SCHEDULED trigger against the added field and action that assigns owner - Existing workflow with SCHEDULED trigger against the added field and action that assigns owner
- Existing doc that matches the trigger - Existing doc that matches the trigger
- Workflow set to trigger at (now - offset) = now - 1 day
- Document added date is 365 days ago
WHEN: WHEN:
- Scheduled workflows are checked - Scheduled workflows are checked
THEN: THEN:
@ -1418,6 +1422,8 @@ class TestWorkflows(
GIVEN: GIVEN:
- Existing workflow with SCHEDULED trigger against the modified field and action that assigns owner - Existing workflow with SCHEDULED trigger against the modified field and action that assigns owner
- Existing doc that matches the trigger - Existing doc that matches the trigger
- Workflow set to trigger at (now - offset) = now - 1 day
- Document modified date is mocked as sufficiently in the past
WHEN: WHEN:
- Scheduled workflows are checked - Scheduled workflows are checked
THEN: THEN:
@ -1458,6 +1464,8 @@ class TestWorkflows(
GIVEN: GIVEN:
- Existing workflow with SCHEDULED trigger against a custom field and action that assigns owner - Existing workflow with SCHEDULED trigger against a custom field and action that assigns owner
- Existing doc that matches the trigger - Existing doc that matches the trigger
- Workflow set to trigger at (now - offset) = now - 1 day
- Custom field date is 2 days ago
WHEN: WHEN:
- Scheduled workflows are checked - Scheduled workflows are checked
THEN: THEN:
@ -1502,6 +1510,7 @@ class TestWorkflows(
GIVEN: GIVEN:
- Existing workflow with SCHEDULED trigger - Existing workflow with SCHEDULED trigger
- Existing doc that has already had the workflow run - Existing doc that has already had the workflow run
- Document created 2 days ago, workflow offset = 1 day trigger time = yesterday
WHEN: WHEN:
- Scheduled workflows are checked - Scheduled workflows are checked
THEN: THEN:
@ -1552,6 +1561,7 @@ class TestWorkflows(
GIVEN: GIVEN:
- Existing workflow with SCHEDULED trigger and recurring interval of 7 days - Existing workflow with SCHEDULED trigger and recurring interval of 7 days
- Workflow run date is 6 days ago - Workflow run date is 6 days ago
- Document created 40 days ago, offset = 30 trigger time = 10 days ago
WHEN: WHEN:
- Scheduled workflows are checked - Scheduled workflows are checked
THEN: THEN:
@ -1600,6 +1610,58 @@ class TestWorkflows(
doc.refresh_from_db() doc.refresh_from_db()
self.assertIsNone(doc.owner) self.assertIsNone(doc.owner)
def test_workflow_scheduled_trigger_negative_offset(self):
"""
GIVEN:
- Existing workflow with SCHEDULED trigger and negative offset of -7 days (so 7 days after date)
- Custom field date initially set to 5 days ago trigger time = 2 days in future
- Then updated to 8 days ago trigger time = 1 day ago
WHEN:
- Scheduled workflows are checked for document with custom field date 8 days in the past
THEN:
- Workflow runs and document owner is updated
"""
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED,
schedule_offset_days=-7,
schedule_date_field=WorkflowTrigger.ScheduleDateField.CUSTOM_FIELD,
schedule_date_custom_field=self.cf1,
)
action = WorkflowAction.objects.create(
assign_title="Doc assign owner",
assign_owner=self.user2,
)
w = Workflow.objects.create(
name="Workflow 1",
order=0,
)
w.triggers.add(trigger)
w.actions.add(action)
w.save()
doc = Document.objects.create(
title="sample test",
correspondent=self.c,
original_filename="sample.pdf",
)
cfi = CustomFieldInstance.objects.create(
document=doc,
field=self.cf1,
value_date=timezone.now() - timedelta(days=5),
)
tasks.check_scheduled_workflows()
doc.refresh_from_db()
self.assertIsNone(doc.owner) # has not triggered yet
cfi.value_date = timezone.now() - timedelta(days=8)
cfi.save()
tasks.check_scheduled_workflows()
doc.refresh_from_db()
self.assertEqual(doc.owner, self.user2)
def test_workflow_enabled_disabled(self): def test_workflow_enabled_disabled(self):
trigger = WorkflowTrigger.objects.create( trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED, type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,

View File

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 16:31+0000\n" "POT-Creation-Date: 2025-05-11 19:44+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n" "PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: English\n" "Language-Team: English\n"
@ -1589,15 +1589,59 @@ msgstr ""
msgid "Adds additional user arguments for OCRMyPDF" msgid "Adds additional user arguments for OCRMyPDF"
msgstr "" msgstr ""
#: paperless/models.py:171 #: paperless/models.py:175
msgid "Application title" msgid "Application title"
msgstr "" msgstr ""
#: paperless/models.py:178 #: paperless/models.py:182
msgid "Application logo" msgid "Application logo"
msgstr "" msgstr ""
#: paperless/models.py:188 #: paperless/models.py:197
msgid "Enables barcode scanning"
msgstr ""
#: paperless/models.py:203
msgid "Enables barcode TIFF support"
msgstr ""
#: paperless/models.py:209
msgid "Sets the barcode string"
msgstr ""
#: paperless/models.py:217
msgid "Retains split pages"
msgstr ""
#: paperless/models.py:223
msgid "Enables ASN barcode"
msgstr ""
#: paperless/models.py:229
msgid "Sets the ASN barcode prefix"
msgstr ""
#: paperless/models.py:237
msgid "Sets the barcode upscale factor"
msgstr ""
#: paperless/models.py:244
msgid "Sets the barcode DPI"
msgstr ""
#: paperless/models.py:251
msgid "Sets the maximum pages for barcode"
msgstr ""
#: paperless/models.py:258
msgid "Enables tag barcode"
msgstr ""
#: paperless/models.py:264
msgid "Sets the tag barcode mapping"
msgstr ""
#: paperless/models.py:269
msgid "paperless application settings" msgid "paperless application settings"
msgstr "" msgstr ""

View File

@ -96,10 +96,65 @@ class OcrConfig(OutputTypeConfig):
user_args = json.loads(settings.OCR_USER_ARGS) user_args = json.loads(settings.OCR_USER_ARGS)
except json.JSONDecodeError: except json.JSONDecodeError:
user_args = {} user_args = {}
self.user_args = user_args self.user_args = user_args
@dataclasses.dataclass
class BarcodeConfig(BaseConfig):
"""
Barcodes settings
"""
barcodes_enabled: bool = dataclasses.field(init=False)
barcode_enable_tiff_support: bool = dataclasses.field(init=False)
barcode_string: str = dataclasses.field(init=False)
barcode_retain_split_pages: bool = dataclasses.field(init=False)
barcode_enable_asn: bool = dataclasses.field(init=False)
barcode_asn_prefix: str = dataclasses.field(init=False)
barcode_upscale: float = dataclasses.field(init=False)
barcode_dpi: int = dataclasses.field(init=False)
barcode_max_pages: int = dataclasses.field(init=False)
barcode_enable_tag: bool = dataclasses.field(init=False)
barcode_tag_mapping: dict[str, str] = dataclasses.field(init=False)
def __post_init__(self) -> None:
app_config = self._get_config_instance()
self.barcodes_enabled = (
app_config.barcodes_enabled or settings.CONSUMER_ENABLE_BARCODES
)
self.barcode_enable_tiff_support = (
app_config.barcode_enable_tiff_support
or settings.CONSUMER_BARCODE_TIFF_SUPPORT
)
self.barcode_string = (
app_config.barcode_string or settings.CONSUMER_BARCODE_STRING
)
self.barcode_retain_split_pages = (
app_config.barcode_retain_split_pages
or settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES
)
self.barcode_enable_asn = (
app_config.barcode_enable_asn or settings.CONSUMER_ENABLE_ASN_BARCODE
)
self.barcode_asn_prefix = (
app_config.barcode_asn_prefix or settings.CONSUMER_ASN_BARCODE_PREFIX
)
self.barcode_upscale = (
app_config.barcode_upscale or settings.CONSUMER_BARCODE_UPSCALE
)
self.barcode_dpi = app_config.barcode_dpi or settings.CONSUMER_BARCODE_DPI
self.barcode_max_pages = (
app_config.barcode_max_pages or settings.CONSUMER_BARCODE_MAX_PAGES
)
self.barcode_enable_tag = (
app_config.barcode_enable_tag or settings.CONSUMER_ENABLE_TAG_BARCODE
)
self.barcode_tag_mapping = (
app_config.barcode_tag_mapping or settings.CONSUMER_TAG_BARCODE_MAPPING
)
@dataclasses.dataclass @dataclasses.dataclass
class GeneralConfig(BaseConfig): class GeneralConfig(BaseConfig):
""" """

View File

@ -0,0 +1,100 @@
# Generated by Django 5.1.7 on 2025-04-02 19:21
import django.core.validators
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("paperless", "0003_alter_applicationconfiguration_max_image_pixels"),
]
operations = [
migrations.AddField(
model_name="applicationconfiguration",
name="barcode_asn_prefix",
field=models.CharField(
blank=True,
max_length=32,
null=True,
verbose_name="Sets the ASN barcode prefix",
),
),
migrations.AddField(
model_name="applicationconfiguration",
name="barcode_dpi",
field=models.PositiveIntegerField(
null=True,
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="Sets the barcode DPI",
),
),
migrations.AddField(
model_name="applicationconfiguration",
name="barcode_enable_asn",
field=models.BooleanField(null=True, verbose_name="Enables ASN barcode"),
),
migrations.AddField(
model_name="applicationconfiguration",
name="barcode_enable_tag",
field=models.BooleanField(null=True, verbose_name="Enables tag barcode"),
),
migrations.AddField(
model_name="applicationconfiguration",
name="barcode_enable_tiff_support",
field=models.BooleanField(
null=True,
verbose_name="Enables barcode TIFF support",
),
),
migrations.AddField(
model_name="applicationconfiguration",
name="barcode_max_pages",
field=models.PositiveIntegerField(
null=True,
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="Sets the maximum pages for barcode",
),
),
migrations.AddField(
model_name="applicationconfiguration",
name="barcode_retain_split_pages",
field=models.BooleanField(null=True, verbose_name="Retains split pages"),
),
migrations.AddField(
model_name="applicationconfiguration",
name="barcode_string",
field=models.CharField(
blank=True,
max_length=32,
null=True,
verbose_name="Sets the barcode string",
),
),
migrations.AddField(
model_name="applicationconfiguration",
name="barcode_tag_mapping",
field=models.JSONField(
null=True,
verbose_name="Sets the tag barcode mapping",
),
),
migrations.AddField(
model_name="applicationconfiguration",
name="barcode_upscale",
field=models.FloatField(
null=True,
validators=[django.core.validators.MinValueValidator(1.0)],
verbose_name="Sets the barcode upscale factor",
),
),
migrations.AddField(
model_name="applicationconfiguration",
name="barcodes_enabled",
field=models.BooleanField(
null=True,
verbose_name="Enables barcode scanning",
),
),
]

View File

@ -167,6 +167,10 @@ class ApplicationConfiguration(AbstractSingletonModel):
null=True, null=True,
) )
"""
Settings for the Paperless application
"""
app_title = models.CharField( app_title = models.CharField(
verbose_name=_("Application title"), verbose_name=_("Application title"),
null=True, null=True,
@ -184,6 +188,83 @@ class ApplicationConfiguration(AbstractSingletonModel):
upload_to="logo/", upload_to="logo/",
) )
"""
Settings for the barcode scanner
"""
# PAPERLESS_CONSUMER_ENABLE_BARCODES
barcodes_enabled = models.BooleanField(
verbose_name=_("Enables barcode scanning"),
null=True,
)
# PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT
barcode_enable_tiff_support = models.BooleanField(
verbose_name=_("Enables barcode TIFF support"),
null=True,
)
# PAPERLESS_CONSUMER_BARCODE_STRING
barcode_string = models.CharField(
verbose_name=_("Sets the barcode string"),
null=True,
blank=True,
max_length=32,
)
# PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES
barcode_retain_split_pages = models.BooleanField(
verbose_name=_("Retains split pages"),
null=True,
)
# PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE
barcode_enable_asn = models.BooleanField(
verbose_name=_("Enables ASN barcode"),
null=True,
)
# PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX
barcode_asn_prefix = models.CharField(
verbose_name=_("Sets the ASN barcode prefix"),
null=True,
blank=True,
max_length=32,
)
# PAPERLESS_CONSUMER_BARCODE_UPSCALE
barcode_upscale = models.FloatField(
verbose_name=_("Sets the barcode upscale factor"),
null=True,
validators=[MinValueValidator(1.0)],
)
# PAPERLESS_CONSUMER_BARCODE_DPI
barcode_dpi = models.PositiveIntegerField(
verbose_name=_("Sets the barcode DPI"),
null=True,
validators=[MinValueValidator(1)],
)
# PAPERLESS_CONSUMER_BARCODE_MAX_PAGES
barcode_max_pages = models.PositiveIntegerField(
verbose_name=_("Sets the maximum pages for barcode"),
null=True,
validators=[MinValueValidator(1)],
)
# PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE
barcode_enable_tag = models.BooleanField(
verbose_name=_("Enables tag barcode"),
null=True,
)
# PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING
barcode_tag_mapping = models.JSONField(
verbose_name=_("Sets the tag barcode mapping"),
null=True,
)
class Meta: class Meta:
verbose_name = _("paperless application settings") verbose_name = _("paperless application settings")

View File

@ -4,6 +4,7 @@ from allauth.mfa.adapter import get_adapter as get_mfa_adapter
from allauth.mfa.models import Authenticator from allauth.mfa.models import Authenticator
from allauth.mfa.totp.internal.auth import TOTP from allauth.mfa.totp.internal.auth import TOTP
from allauth.socialaccount.models import SocialAccount from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.models import SocialApp
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -146,8 +147,11 @@ class SocialAccountSerializer(serializers.ModelSerializer):
"name", "name",
) )
def get_name(self, obj) -> str: def get_name(self, obj: SocialAccount) -> str:
return obj.get_provider_account().to_str() try:
return obj.get_provider_account().to_str()
except SocialApp.DoesNotExist:
return "Unknown App"
class ProfileSerializer(serializers.ModelSerializer): class ProfileSerializer(serializers.ModelSerializer):
@ -185,11 +189,14 @@ class ProfileSerializer(serializers.ModelSerializer):
class ApplicationConfigurationSerializer(serializers.ModelSerializer): class ApplicationConfigurationSerializer(serializers.ModelSerializer):
user_args = serializers.JSONField(binary=True, allow_null=True) user_args = serializers.JSONField(binary=True, allow_null=True)
barcode_tag_mapping = serializers.JSONField(binary=True, allow_null=True)
def run_validation(self, data): def run_validation(self, data):
# Empty strings treated as None to avoid unexpected behavior # Empty strings treated as None to avoid unexpected behavior
if "user_args" in data and data["user_args"] == "": if "user_args" in data and data["user_args"] == "":
data["user_args"] = None data["user_args"] = None
if "barcode_tag_mapping" in data and data["barcode_tag_mapping"] == "":
data["barcode_tag_mapping"] = None
if "language" in data and data["language"] == "": if "language" in data and data["language"] == "":
data["language"] = None data["language"] = None
return super().run_validation(data) return super().run_validation(data)