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,
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
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:

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 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">201</context>
<context context-type="linenumber">209</context>
</context-group>
<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">220</context>
<context context-type="linenumber">228</context>
</context-group>
<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">287</context>
<context context-type="linenumber">295</context>
</context-group>
<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">306</context>
<context context-type="linenumber">314</context>
</context-group>
<context-group purpose="location">
<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 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">209</context>
<context context-type="linenumber">217</context>
</context-group>
<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">228</context>
<context context-type="linenumber">236</context>
</context-group>
<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">295</context>
<context context-type="linenumber">303</context>
</context-group>
<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">314</context>
<context context-type="linenumber">322</context>
</context-group>
<context-group purpose="location">
<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 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">234</context>
<context context-type="linenumber">242</context>
</context-group>
<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">320</context>
<context context-type="linenumber">328</context>
</context-group>
<context-group purpose="location">
<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 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">163</context>
<context context-type="linenumber">171</context>
</context-group>
</trans-unit>
<trans-unit id="6457471243969293847" datatype="html">
@ -4115,7 +4115,7 @@
</context-group>
<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">188</context>
<context context-type="linenumber">196</context>
</context-group>
</trans-unit>
<trans-unit id="4754802869258527587" datatype="html">
@ -4133,7 +4133,7 @@
</context-group>
<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">189</context>
<context context-type="linenumber">197</context>
</context-group>
</trans-unit>
<trans-unit id="1519954996184640001" datatype="html">
@ -4645,381 +4645,388 @@
<source>Offset days</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">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>
</trans-unit>
<trans-unit id="3726450101884717309" datatype="html">
<source>Relative to</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">129</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit>
<trans-unit id="3878884308536053934" datatype="html">
<source>Custom field</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">133</context>
<context context-type="linenumber">141</context>
</context-group>
</trans-unit>
<trans-unit id="1088170562604583291" datatype="html">
<source>Custom field to use for date.</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">133</context>
<context context-type="linenumber">141</context>
</context-group>
</trans-unit>
<trans-unit id="1011433830042635014" datatype="html">
<source>Recurring</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">139</context>
<context context-type="linenumber">147</context>
</context-group>
</trans-unit>
<trans-unit id="1421663004162437543" datatype="html">
<source>Trigger is recurring.</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">139</context>
<context context-type="linenumber">147</context>
</context-group>
</trans-unit>
<trans-unit id="5937989815294159481" datatype="html">
<source>Recurring interval days</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">143</context>
<context context-type="linenumber">151</context>
</context-group>
</trans-unit>
<trans-unit id="722765958672682251" datatype="html">
<source>Repeat the trigger every n days.</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">143</context>
<context context-type="linenumber">151</context>
</context-group>
</trans-unit>
<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>
<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">148</context>
<context context-type="linenumber">156</context>
</context-group>
</trans-unit>
<trans-unit id="7467799586957602479" datatype="html">
<source>Filter filename</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">151</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<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>
<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">151</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit id="1473412958770421458" datatype="html">
<source>Filter sources</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">153</context>
<context context-type="linenumber">161</context>
</context-group>
</trans-unit>
<trans-unit id="6540860478788535250" datatype="html">
<source>Filter path</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">154</context>
<context context-type="linenumber">162</context>
</context-group>
</trans-unit>
<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>
<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">154</context>
<context context-type="linenumber">162</context>
</context-group>
</trans-unit>
<trans-unit id="7468453896129193641" datatype="html">
<source>Filter mail rule</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">155</context>
<context context-type="linenumber">163</context>
</context-group>
</trans-unit>
<trans-unit id="8663702115863339485" datatype="html">
<source>Apply to documents consumed via this mail rule.</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">155</context>
<context context-type="linenumber">163</context>
</context-group>
</trans-unit>
<trans-unit id="6840369584127435743" datatype="html">
<source>Content matching algorithm</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">158</context>
<context context-type="linenumber">166</context>
</context-group>
</trans-unit>
<trans-unit id="510635115034690805" datatype="html">
<source>Content matching pattern</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">160</context>
<context context-type="linenumber">168</context>
</context-group>
</trans-unit>
<trans-unit id="3484236514968690689" datatype="html">
<source>Has any of tags</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">169</context>
<context context-type="linenumber">177</context>
</context-group>
</trans-unit>
<trans-unit id="5281365940563983618" datatype="html">
<source>Has correspondent</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">170</context>
<context context-type="linenumber">178</context>
</context-group>
</trans-unit>
<trans-unit id="4806713133917046341" datatype="html">
<source>Has document type</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">171</context>
<context context-type="linenumber">179</context>
</context-group>
</trans-unit>
<trans-unit id="6417103744331194518" datatype="html">
<source>Action type</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">181</context>
<context context-type="linenumber">189</context>
</context-group>
</trans-unit>
<trans-unit id="6019822389883736115" datatype="html">
<source>Assign title</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">186</context>
<context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<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>
<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">186</context>
<context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<trans-unit id="6528897010417701530" datatype="html">
<source>Assign tags</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">187</context>
<context context-type="linenumber">195</context>
</context-group>
</trans-unit>
<trans-unit id="7198346314713788799" datatype="html">
<source>Assign storage path</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">190</context>
<context context-type="linenumber">198</context>
</context-group>
</trans-unit>
<trans-unit id="475685412372379925" datatype="html">
<source>Assign custom fields</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">191</context>
<context context-type="linenumber">199</context>
</context-group>
</trans-unit>
<trans-unit id="5057200219587080996" datatype="html">
<source>Assign owner</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">195</context>
<context context-type="linenumber">203</context>
</context-group>
</trans-unit>
<trans-unit id="1749184201773078639" datatype="html">
<source>Assign view permissions</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">197</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="1744964187586405039" datatype="html">
<source>Assign edit permissions</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">216</context>
<context context-type="linenumber">224</context>
</context-group>
</trans-unit>
<trans-unit id="6236311670364192011" datatype="html">
<source>Remove tags</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">243</context>
<context context-type="linenumber">251</context>
</context-group>
</trans-unit>
<trans-unit id="7890599006071681081" datatype="html">
<source>Remove all</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">244</context>
<context context-type="linenumber">252</context>
</context-group>
<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">250</context>
<context context-type="linenumber">258</context>
</context-group>
<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">256</context>
<context context-type="linenumber">264</context>
</context-group>
<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">262</context>
<context context-type="linenumber">270</context>
</context-group>
<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">268</context>
<context context-type="linenumber">276</context>
</context-group>
<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">275</context>
<context context-type="linenumber">283</context>
</context-group>
<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">281</context>
<context context-type="linenumber">289</context>
</context-group>
</trans-unit>
<trans-unit id="8636414563726517994" datatype="html">
<source>Remove correspondents</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">249</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="5305293055593064952" datatype="html">
<source>Remove document types</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">255</context>
<context context-type="linenumber">263</context>
</context-group>
</trans-unit>
<trans-unit id="2400388879708187" datatype="html">
<source>Remove storage paths</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">261</context>
<context context-type="linenumber">269</context>
</context-group>
</trans-unit>
<trans-unit id="4324304327041955720" datatype="html">
<source>Remove custom fields</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">267</context>
<context context-type="linenumber">275</context>
</context-group>
</trans-unit>
<trans-unit id="8367536502602515064" datatype="html">
<source>Remove owners</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">274</context>
<context context-type="linenumber">282</context>
</context-group>
</trans-unit>
<trans-unit id="3393772184866313281" datatype="html">
<source>Remove permissions</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">280</context>
<context context-type="linenumber">288</context>
</context-group>
</trans-unit>
<trans-unit id="3145629643370481114" datatype="html">
<source>View permissions</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">283</context>
<context context-type="linenumber">291</context>
</context-group>
</trans-unit>
<trans-unit id="1946660694635960249" datatype="html">
<source>Edit permissions</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">302</context>
<context context-type="linenumber">310</context>
</context-group>
</trans-unit>
<trans-unit id="8987736563240025468" datatype="html">
<source>Email subject</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">330</context>
<context context-type="linenumber">338</context>
</context-group>
</trans-unit>
<trans-unit id="8239445959209739142" datatype="html">
<source>Email body</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">331</context>
<context context-type="linenumber">339</context>
</context-group>
</trans-unit>
<trans-unit id="1222152280703048012" datatype="html">
<source>Email recipients</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">332</context>
<context context-type="linenumber">340</context>
</context-group>
</trans-unit>
<trans-unit id="7916910101279824329" datatype="html">
<source>Attach document</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">333</context>
<context context-type="linenumber">341</context>
</context-group>
</trans-unit>
<trans-unit id="5028001922785731600" datatype="html">
<source>Webhook url</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">341</context>
<context context-type="linenumber">349</context>
</context-group>
</trans-unit>
<trans-unit id="7491983459027245019" datatype="html">
<source>Use parameters for webhook body</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">343</context>
<context context-type="linenumber">351</context>
</context-group>
</trans-unit>
<trans-unit id="4078214298308732810" datatype="html">
<source>Send webhook payload as JSON</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">344</context>
<context context-type="linenumber">352</context>
</context-group>
</trans-unit>
<trans-unit id="6806149889743731985" datatype="html">
<source>Webhook params</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">347</context>
<context context-type="linenumber">355</context>
</context-group>
</trans-unit>
<trans-unit id="7089924379374330" datatype="html">
<source>Webhook body</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">349</context>
<context context-type="linenumber">357</context>
</context-group>
</trans-unit>
<trans-unit id="3829826512656746316" datatype="html">
<source>Webhook headers</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">351</context>
<context context-type="linenumber">359</context>
</context-group>
</trans-unit>
<trans-unit id="2114525789021600887" datatype="html">
<source>Include document</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">352</context>
<context context-type="linenumber">360</context>
</context-group>
</trans-unit>
<trans-unit id="4626030417479279989" datatype="html">
@ -7901,7 +7908,7 @@
</context-group>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="329406837759048287" datatype="html">
@ -8248,7 +8255,18 @@
<source>Initiating upload...</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="6316128875819022658" datatype="html">
@ -9260,102 +9278,186 @@
<context context-type="linenumber">51</context>
</context-group>
</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">
<source>Output Type</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="2826581353496868063" datatype="html">
<source>Language</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
<source>Mode</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="6114528299376689399" datatype="html">
<source>Skip Archive File</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="1115402553541327390" datatype="html">
<source>Image DPI</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="6352596107300820129" datatype="html">
<source>Clean</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="725308589819024010" datatype="html">
<source>Deskew</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="6256076128297775802" datatype="html">
<source>Rotate Pages</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="8527188778859256947" datatype="html">
<source>Rotate Pages Threshold</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="3762131309176747817" datatype="html">
<source>Max Image Pixels</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="7846583355792281769" datatype="html">
<source>Color Conversion Strategy</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="4696480417479207939" datatype="html">
<source>OCR Arguments</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="7106327322456204362" datatype="html">
<source>Application Logo</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="2684743776608068095" datatype="html">
<source>Application Title</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="5948496158474272829" datatype="html">
@ -9826,28 +9928,28 @@
<source>Connecting...</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="1245343823699368872" datatype="html">
<source>Uploading...</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="7446520539098045935" datatype="html">
<source>Upload complete, waiting...</source>
<context-group purpose="location">
<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>
</trans-unit>
<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>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="2119857572761283468" datatype="html">

View File

@ -7,8 +7,7 @@
"start": "ng serve",
"build": "ng build",
"test": "ng test --no-watch --coverage",
"lint": "ng lint",
"postinstall": "patch-package"
"lint": "ng lint"
},
"private": true,
"dependencies": {
@ -33,7 +32,6 @@
"ngx-color": "^10.0.0",
"ngx-cookie-service": "^19.1.2",
"ngx-device-detector": "^9.0.0",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^16.0.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
@ -67,7 +65,6 @@
"jest-junit": "^16.0.0",
"jest-preset-angular": "^14.5.5",
"jest-websocket-mock": "^2.5.0",
"patch-package": "^8.0.0",
"prettier-plugin-organize-imports": "^4.1.0",
"ts-node": "~10.9.1",
"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:
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))
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:
specifier: ^16.0.0
version: 16.0.0(nfyq54qpjcnpjhdwpvrgi4nyra)
version: 16.0.0(9bfbf45bdfd1029869b23707ee6bb312)
rxjs:
specifier: ^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)
'@angular-builders/jest':
specifier: ^19.0.1
version: 19.0.1(2xzflpchozqzu223xxigbxxvgu)
version: 19.0.1(025d537e0c22047bb48e08c4eeeaebf1)
'@angular-devkit/build-angular':
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)
@ -168,9 +165,6 @@ importers:
jest-websocket-mock:
specifier: ^2.5.0
version: 2.5.0
patch-package:
specifier: ^8.0.0
version: 8.0.0
prettier-plugin-organize-imports:
specifier: ^4.1.0
version: 4.1.0(prettier@3.4.2)(typescript@5.5.4)
@ -2780,10 +2774,6 @@ packages:
asynckit@0.4.0:
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:
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
engines: {node: ^10 || ^12 || >=14}
@ -2931,10 +2921,6 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'}
call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
@ -3226,10 +3212,6 @@ packages:
defaults@1.0.4:
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:
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
engines: {node: '>=12'}
@ -3607,9 +3589,6 @@ packages:
resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==}
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:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
@ -3652,10 +3631,6 @@ packages:
fs-constants@1.0.0:
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:
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
engines: {node: '>= 8'}
@ -3692,10 +3667,6 @@ packages:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
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:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@ -3763,9 +3734,6 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
@ -3943,11 +3911,6 @@ packages:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -4020,10 +3983,6 @@ packages:
is-what@3.14.1:
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:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
engines: {node: '>=16'}
@ -4031,9 +3990,6 @@ packages:
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@ -4310,10 +4266,6 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
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:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
@ -4322,12 +4274,6 @@ packages:
jsonc-parser@3.3.1:
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:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0}
@ -4342,9 +4288,6 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
klaw-sync@6.0.0:
resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==}
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
@ -4708,13 +4651,6 @@ packages:
'@angular/common': ^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:
resolution: {integrity: sha512-6pzzEwxn/gCS3puEXDqgINBRbhvhzHYjmiA9DTCNEx1dPfYwjZVmPqNvNeZIVHucVnVZViAAKvA6MTc3Gm7aOw==}
peerDependencies:
@ -4814,10 +4750,6 @@ packages:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
obuf@1.1.2:
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
@ -4844,10 +4776,6 @@ packages:
resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==}
engines: {node: '>=18'}
open@7.4.2:
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
engines: {node: '>=8'}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@ -4935,11 +4863,6 @@ packages:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
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:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@ -5265,11 +5188,6 @@ packages:
rfdc@1.4.1:
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:
resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -5369,10 +5287,6 @@ packages:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
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:
resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
@ -5434,10 +5348,6 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slash@2.0.0:
resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
engines: {node: '>=6'}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@ -5825,10 +5735,6 @@ packages:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
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:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
@ -6233,7 +6139,7 @@ snapshots:
- webpack-cli
- yaml
'@angular-builders/jest@19.0.1(2xzflpchozqzu223xxigbxxvgu)':
'@angular-builders/jest@19.0.1(025d537e0c22047bb48e08c4eeeaebf1)':
dependencies:
'@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)
@ -8995,8 +8901,6 @@ snapshots:
asynckit@0.4.0: {}
at-least-node@1.0.0: {}
autoprefixer@10.4.20(postcss@8.5.2):
dependencies:
browserslist: 4.24.4
@ -9215,14 +9119,6 @@ snapshots:
es-errors: 1.3.0
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:
dependencies:
call-bind-apply-helpers: 1.0.2
@ -9497,12 +9393,6 @@ snapshots:
dependencies:
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: {}
delayed-stream@1.0.0: {}
@ -9970,10 +9860,6 @@ snapshots:
locate-path: 7.2.0
path-exists: 5.0.0
find-yarn-workspace-root@2.0.0:
dependencies:
micromatch: 4.0.8
flat-cache@4.0.1:
dependencies:
flatted: 3.3.3
@ -10007,13 +9893,6 @@ snapshots:
fs-constants@1.0.0:
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:
dependencies:
minipass: 3.3.6
@ -10038,19 +9917,6 @@ snapshots:
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:
dependencies:
call-bind-apply-helpers: 1.0.2
@ -10127,10 +9993,6 @@ snapshots:
has-flag@4.0.0: {}
has-property-descriptors@1.0.2:
dependencies:
es-define-property: 1.0.1
has-symbols@1.1.0: {}
hasown@2.0.2:
@ -10320,8 +10182,6 @@ snapshots:
dependencies:
hasown: 2.0.2
is-docker@2.2.1: {}
is-docker@3.0.0: {}
is-extglob@2.1.1: {}
@ -10366,18 +10226,12 @@ snapshots:
is-what@3.14.1: {}
is-wsl@2.2.0:
dependencies:
is-docker: 2.2.1
is-wsl@3.1.0:
dependencies:
is-inside-container: 1.0.0
isarray@1.0.0: {}
isarray@2.0.5: {}
isexe@2.0.0: {}
isexe@3.1.1: {}
@ -10896,25 +10750,10 @@ snapshots:
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: {}
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: {}
karma-source-map-support@1.4.0:
@ -10927,10 +10766,6 @@ snapshots:
kind-of@6.0.3: {}
klaw-sync@6.0.0:
dependencies:
graceful-fs: 4.2.11
kleur@3.0.3: {}
launch-editor@2.10.0:
@ -11289,12 +11124,6 @@ snapshots:
'@angular/core': 19.2.9(rxjs@7.8.2)(zone.js@0.15.0)
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):
dependencies:
'@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
tslib: 2.8.1
ngx-ui-tour-ng-bootstrap@16.0.0(nfyq54qpjcnpjhdwpvrgi4nyra):
ngx-ui-tour-ng-bootstrap@16.0.0(9bfbf45bdfd1029869b23707ee6bb312):
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)
@ -11412,8 +11241,6 @@ snapshots:
object-inspect@1.13.4: {}
object-keys@1.1.1: {}
obuf@1.1.2: {}
on-finished@2.4.1:
@ -11441,11 +11268,6 @@ snapshots:
is-inside-container: 1.0.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:
dependencies:
deep-is: 0.1.4
@ -11561,24 +11383,6 @@ snapshots:
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@5.0.0: {}
@ -11883,10 +11687,6 @@ snapshots:
rfdc@1.4.1: {}
rimraf@2.7.1:
dependencies:
glob: 7.2.3
rollup@4.34.8:
dependencies:
'@types/estree': 1.0.6
@ -12018,15 +11818,6 @@ snapshots:
transitivePeerDependencies:
- 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.2.0: {}
@ -12105,8 +11896,6 @@ snapshots:
sisteransi@1.0.5: {}
slash@2.0.0: {}
slash@3.0.0: {}
slash@5.1.0: {}
@ -12517,8 +12306,6 @@ snapshots:
universalify@0.2.0: {}
universalify@2.0.1: {}
unpipe@1.0.0: {}
unplugin@1.16.1:
@ -12783,7 +12570,8 @@ snapshots:
yallist@5.0.0: {}
yaml@2.7.0: {}
yaml@2.7.0:
optional: true
yargs-parser@21.1.1: {}

View File

@ -121,19 +121,4 @@ HTMLCanvasElement.prototype.getContext = <
typeof HTMLCanvasElement.prototype.getContext
>jest.fn()
// pdfjs
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', () => ({}))
jest.mock('pdfjs-dist')

View File

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

View File

@ -105,9 +105,9 @@ describe('ConfigComponent', () => {
it('should support JSON validation for e.g. user_args', () => {
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" }' })
expect(component.errors).toEqual({ user_args: null })
expect(component.errors['user_args']).toBeNull()
})
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>
<div class="row">
<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 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>

View File

@ -82,10 +82,20 @@ describe('UploadFileWidgetComponent', () => {
})
it('should upload files', () => {
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
fixture.debugElement
.query(By.css('input'))
.nativeElement.dispatchEvent(new Event('change'))
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const file = new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'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()
})

View File

@ -134,9 +134,11 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
}
public onFileSelected(event: Event) {
this.uploadDocumentsService.uploadFiles(
(event.target as HTMLInputElement).files
)
const 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 {

View File

@ -2,13 +2,6 @@
<ng-content select="[content]"></ng-content>
</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>
</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 {
opacity: 0;
transition: opacity 0.25s ease-in-out;
background-color: hsla(var(--pngx-primary), var(--pngx-primary-lightness), .8);
z-index: 1200;
h2 {
color: var(--pngx-primary-text-contrast)
}
&.active {
opacity: 1;
}
}

View File

@ -9,7 +9,6 @@ import {
tick,
} from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop'
import { PermissionsService } from 'src/app/services/permissions.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
@ -27,7 +26,7 @@ describe('FileDropComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NgxFileDropModule, FileDropComponent, ToastsComponent],
imports: [FileDropComponent, ToastsComponent],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
@ -66,12 +65,12 @@ describe('FileDropComponent', () => {
const dropzone = fixture.debugElement.query(
By.css('.global-dropzone-overlay')
)
expect(dropzone.classes['hide']).toBeTruthy()
expect(dropzone.classes['active']).toBeFalsy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
// drop
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const dragEvent = new Event('drop')
dragEvent['dataTransfer'] = {
files: {
@ -93,53 +92,209 @@ describe('FileDropComponent', () => {
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
const dropzone = fixture.debugElement.query(
By.css('.global-dropzone-overlay')
)
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
expect(dropzone.classes['hide']).toBeTruthy()
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(
UploadDocumentsService.prototype as any,
'uploadFile'
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'] = {
files: {
item: () => {
return new File(
[new Blob(['testing'], { type: 'application/pdf' })],
'file.pdf'
)
items: [
{
kind: 'file',
type: 'application/pdf',
getAsFile: () => file,
},
length: 1,
} as unknown as FileList,
],
}
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)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
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(() => {
settingsService.globalDropzoneEnabled = false
expect(settingsService.globalDropzoneActive).toBeFalsy()

View File

@ -1,9 +1,4 @@
import { Component, HostListener, ViewChild } from '@angular/core'
import {
NgxFileDropComponent,
NgxFileDropEntry,
NgxFileDropModule,
} from 'ngx-file-drop'
import { Component, HostListener } from '@angular/core'
import {
PermissionAction,
PermissionsService,
@ -17,7 +12,7 @@ import { UploadDocumentsService } from 'src/app/services/upload-documents.servic
selector: 'pngx-file-drop',
templateUrl: './file-drop.component.html',
styleUrls: ['./file-drop.component.scss'],
imports: [NgxFileDropModule],
imports: [],
})
export class FileDropComponent {
private fileLeaveTimeoutID: any
@ -41,8 +36,6 @@ export class FileDropComponent {
)
}
@ViewChild('ngxFileDrop') ngxFileDrop: NgxFileDropComponent
@HostListener('document:dragover', ['$event']) onDragOver(event: DragEvent) {
if (!this.dragDropEnabled || !event.dataTransfer?.types?.includes('Files'))
return
@ -78,19 +71,85 @@ export class FileDropComponent {
}, 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) {
if (!this.dragDropEnabled) return
event.preventDefault()
event.stopImmediatePropagation()
// pass event onto ngx-file-drop to handle files
this.ngxFileDrop.dropFiles(event)
this.onDragLeave(event, true)
}
public dropped(files: NgxFileDropEntry[]) {
this.uploadDocumentsService.onNgxFileDrop(files)
if (files.length > 0)
const files: File[] = []
const entries: FileSystemEntry[] = []
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)
files.forEach((file) => this.uploadDocumentsService.uploadFile(file))
}
this.onDragLeave(event, true)
}
@HostListener('window:blur', ['$event']) public onWindowBlur() {

View File

@ -49,6 +49,7 @@ export enum ConfigOptionType {
export const ConfigCategory = {
General: $localize`General Settings`,
OCR: $localize`OCR Settings`,
Barcode: $localize`Barcode Settings`,
}
export interface ConfigOption {
@ -180,6 +181,83 @@ export const PaperlessConfigOptions: ConfigOption[] = [
config_key: 'PAPERLESS_APP_TITLE',
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 {
@ -198,4 +276,15 @@ export interface PaperlessConfig extends ObjectWithId {
user_args: object
app_logo: 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,
} 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', () => {
let httpTestingController: HttpTestingController
let uploadDocumentsService: UploadDocumentsService
@ -68,7 +41,11 @@ describe('UploadDocumentsService', () => {
})
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(
`${environment.apiBaseUrl}documents/post_document/`
)
@ -78,7 +55,16 @@ describe('UploadDocumentsService', () => {
})
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(
2
@ -103,7 +89,11 @@ describe('UploadDocumentsService', () => {
})
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(
`${environment.apiBaseUrl}documents/post_document/`
@ -125,7 +115,7 @@ describe('UploadDocumentsService', () => {
websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
).toHaveLength(1)
uploadDocumentsService.uploadFiles(fileList)
uploadDocumentsService.uploadFile(file)
req = httpTestingController.match(
`${environment.apiBaseUrl}documents/post_document/`
@ -143,35 +133,4 @@ describe('UploadDocumentsService', () => {
websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
).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 { Injectable } from '@angular/core'
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'
import { Subscription } from 'rxjs'
import { DocumentService } from './rest/document.service'
import {
@ -19,22 +18,7 @@ export class UploadDocumentsService {
private websocketStatusService: WebsocketStatusService
) {}
onNgxFileDrop(files: NgxFileDropEntry[]) {
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) {
public uploadFile(file: File) {
let formData = new FormData()
formData.append('document', file, file.name)
formData.append('from_webui', 'true')

View File

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

View File

@ -15,13 +15,16 @@ from pikepdf import Pdf
from documents.converters import convert_from_tiff_to_pdf
from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.models import Tag
from documents.plugins.base import ConsumeTaskPlugin
from documents.plugins.base import StopConsumeTaskError
from documents.plugins.helpers import ProgressManager
from documents.plugins.helpers import ProgressStatusOptions
from documents.utils import copy_basic_file_stats
from documents.utils import copy_file_with_basic_stats
from documents.utils import maybe_override_pixel_limit
from paperless.config import BarcodeConfig
if TYPE_CHECKING:
from collections.abc import Callable
@ -39,6 +42,7 @@ class Barcode:
page: int
value: str
settings: BarcodeConfig
@property
def is_separator(self) -> bool:
@ -46,7 +50,7 @@ class Barcode:
Returns True if the barcode value equals the configured separation value,
False otherwise
"""
return self.value == settings.CONSUMER_BARCODE_STRING
return self.value == self.settings.barcode_string
@property
def is_asn(self) -> bool:
@ -54,7 +58,7 @@ class Barcode:
Returns True if the barcode value matches the configured ASN prefix,
False otherwise
"""
return self.value.startswith(settings.CONSUMER_ASN_BARCODE_PREFIX)
return self.value.startswith(self.settings.barcode_asn_prefix)
class BarcodePlugin(ConsumeTaskPlugin):
@ -67,17 +71,41 @@ class BarcodePlugin(ConsumeTaskPlugin):
- ASN from barcode detection is enabled or
- 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"}
else:
supported_mimes = {"application/pdf"}
return (
settings.CONSUMER_ENABLE_ASN_BARCODE
or settings.CONSUMER_ENABLE_BARCODES
or settings.CONSUMER_ENABLE_TAG_BARCODE
self.settings.barcode_enable_asn
or self.settings.barcodes_enabled
or self.settings.barcode_enable_tag
) 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:
self.temp_dir = tempfile.TemporaryDirectory(
dir=self.base_tmp_dir,
@ -99,7 +127,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
# try reading tags from barcodes
if (
settings.CONSUMER_ENABLE_TAG_BARCODE
self.settings.barcode_enable_tag
and (tags := self.tags) is not None
and len(tags) > 0
):
@ -110,7 +138,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
logger.info(f"Found tags in barcode: {tags}")
# Lastly attempt to split documents
if settings.CONSUMER_ENABLE_BARCODES and (
if self.settings.barcodes_enabled and (
separator_pages := self.get_separation_pages()
):
# We have pages to split against
@ -155,10 +183,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Update/overwrite an ASN if possible
# After splitting, as otherwise each split document gets the same ASN
if (
settings.CONSUMER_ENABLE_ASN_BARCODE
and (located_asn := self.asn) is not None
):
if self.settings.barcode_enable_asn and (located_asn := self.asn) is not None:
logger.info(f"Found ASN in barcode: {located_asn}")
self.metadata.asn = located_asn
@ -245,8 +270,8 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Get limit from configuration
barcode_max_pages: int = (
num_of_pages
if settings.CONSUMER_BARCODE_MAX_PAGES == 0
else settings.CONSUMER_BARCODE_MAX_PAGES
if self.settings.barcode_max_pages == 0
else self.settings.barcode_max_pages
)
if barcode_max_pages < num_of_pages: # pragma: no cover
@ -261,7 +286,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Convert page to image
page = convert_from_path(
self.pdf_file,
dpi=settings.CONSUMER_BARCODE_DPI,
dpi=self.settings.barcode_dpi,
output_folder=self.temp_dir.name,
first_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}")
# Upscale image if configured
factor = settings.CONSUMER_BARCODE_UPSCALE
factor = self.settings.barcode_upscale
if factor > 1.0:
logger.debug(
f"Upscaling image by {factor} for better barcode detection",
@ -285,7 +310,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Detect barcodes
for barcode_value in reader(page):
self.barcodes.append(
Barcode(current_page_number, barcode_value),
Barcode(current_page_number, barcode_value, self.settings),
)
# Delete temporary image file
@ -308,7 +333,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
def asn(self) -> int | None:
"""
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.
Returns the detected ASN (or None)
"""
@ -317,7 +342,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Ensure the barcodes have been read
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(
(x.value for x in self.barcodes if x.is_asn),
None,
@ -326,7 +351,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
if asn_text:
logger.debug(f"Found ASN Barcode: {asn_text}")
# 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
asn_text = re.sub(r"\D", "", asn_text)
@ -356,9 +381,9 @@ class BarcodePlugin(ConsumeTaskPlugin):
for raw in tag_texts.split(","):
try:
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):
sub = settings.CONSUMER_TAG_BARCODE_MAPPING[regex]
sub = self.settings.barcode_tag_mapping[regex]
tag_str = (
re.sub(regex, sub, raw, flags=re.IGNORECASE)
if sub
@ -394,13 +419,13 @@ class BarcodePlugin(ConsumeTaskPlugin):
"""
# filter all barcodes for the separator string
# get the page numbers of the separating barcodes
retain = settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES
retain = self.settings.barcode_retain_split_pages
separator_pages = {
bc.page: retain
for bc in self.barcodes
if bc.is_separator and (not retain or (retain and bc.page > 0))
} # 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
# 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"),
)
schedule_offset_days = models.PositiveIntegerField(
schedule_offset_days = models.IntegerField(
_("schedule offset days"),
default=0,
help_text=_(

View File

@ -1,8 +1,8 @@
import datetime
import hashlib
import logging
import shutil
import uuid
from datetime import timedelta
from pathlib import Path
from tempfile import TemporaryDirectory
@ -357,7 +357,7 @@ def empty_trash(doc_ids=None):
if doc_ids is not None
else Document.deleted_objects.filter(
deleted_at__lt=timezone.localtime(timezone.now())
- timedelta(
- datetime.timedelta(
days=settings.EMPTY_TRASH_DELAY,
),
)
@ -397,6 +397,7 @@ def check_scheduled_workflows():
)
if scheduled_workflows.count() > 0:
logger.debug(f"Checking {len(scheduled_workflows)} scheduled workflows")
now = timezone.now()
for workflow in scheduled_workflows:
schedule_triggers = workflow.triggers.filter(
type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED,
@ -404,31 +405,60 @@ def check_scheduled_workflows():
trigger: WorkflowTrigger
for trigger in schedule_triggers:
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(
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:
case WorkflowTrigger.ScheduleDateField.ADDED:
documents = Document.objects.filter(
added__lt=timezone.now() - offset_td,
)
documents = Document.objects.filter(added__lte=threshold)
case WorkflowTrigger.ScheduleDateField.CREATED:
documents = Document.objects.filter(
created__lt=timezone.now() - offset_td,
)
documents = Document.objects.filter(created__lte=threshold)
case WorkflowTrigger.ScheduleDateField.MODIFIED:
documents = Document.objects.filter(
modified__lt=timezone.now() - offset_td,
)
documents = Document.objects.filter(modified__lte=threshold)
case WorkflowTrigger.ScheduleDateField.CUSTOM_FIELD:
cf_instances = CustomFieldInstance.objects.filter(
field=trigger.schedule_date_custom_field,
value_date__lt=timezone.now() - offset_td,
)
documents = Document.objects.filter(
id__in=cf_instances.values_list("document", flat=True),
# cap earliest date to avoid massive scans
earliest_date = now - datetime.timedelta(days=365)
if offset_td.days < -365:
logger.warning(
f"Trigger {trigger.id} has large negative offset ({offset_td.days}), "
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:
logger.debug(
f"Found {documents.count()} documents for trigger {trigger}",
@ -440,18 +470,18 @@ def check_scheduled_workflows():
workflow=workflow,
).order_by("-run_at")
if not trigger.schedule_is_recurring and workflow_runs.exists():
# schedule is non-recurring and the workflow has already been run
logger.debug(
f"Skipping document {document} for non-recurring workflow {workflow} as it has already been run",
)
continue
elif (
if (
trigger.schedule_is_recurring
and workflow_runs.exists()
and (
workflow_runs.last().run_at
> timezone.now()
- timedelta(
> now
- datetime.timedelta(
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(
json.dumps(response.data[0]),
json.dumps(
{
"id": 1,
"user_args": None,
"output_type": None,
"pages": None,
"language": None,
"mode": None,
"skip_archive_file": None,
"image_dpi": None,
"unpaper_clean": None,
"deskew": None,
"rotate_pages": None,
"rotate_pages_threshold": None,
"max_image_pixels": None,
"color_conversion_strategy": None,
"app_title": None,
"app_logo": None,
},
),
self.maxDiff = None
self.assertDictEqual(
response.data[0],
{
"id": 1,
"output_type": None,
"pages": None,
"language": None,
"mode": None,
"skip_archive_file": None,
"image_dpi": None,
"unpaper_clean": None,
"deskew": None,
"rotate_pages": None,
"rotate_pages_threshold": None,
"max_image_pixels": None,
"color_conversion_strategy": None,
"user_args": 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):
@ -118,6 +129,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
{
"user_args": "",
"language": "",
"barcode_tag_mapping": "",
},
),
content_type="application/json",
@ -126,6 +138,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
config = ApplicationConfiguration.objects.first()
self.assertEqual(config.user_args, None)
self.assertEqual(config.language, None)
self.assertEqual(config.barcode_tag_mapping, None)
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):
"""
GIVEN:

View File

@ -22,6 +22,7 @@ from documents.tests.utils import DocumentConsumeDelayMixin
from documents.tests.utils import DummyProgressManager
from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import SampleDirMixin
from paperless.models import ApplicationConfiguration
try:
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")
class TestBarcodeNewConsume(

View File

@ -1336,6 +1336,8 @@ class TestWorkflows(
GIVEN:
- Existing workflow with SCHEDULED trigger against the created field and action that assigns owner
- 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:
- Scheduled workflows are checked
THEN:
@ -1359,7 +1361,7 @@ class TestWorkflows(
w.save()
now = timezone.localtime(timezone.now())
created = now - timedelta(weeks=520)
created = now - timedelta(days=2)
doc = Document.objects.create(
title="sample test",
correspondent=self.c,
@ -1377,6 +1379,8 @@ class TestWorkflows(
GIVEN:
- Existing workflow with SCHEDULED trigger against the added field and action that assigns owner
- Existing doc that matches the trigger
- Workflow set to trigger at (now - offset) = now - 1 day
- Document added date is 365 days ago
WHEN:
- Scheduled workflows are checked
THEN:
@ -1418,6 +1422,8 @@ class TestWorkflows(
GIVEN:
- Existing workflow with SCHEDULED trigger against the modified field and action that assigns owner
- 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:
- Scheduled workflows are checked
THEN:
@ -1458,6 +1464,8 @@ class TestWorkflows(
GIVEN:
- Existing workflow with SCHEDULED trigger against a custom field and action that assigns owner
- Existing doc that matches the trigger
- Workflow set to trigger at (now - offset) = now - 1 day
- Custom field date is 2 days ago
WHEN:
- Scheduled workflows are checked
THEN:
@ -1502,6 +1510,7 @@ class TestWorkflows(
GIVEN:
- Existing workflow with SCHEDULED trigger
- Existing doc that has already had the workflow run
- Document created 2 days ago, workflow offset = 1 day trigger time = yesterday
WHEN:
- Scheduled workflows are checked
THEN:
@ -1552,6 +1561,7 @@ class TestWorkflows(
GIVEN:
- Existing workflow with SCHEDULED trigger and recurring interval of 7 days
- Workflow run date is 6 days ago
- Document created 40 days ago, offset = 30 trigger time = 10 days ago
WHEN:
- Scheduled workflows are checked
THEN:
@ -1600,6 +1610,58 @@ class TestWorkflows(
doc.refresh_from_db()
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):
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,

View File

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\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"
"Last-Translator: \n"
"Language-Team: English\n"
@ -1589,15 +1589,59 @@ msgstr ""
msgid "Adds additional user arguments for OCRMyPDF"
msgstr ""
#: paperless/models.py:171
#: paperless/models.py:175
msgid "Application title"
msgstr ""
#: paperless/models.py:178
#: paperless/models.py:182
msgid "Application logo"
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"
msgstr ""

View File

@ -96,10 +96,65 @@ class OcrConfig(OutputTypeConfig):
user_args = json.loads(settings.OCR_USER_ARGS)
except json.JSONDecodeError:
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
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,
)
"""
Settings for the Paperless application
"""
app_title = models.CharField(
verbose_name=_("Application title"),
null=True,
@ -184,6 +188,83 @@ class ApplicationConfiguration(AbstractSingletonModel):
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:
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.totp.internal.auth import TOTP
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 Permission
from django.contrib.auth.models import User
@ -146,8 +147,11 @@ class SocialAccountSerializer(serializers.ModelSerializer):
"name",
)
def get_name(self, obj) -> str:
return obj.get_provider_account().to_str()
def get_name(self, obj: SocialAccount) -> str:
try:
return obj.get_provider_account().to_str()
except SocialApp.DoesNotExist:
return "Unknown App"
class ProfileSerializer(serializers.ModelSerializer):
@ -185,11 +189,14 @@ class ProfileSerializer(serializers.ModelSerializer):
class ApplicationConfigurationSerializer(serializers.ModelSerializer):
user_args = serializers.JSONField(binary=True, allow_null=True)
barcode_tag_mapping = serializers.JSONField(binary=True, allow_null=True)
def run_validation(self, data):
# Empty strings treated as None to avoid unexpected behavior
if "user_args" in data and data["user_args"] == "":
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"] == "":
data["language"] = None
return super().run_validation(data)