mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-07 19:08:32 -05:00
Compare commits
363 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9afc8ba43d | ||
![]() |
b122a05c71 | ||
![]() |
f568a9fdfa | ||
![]() |
a350bb3086 | ||
![]() |
8ae4b7560b | ||
![]() |
96f4924911 | ||
![]() |
f3703fc6e3 | ||
![]() |
0d5fd229bf | ||
![]() |
1519e66550 | ||
![]() |
fcae461430 | ||
![]() |
039d797dfc | ||
![]() |
4f61868b28 | ||
![]() |
9efbc55fd6 | ||
![]() |
2e4e88114e | ||
![]() |
8ee2e8b23d | ||
![]() |
37287c3e7e | ||
![]() |
814d90745b | ||
![]() |
48e8076e34 | ||
![]() |
edb9d934b2 | ||
![]() |
ed44f0cb04 | ||
![]() |
1c2f3cf9af | ||
![]() |
13afd25690 | ||
![]() |
f9b6374685 | ||
![]() |
56c9b578e3 | ||
![]() |
e792a00feb | ||
![]() |
c35c4eade9 | ||
![]() |
5bf725546b | ||
![]() |
6b6862705e | ||
![]() |
b89b51b121 | ||
![]() |
1b31388232 | ||
![]() |
8256e73f04 | ||
![]() |
fc4e59ec00 | ||
![]() |
428e00ba23 | ||
![]() |
2ccbb08c3f | ||
![]() |
eefc026a17 | ||
![]() |
d89022a280 | ||
![]() |
93bd24c9d2 | ||
![]() |
197aec2945 | ||
![]() |
e16d58cff5 | ||
![]() |
92667009be | ||
![]() |
c93657f40e | ||
![]() |
16658f5939 | ||
![]() |
ec0af59596 | ||
![]() |
9e1f9d6d9d | ||
![]() |
e652b927ce | ||
![]() |
5a97f3f94b | ||
![]() |
9494c243a4 | ||
![]() |
c0a1be4325 | ||
![]() |
70fda8e2f8 | ||
![]() |
5765893f69 | ||
![]() |
dc26c9b7cc | ||
![]() |
17b9d7eb42 | ||
![]() |
0a897cae32 | ||
![]() |
37f56e824a | ||
![]() |
a8e7ec0e96 | ||
![]() |
b86d853cf1 | ||
![]() |
da0c94c9a1 | ||
![]() |
0547fd2760 | ||
![]() |
e0a6df8435 | ||
![]() |
c2d4ca7566 | ||
![]() |
56ce278bbd | ||
![]() |
e112541f5b | ||
![]() |
17c7b0840b | ||
![]() |
68658bd407 | ||
![]() |
9184845a9d | ||
![]() |
8ddb2fb305 | ||
![]() |
96f53986b7 | ||
![]() |
8566682209 | ||
![]() |
4f9ae17059 | ||
![]() |
7f9f805c5f | ||
![]() |
5208e81715 | ||
![]() |
01a786c117 | ||
![]() |
8bf4f31954 | ||
![]() |
07ab3e98ed | ||
![]() |
ba5899de7b | ||
![]() |
a4fef056ed | ||
![]() |
c266cd6aae | ||
![]() |
43d05b2e6d | ||
![]() |
6f0c14cc07 | ||
![]() |
2eac8fa91c | ||
![]() |
31f5e178b3 | ||
![]() |
c3d7088168 | ||
![]() |
a9e11b1cb7 | ||
![]() |
ee51a0be13 | ||
![]() |
1091387f48 | ||
![]() |
4db75537cb | ||
![]() |
cb4e738a0f | ||
![]() |
0cc32e7ed7 | ||
![]() |
94f90e1e81 | ||
![]() |
ce8f315747 | ||
![]() |
b761a549c0 | ||
![]() |
dd7c5da256 | ||
![]() |
02dd7ec615 | ||
![]() |
b7b448b870 | ||
![]() |
b7326afe5a | ||
![]() |
a3c4e4c6b6 | ||
![]() |
645297d68c | ||
![]() |
6d8782f771 | ||
![]() |
2728183eee | ||
![]() |
3d0891c73b | ||
![]() |
8226a8793e | ||
![]() |
d01477b337 | ||
![]() |
6bb07c72ab | ||
![]() |
a009462b80 | ||
![]() |
9111b130e4 | ||
![]() |
1dbd7b9bb4 | ||
![]() |
2f95dfc0e7 | ||
![]() |
acc317ddfd | ||
![]() |
1fbd3935ea | ||
![]() |
ca2bf962e9 | ||
![]() |
1d27a3a14b | ||
![]() |
8e1a9dde05 | ||
![]() |
8eabf8c77a | ||
![]() |
ec4ec41552 | ||
![]() |
8960b0300f | ||
![]() |
d6a2672cab | ||
![]() |
4b281ca89d | ||
![]() |
808b507b0f | ||
![]() |
ab47d03e1a | ||
![]() |
aca999090b | ||
![]() |
1322aaf4da | ||
![]() |
c49471fb3b | ||
![]() |
d13baab0a6 | ||
![]() |
359b46c15b | ||
![]() |
3b83e9a43d | ||
![]() |
ab7a499e8f | ||
![]() |
fffe4f694f | ||
![]() |
18c028cafd | ||
![]() |
4e289c7dab | ||
![]() |
3dfe5c9262 | ||
![]() |
be87caf7f6 | ||
![]() |
b7063b199a | ||
![]() |
1ed9c245f5 | ||
![]() |
fb1e9fe66a | ||
![]() |
38a386d5ae | ||
![]() |
726114575e | ||
![]() |
026c213ea4 | ||
![]() |
cd85d4e86a | ||
![]() |
e906bf58f0 | ||
![]() |
87d2209e6d | ||
![]() |
704f8ea680 | ||
![]() |
1e101fb3db | ||
![]() |
a16643ee29 | ||
![]() |
b783e0e211 | ||
![]() |
1ec4570c8d | ||
![]() |
434194d290 | ||
![]() |
f62e64357b | ||
![]() |
b9f49c5eca | ||
![]() |
13a839de57 | ||
![]() |
7eb4253c00 | ||
![]() |
dab210a183 | ||
![]() |
5319d4782a | ||
![]() |
0463c3fe54 | ||
![]() |
fe84430679 | ||
![]() |
9514e79bfb | ||
![]() |
b6756595d9 | ||
![]() |
8ddb3e80b7 | ||
![]() |
52bc1a62e1 | ||
![]() |
cd72ed2cec | ||
![]() |
ccaee4ce62 | ||
![]() |
0e596bd1fc | ||
![]() |
21fafd3255 | ||
![]() |
fda2bfbea7 | ||
![]() |
d26c46e034 | ||
![]() |
27cb243a2f | ||
![]() |
d3514fc5f9 | ||
![]() |
4954851b68 | ||
![]() |
6bcb12ddc9 | ||
![]() |
126a4ec21f | ||
![]() |
0aa084c792 | ||
![]() |
8f6e1360e1 | ||
![]() |
b7e570aba0 | ||
![]() |
ce2bae12af | ||
![]() |
b6111d8da6 | ||
![]() |
2fb1132b69 | ||
![]() |
9a04bc1beb | ||
![]() |
b39c3f7866 | ||
![]() |
740237a8fa | ||
![]() |
391db73ea8 | ||
![]() |
b6ff88645b | ||
![]() |
4cd1672094 | ||
![]() |
630cd814e2 | ||
![]() |
6606d7572c | ||
![]() |
b331e3388f | ||
![]() |
b6428aa85f | ||
![]() |
fa6d554d1f | ||
![]() |
816871eed3 | ||
![]() |
ee04a9226b | ||
![]() |
6b15093c43 | ||
![]() |
b9d0954924 | ||
![]() |
a94a81d839 | ||
![]() |
46be3924e4 | ||
![]() |
316a2469b8 | ||
![]() |
2e21fbf619 | ||
![]() |
e1254a053a | ||
![]() |
50bcd3daf1 | ||
![]() |
e3ba968fef | ||
![]() |
7725973b62 | ||
![]() |
30bf7418f5 | ||
![]() |
eb611b2c41 | ||
![]() |
e4909c3133 | ||
![]() |
2216330118 | ||
![]() |
ca090aa2fa | ||
![]() |
ec7ca2c352 | ||
![]() |
2eb5e289ce | ||
![]() |
40ce38254b | ||
![]() |
0ad2b05455 | ||
![]() |
c74d261f6a | ||
![]() |
68fe18fe36 | ||
![]() |
47313db2c7 | ||
![]() |
127129fbeb | ||
![]() |
f43a8d6f2e | ||
![]() |
7cde850d98 | ||
![]() |
1bcfc5b54d | ||
![]() |
7a8494da4d | ||
![]() |
65733863b1 | ||
![]() |
e62ecefcd7 | ||
![]() |
0aacebf783 | ||
![]() |
43b1700e91 | ||
![]() |
9b4625b4a5 | ||
![]() |
27998df6e4 | ||
![]() |
eb5eabf3c6 | ||
![]() |
76cee78408 | ||
![]() |
0ba35a6eb5 | ||
![]() |
6a58bd30ad | ||
![]() |
e71d055ef2 | ||
![]() |
cb2eff47cc | ||
![]() |
4696f9896b | ||
![]() |
424835880d | ||
![]() |
e0e0ee4a77 | ||
![]() |
5eca0ce554 | ||
![]() |
f5a06ac0dd | ||
![]() |
81ea8873e0 | ||
![]() |
63b4ccacde | ||
![]() |
ccc39e0e55 | ||
![]() |
d264df1504 | ||
![]() |
9b7bc16b3e | ||
![]() |
f7f51f4f73 | ||
![]() |
143ae1092c | ||
![]() |
6470a443ac | ||
![]() |
7795baf989 | ||
![]() |
5ab717c6be | ||
![]() |
fac265486a | ||
![]() |
707efff644 | ||
![]() |
15291fd659 | ||
![]() |
6535a20b21 | ||
![]() |
8c922dbffc | ||
![]() |
874f5fd2ca | ||
![]() |
50864b10dd | ||
![]() |
57a5df1fce | ||
![]() |
14828b15d8 | ||
![]() |
5f03e9e4cc | ||
![]() |
c2b82fa063 | ||
![]() |
40c4cfda75 | ||
![]() |
5c5ce6eedb | ||
![]() |
f9263ddb62 | ||
![]() |
2fd1074729 | ||
![]() |
2e379ad422 | ||
![]() |
c1cecb3e0e | ||
![]() |
b0d13bed72 | ||
![]() |
d23a0d230c | ||
![]() |
91b87f4e2c | ||
![]() |
a77c1f3ad1 | ||
![]() |
4c3cdaab0e | ||
![]() |
3e86c3d0d0 | ||
![]() |
5a40d3c6fe | ||
![]() |
f79c1e0bcc | ||
![]() |
8f14ff7210 | ||
![]() |
90732d6a2f | ||
![]() |
f179ff9da4 | ||
![]() |
90fd999220 | ||
![]() |
04afbdd938 | ||
![]() |
5d3ef3d338 | ||
![]() |
4477d083e8 | ||
![]() |
b7b3f54617 | ||
![]() |
5fc9f3f4da | ||
![]() |
c4b31f2f72 | ||
![]() |
6c6b0511a6 | ||
![]() |
6d20fc14ab | ||
![]() |
895c10600d | ||
![]() |
c024efa578 | ||
![]() |
0022588b02 | ||
![]() |
47ec3d9149 | ||
![]() |
811b82181d | ||
![]() |
32318ddbf6 | ||
![]() |
65a416fdae | ||
![]() |
58d1f98368 | ||
![]() |
4cd7bf7123 | ||
![]() |
d4dea13eee | ||
![]() |
507cdca757 | ||
![]() |
27bfb2d3b0 | ||
![]() |
43118a1ffd | ||
![]() |
729b305860 | ||
![]() |
72950d7872 | ||
![]() |
364ffe8f38 | ||
![]() |
84d822b497 | ||
![]() |
df99035c93 | ||
![]() |
63fc68cf83 | ||
![]() |
9c742fbf08 | ||
![]() |
9965c35d2f | ||
![]() |
0af567bc72 | ||
![]() |
0959a4e915 | ||
![]() |
83be71f622 | ||
![]() |
9c985b36b3 | ||
![]() |
4ef0e20b5c | ||
![]() |
d1cef044e5 | ||
![]() |
f1d4860c79 | ||
![]() |
f1d6dd7d36 | ||
![]() |
46ae7f16dd | ||
![]() |
5da441a001 | ||
![]() |
d495bfab51 | ||
![]() |
4b4f3e76cc | ||
![]() |
ab0e97c1a0 | ||
![]() |
381af329bd | ||
![]() |
4a0d17fc57 | ||
![]() |
e61f042547 | ||
![]() |
77815af5fb | ||
![]() |
e9cdb21164 | ||
![]() |
59bc467c50 | ||
![]() |
254dc62ca6 | ||
![]() |
253bde4e25 | ||
![]() |
ea9e852216 | ||
![]() |
89150e99bf | ||
![]() |
5fd05f3d20 | ||
![]() |
6c1a5f1a98 | ||
![]() |
7815a011af | ||
![]() |
efca0083bf | ||
![]() |
4c983b0e25 | ||
![]() |
d81f64ec06 | ||
![]() |
5735313392 | ||
![]() |
259b2fe429 | ||
![]() |
bb739a55a5 | ||
![]() |
7436b75566 | ||
![]() |
573ac882f5 | ||
![]() |
7f70a886d5 | ||
![]() |
4a1c0c2971 | ||
![]() |
ad37ccd76d | ||
![]() |
d3a4b17d59 | ||
![]() |
4233d73569 | ||
![]() |
6d9a0162b2 | ||
![]() |
72f89de994 | ||
![]() |
bf01799d57 | ||
![]() |
6b63d3855b | ||
![]() |
f0e0e780c5 | ||
![]() |
646803b93e | ||
![]() |
3026b59af4 | ||
![]() |
5adee00ca1 | ||
![]() |
47e887dac5 | ||
![]() |
70395a035c | ||
![]() |
2f63dcb3b5 | ||
![]() |
468ea09965 | ||
![]() |
b6a94d33b8 | ||
![]() |
40c89a8a38 | ||
![]() |
92a61902b5 | ||
![]() |
23ee01b1ce | ||
![]() |
fc727e0472 | ||
![]() |
9427c70b10 | ||
![]() |
cba71ed8e0 | ||
![]() |
728778bdf4 | ||
![]() |
30a92e25c1 | ||
![]() |
4862eb5e84 | ||
![]() |
42fd62ecfd | ||
![]() |
67437f7b9c |
50
Dockerfile
50
Dockerfile
@@ -12,30 +12,38 @@ FROM python:3.7-slim
|
||||
|
||||
# Binary dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get -y --no-install-recommends install \
|
||||
&& apt-get -y --no-install-recommends install \
|
||||
# Basic dependencies
|
||||
curl \
|
||||
file \
|
||||
# fonts for text file thumbnail generation
|
||||
fonts-liberation \
|
||||
# for making translations further down
|
||||
gettext \
|
||||
gnupg \
|
||||
imagemagick \
|
||||
gettext \
|
||||
tzdata \
|
||||
gosu \
|
||||
# fonts for text file thumbnail generation
|
||||
fonts-liberation \
|
||||
# for Numpy
|
||||
libatlas-base-dev \
|
||||
libxslt1-dev \
|
||||
mime-support \
|
||||
# thumbnail size reduction
|
||||
optipng \
|
||||
sudo \
|
||||
tzdata \
|
||||
# OCRmyPDF dependencies
|
||||
ghostscript \
|
||||
icc-profiles-free \
|
||||
liblept5 \
|
||||
libxml2 \
|
||||
pngquant \
|
||||
unpaper \
|
||||
zlib1g \
|
||||
ghostscript \
|
||||
icc-profiles-free \
|
||||
&& echo "deb http://deb.debian.org/debian bullseye main" > /etc/apt/sources.list.d/bullseye.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y --no-install-recommends install \
|
||||
# fixes sudo / gosu issues
|
||||
libseccomp2 \
|
||||
# Mime type detection
|
||||
file \
|
||||
libmagic-dev \
|
||||
media-types \
|
||||
# OCRmyPDF dependencies
|
||||
liblept5 \
|
||||
qpdf \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
@@ -43,16 +51,7 @@ RUN apt-get update \
|
||||
tesseract-ocr-fra \
|
||||
tesseract-ocr-ita \
|
||||
tesseract-ocr-spa \
|
||||
unpaper \
|
||||
zlib1g \
|
||||
|
||||
# This pulls in updated dependencies from bullseye to fix some issues with file type detection.
|
||||
# TODO: Remove this once bullseye releases.
|
||||
&& echo "deb http://deb.debian.org/debian bullseye main" > /etc/apt/sources.list.d/bullseye.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install --no-install-recommends -y file libmagic-dev \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm /etc/apt/sources.list.d/bullseye.list
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# copy jbig2enc
|
||||
COPY --from=jbig2enc /usr/src/jbig2enc/src/.libs/libjbig2enc* /usr/local/lib/
|
||||
@@ -83,6 +82,7 @@ RUN cd docker \
|
||||
&& mkdir /var/log/supervisord /var/run/supervisord \
|
||||
&& cp supervisord.conf /etc/supervisord.conf \
|
||||
&& cp docker-entrypoint.sh /sbin/docker-entrypoint.sh \
|
||||
&& cp docker-prepare.sh /sbin/docker-prepare.sh \
|
||||
&& chmod 755 /sbin/docker-entrypoint.sh \
|
||||
&& chmod +x install_management_commands.sh \
|
||||
&& ./install_management_commands.sh \
|
||||
@@ -98,8 +98,8 @@ COPY src/ ./
|
||||
RUN addgroup --gid 1000 paperless \
|
||||
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
|
||||
&& chown -R paperless:paperless ../ \
|
||||
&& sudo -HEu paperless python3 manage.py collectstatic --clear --no-input \
|
||||
&& sudo -HEu paperless python3 manage.py compilemessages
|
||||
&& gosu paperless python3 manage.py collectstatic --clear --no-input \
|
||||
&& gosu paperless python3 manage.py compilemessages
|
||||
|
||||
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"]
|
||||
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
||||
|
10
Pipfile
10
Pipfile
@@ -10,7 +10,7 @@ name = "piwheels"
|
||||
|
||||
[packages]
|
||||
dateparser = "~=1.0.0"
|
||||
django = "~=3.1.3"
|
||||
django = "~=3.2"
|
||||
django-cors-headers = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "~=2.4.0"
|
||||
@@ -24,9 +24,8 @@ langdetect = "*"
|
||||
# numpy 1.20.0 drops python 3.6 support
|
||||
numpy = "~=1.19.5"
|
||||
pathvalidate = "*"
|
||||
# pinned to 8.1.0, since aarch64 wheels might not be available beyond that https://github.com/python-pillow/Pillow/issues/5202
|
||||
pillow = "==8.1.0"
|
||||
pikepdf = "~=2.5.0"
|
||||
pillow = "~=8.1"
|
||||
pikepdf = "~=2.5"
|
||||
python-gnupg = "*"
|
||||
python-dotenv = "*"
|
||||
python-dateutil = "*"
|
||||
@@ -41,7 +40,7 @@ whitenoise = "~=5.2.0"
|
||||
watchdog = "~=1.0.0"
|
||||
whoosh="~=2.7.4"
|
||||
inotifyrecursive = "~=0.3.4"
|
||||
ocrmypdf = "~=11.6"
|
||||
ocrmypdf = "~=12.0"
|
||||
tqdm = "*"
|
||||
tika = "*"
|
||||
# TODO: This will sadly also install daphne+dependencies,
|
||||
@@ -52,7 +51,6 @@ uvicorn = {extras = ["standard"], version = "*"}
|
||||
concurrent-log-handler = "*"
|
||||
# uvloop 0.15+ incompatible with python 3.6
|
||||
uvloop = "~=0.14.0"
|
||||
# TODO: keep an eye on piwheel builds and update this once available (https://www.piwheels.org/project/cryptography/)
|
||||
cryptography = "~=3.4"
|
||||
"pdfminer.six" = "*"
|
||||
|
||||
|
1080
Pipfile.lock
generated
1080
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
25
README.md
25
README.md
@@ -1,5 +1,6 @@
|
||||
[](https://github.com/jonaswinkler/paperless-ng/actions)
|
||||

|
||||
[](https://crowdin.com/project/paperless-ng)
|
||||
[](https://paperless-ng.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://gitter.im/paperless-ng/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://hub.docker.com/r/jonaswinkler/paperless-ng)
|
||||
@@ -11,12 +12,12 @@
|
||||
|
||||
Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. These key points should help you decide whether Paperless-ng is something you would prefer over Paperless:
|
||||
|
||||
* Interface: The new front end is the main interface for paperless-ng, the old interface still exists but most customizations (such as thumbnails for the document list) have been removed.
|
||||
* Interface: The new front end is the main interface for Paperless-ng, the old interface still exists but most customizations (such as thumbnails for the document list) have been removed.0
|
||||
* Encryption: Paperless-ng does not support GnuPG anymore, since storing your data on encrypted file systems (that you optionally mount on demand) achieves about the same result.
|
||||
* Resource usage: Paperless-ng does use a bit more resources than Paperless. Running the web server requires about 300MB of RAM or more, depending on the configuration. While adding documents, it requires about 300MB additional RAM, depending on the document. It still runs on Pi (many users do that), but it has been generally geared to better use the resources of more powerful systems.
|
||||
* Resource usage: Paperless-ng does use a bit more resources than Paperless. Running the web server requires about 300MB of RAM or more, depending on the configuration. While adding documents, it requires about 300MB additional RAM, depending on the document. It still runs on Raspberry Pi (many users do that), but it has been generally geared to better use the resources of more powerful systems.
|
||||
* API changes: If you rely on the REST API of paperless, some of its functionality has been changed.
|
||||
|
||||
For a detailed list of changes, have a look at the [change log](https://paperless-ng.readthedocs.io/en/latest/changelog.html) in the documentation.
|
||||
For a detailed list of changes, have a look at the [change log](https://paperless-ng.readthedocs.io/en/latest/changelog.html) in the documentation, especially the section about the [0.9.0 release](https://paperless-ng.readthedocs.io/en/latest/changelog.html#paperless-ng-0-9-0).
|
||||
|
||||
# How it Works
|
||||
|
||||
@@ -24,7 +25,7 @@ Paperless does not control your scanner, it only helps you deal with what your s
|
||||
|
||||
1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory.
|
||||
|
||||
- Alternatively, you can use any of the mobile scanning apps out there. We have an app that allows you to share documents with paperless, if you're on Android. See the section on affiliated projects.
|
||||
- Alternatively, you can use any of the mobile scanning apps out there. We have an app that allows you to share documents with paperless, if you're on Android. See the section on affiliated projects below.
|
||||
|
||||
2. Wait for paperless to process your files. OCR is expensive, and depending on the power of your machine, this might take a bit of time.
|
||||
3. Use the web frontend to sift through the database and find what you want.
|
||||
@@ -34,6 +35,8 @@ Here's what you get:
|
||||
|
||||

|
||||
|
||||
If you want to see paperless-ng in action, [more screenshots are available in the documentation](https://paperless-ng.readthedocs.io/en/latest/screenshots.html).
|
||||
|
||||
# Features
|
||||
|
||||
* Performs OCR on your documents, adds selectable text to image only documents and adds tags, correspondents and document types to your documents.
|
||||
@@ -57,19 +60,17 @@ Here's what you get:
|
||||
* Optimized for multi core systems: Paperless-ng consumes multiple documents in parallel.
|
||||
* The integrated sanity checker makes sure that your document archive is in good health.
|
||||
|
||||
If you want to see some screenshots of paperless-ng in action, [some are available in the documentation](https://paperless-ng.readthedocs.io/en/latest/screenshots.html).
|
||||
|
||||
# Getting started
|
||||
|
||||
The recommended way to deploy paperless is docker-compose. The files in the /docker/hub directory are configured to pull the image from Docker Hub.
|
||||
|
||||
Read the [documentation](https://paperless-ng.readthedocs.io/en/latest/setup.html#installation) on how to get started.
|
||||
|
||||
Alternatively, you can install the dependencies and setup apache and a database server yourself. The documenation has a step by step guide on how to do it.
|
||||
Alternatively, you can install the dependencies and setup apache and a database server yourself. The documenation has a step by step guide on how to do it. Consider giving the Ansible role a shot, this essentially automates the entire bare metal installation process.
|
||||
|
||||
# Migrating to paperless-ng
|
||||
# Migrating from Paperless to Paperless-ng
|
||||
|
||||
Read the section about [migration](https://paperless-ng.readthedocs.io/en/latest/setup.html#migration-to-paperless-ng) in the documentation. Its also entirely possible to go back to paperless by reverting the database migrations.
|
||||
Read the section about [migration](https://paperless-ng.readthedocs.io/en/latest/setup.html#migration-to-paperless-ng) in the documentation. Its also entirely possible to go back to Paperless by reverting the database migrations.
|
||||
|
||||
# Documentation
|
||||
|
||||
@@ -77,9 +78,7 @@ The documentation for Paperless-ng is available on [ReadTheDocs](https://paperle
|
||||
|
||||
# Translation
|
||||
|
||||
Paperless is currently available in English, German, Dutch, French, and Portuguese.
|
||||
|
||||
There's an active translation project at transifex! If you want to help out by translating paperless into your language, please head over to https://github.com/jonaswinkler/paperless-ng/issues/212 for details.
|
||||
Paperless is available in many different languages. Translation is coordinated at crowdin. If you want to help out by translating paperless into your language, please head over to https://github.com/jonaswinkler/paperless-ng/issues/212 for details!
|
||||
|
||||
# Feature Requests
|
||||
|
||||
@@ -93,7 +92,7 @@ For bugs please [open an issue](https://github.com/jonaswinkler/paperless-ng/iss
|
||||
|
||||
There's still lots of things to be done, just have a look at open issues & discussions. If you feel like contributing to the project, please do! Bug fixes and improvements to the front end (I just can't seem to get some of these CSS things right) are always welcome. The documentation has some basic information on how to get started.
|
||||
|
||||
If you want to implement something big: Please start a discussion about that in the issues! Maybe I've already had something similar in mind and we can make it happen together. However, keep in mind that the general roadmap is to make the existing features stable and get them tested. See the roadmap above.
|
||||
If you want to implement something big: Please start a discussion about that! Maybe I've already had something similar in mind and we can make it happen together. However, keep in mind that the general roadmap is to make the existing features stable and get them tested.
|
||||
|
||||
# Affiliated Projects
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
commit_message: '[ci skip]'
|
||||
files:
|
||||
- source: /src/locale/en_US/LC_MESSAGES/django.po
|
||||
translation: /src/locale/%locale_with_underscore%/LC_MESSAGES/django.po
|
||||
|
94
docker/compose/docker-compose.portainer.yml
Normal file
94
docker/compose/docker-compose.portainer.yml
Normal file
@@ -0,0 +1,94 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
# All compose files of paperless configure paperless in the following way:
|
||||
#
|
||||
# - Paperless is (re)started on system boot, if it was running before shutdown.
|
||||
# - Docker volumes for storing data are managed by Docker.
|
||||
# - Folders for importing and exporting files are created in the same directory
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8010.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), PostgreSQL is used as the database server.
|
||||
#
|
||||
# To install and update paperless with this file, do the following:
|
||||
#
|
||||
# - Open portainer Stacks list and click 'Add stack'
|
||||
# - Paste the contents of this file and assign a name, e.g. 'Paperless'
|
||||
# - Click 'Deploy the stack' and wait for it to be deployed
|
||||
# - Open the list of containers, select paperless_webserver_1
|
||||
# - Click 'Console' and then 'Connect' to open the command line inside the container
|
||||
# - Run 'python3 manage.py createsuperuser' to create a user
|
||||
# - Exit the console
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: redis:6.0
|
||||
restart: unless-stopped
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: paperless
|
||||
POSTGRES_USER: paperless
|
||||
POSTGRES_PASSWORD: paperless
|
||||
|
||||
webserver:
|
||||
image: jonaswinkler/paperless-ng:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db
|
||||
- broker
|
||||
ports:
|
||||
- 8010:8000
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
- ./export:/usr/src/paperless/export
|
||||
- ./consume:/usr/src/paperless/consume
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_DBHOST: db
|
||||
# The UID and GID of the user used to run paperless in the container. Set this
|
||||
# to your UID and GID on the host so that you have write access to the
|
||||
# consumption directory.
|
||||
USERMAP_UID: 1000
|
||||
USERMAP_GID: 100
|
||||
# Additional languages to install for text recognition, separated by a
|
||||
# whitespace. Note that this is
|
||||
# different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the
|
||||
# language used for OCR.
|
||||
# The container installs English, German, Italian, Spanish and French by
|
||||
# default.
|
||||
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster
|
||||
# for available languages.
|
||||
#PAPERLESS_OCR_LANGUAGES: tur ces
|
||||
# Adjust this key if you plan to make paperless available publicly. It should
|
||||
# be a very long sequence of random characters. You don't need to remember it.
|
||||
#PAPERLESS_SECRET_KEY: change-me
|
||||
# Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC.
|
||||
#PAPERLESS_TIME_ZONE: America/Los_Angeles
|
||||
# The default language to use for OCR. Set this to the language most of your
|
||||
# documents are written in.
|
||||
#PAPERLESS_OCR_LANGUAGE: eng
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
pgdata:
|
61
docker/docker-entrypoint.sh
Normal file → Executable file
61
docker/docker-entrypoint.sh
Normal file → Executable file
@@ -15,59 +15,6 @@ map_uidgid() {
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
wait_for_postgres() {
|
||||
attempt_num=1
|
||||
max_attempts=5
|
||||
|
||||
echo "Waiting for PostgreSQL to start..."
|
||||
|
||||
host="${PAPERLESS_DBHOST}"
|
||||
port="${PAPERLESS_DBPORT}"
|
||||
|
||||
if [[ -z $port ]] ;
|
||||
then
|
||||
port="5432"
|
||||
fi
|
||||
|
||||
while !</dev/tcp/$host/$port ;
|
||||
do
|
||||
|
||||
if [ $attempt_num -eq $max_attempts ]
|
||||
then
|
||||
echo "Unable to connect to database."
|
||||
exit 1
|
||||
else
|
||||
echo "Attempt $attempt_num failed! Trying again in 5 seconds..."
|
||||
|
||||
fi
|
||||
|
||||
attempt_num=$(expr "$attempt_num" + 1)
|
||||
sleep 5
|
||||
done
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
migrations() {
|
||||
|
||||
if [[ -n "${PAPERLESS_DBHOST}" ]]
|
||||
then
|
||||
wait_for_postgres
|
||||
fi
|
||||
|
||||
(
|
||||
# flock is in place to prevent multiple containers from doing migrations
|
||||
# simultaneously. This also ensures that the db is ready when the command
|
||||
# of the current container starts.
|
||||
flock 200
|
||||
echo "Apply database migrations..."
|
||||
sudo -HEu paperless python3 manage.py migrate
|
||||
) 200>/usr/src/paperless/data/migration_lock
|
||||
|
||||
}
|
||||
|
||||
initialize() {
|
||||
map_uidgid
|
||||
|
||||
@@ -82,11 +29,12 @@ initialize() {
|
||||
echo "creating directory /tmp/paperless"
|
||||
mkdir -p /tmp/paperless
|
||||
|
||||
set +e
|
||||
chown -R paperless:paperless ../
|
||||
chown -R paperless:paperless /tmp/paperless
|
||||
set -e
|
||||
|
||||
migrations
|
||||
|
||||
gosu paperless /sbin/docker-prepare.sh
|
||||
}
|
||||
|
||||
install_languages() {
|
||||
@@ -137,9 +85,8 @@ initialize
|
||||
|
||||
if [[ "$1" != "/"* ]]; then
|
||||
echo Executing management command "$@"
|
||||
exec sudo -HEu paperless python3 manage.py "$@"
|
||||
exec gosu paperless python3 manage.py "$@"
|
||||
else
|
||||
echo Executing "$@"
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
|
72
docker/docker-prepare.sh
Executable file
72
docker/docker-prepare.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
wait_for_postgres() {
|
||||
attempt_num=1
|
||||
max_attempts=5
|
||||
|
||||
echo "Waiting for PostgreSQL to start..."
|
||||
|
||||
host="${PAPERLESS_DBHOST}"
|
||||
port="${PAPERLESS_DBPORT}"
|
||||
|
||||
if [[ -z $port ]]; then
|
||||
port="5432"
|
||||
fi
|
||||
|
||||
while ! </dev/tcp/$host/$port; do
|
||||
|
||||
if [ $attempt_num -eq $max_attempts ]; then
|
||||
echo "Unable to connect to database."
|
||||
exit 1
|
||||
else
|
||||
echo "Attempt $attempt_num failed! Trying again in 5 seconds..."
|
||||
|
||||
fi
|
||||
|
||||
attempt_num=$(expr "$attempt_num" + 1)
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
migrations() {
|
||||
(
|
||||
# flock is in place to prevent multiple containers from doing migrations
|
||||
# simultaneously. This also ensures that the db is ready when the command
|
||||
# of the current container starts.
|
||||
flock 200
|
||||
echo "Apply database migrations..."
|
||||
python3 manage.py migrate
|
||||
) 200>/usr/src/paperless/data/migration_lock
|
||||
}
|
||||
|
||||
search_index() {
|
||||
index_version=1
|
||||
index_version_file=/usr/src/paperless/data/.index_version
|
||||
|
||||
if [[ (! -f "$index_version_file") || $(<$index_version_file) != "$index_version" ]]; then
|
||||
echo "Search index out of date. Updating..."
|
||||
python3 manage.py document_index reindex
|
||||
echo $index_version | tee $index_version_file >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
superuser() {
|
||||
if [[ -n "${PAPERLESS_ADMIN_USER}" ]]; then
|
||||
python3 manage.py manage_superuser
|
||||
fi
|
||||
}
|
||||
|
||||
do_work() {
|
||||
if [[ -n "${PAPERLESS_DBHOST}" ]]; then
|
||||
wait_for_postgres
|
||||
fi
|
||||
|
||||
migrations
|
||||
|
||||
search_index
|
||||
|
||||
superuser
|
||||
|
||||
}
|
||||
|
||||
do_work
|
@@ -1,4 +1,4 @@
|
||||
for command in document_archiver document_exporter document_importer mail_fetcher document_create_classifier document_index document_renamer document_retagger document_thumbnails document_sanity_checker;
|
||||
for command in document_archiver document_exporter document_importer mail_fetcher document_create_classifier document_index document_renamer document_retagger document_thumbnails document_sanity_checker manage_superuser;
|
||||
do
|
||||
echo "installing $command..."
|
||||
sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command
|
||||
|
2
docker/management_script.sh
Normal file → Executable file
2
docker/management_script.sh
Normal file → Executable file
@@ -6,7 +6,7 @@ cd /usr/src/paperless/src/
|
||||
|
||||
if [[ $(id -u) == 0 ]] ;
|
||||
then
|
||||
sudo -HEu paperless python3 manage.py management_command "$@"
|
||||
gosu paperless python3 manage.py management_command "$@"
|
||||
elif [[ $(id -un) == "paperless" ]] ;
|
||||
then
|
||||
python3 manage.py management_command "$@"
|
||||
|
@@ -193,7 +193,9 @@ This table lists the compatible versions for each database migration number.
|
||||
+------------------+-----------------+
|
||||
| 1012 | 1.1.0 - 1.2.1 |
|
||||
+------------------+-----------------+
|
||||
| 1014 | 1.3.0 - current |
|
||||
| 1014 | 1.3.0 - 1.3.1 |
|
||||
+------------------+-----------------+
|
||||
| 1016 | 1.3.2 - current |
|
||||
+------------------+-----------------+
|
||||
|
||||
Execute the following management command to migrate your database:
|
||||
|
@@ -10,14 +10,13 @@ easier.
|
||||
Matching tags, correspondents and document types
|
||||
################################################
|
||||
|
||||
After the consumer has tried to figure out what it could from the file name,
|
||||
it starts looking at the content of the document itself. It will compare the
|
||||
matching algorithms defined by every tag and correspondent already set in your
|
||||
database to see if they apply to the text in that document. In other words,
|
||||
if you defined a tag called ``Home Utility`` that had a ``match`` property of
|
||||
``bc hydro`` and a ``matching_algorithm`` of ``literal``, Paperless will
|
||||
automatically tag your newly-consumed document with your ``Home Utility`` tag
|
||||
so long as the text ``bc hydro`` appears in the body of the document somewhere.
|
||||
Paperless will compare the matching algorithms defined by every tag and
|
||||
correspondent already set in your database to see if they apply to the text in
|
||||
a document. In other words, if you defined a tag called ``Home Utility``
|
||||
that had a ``match`` property of ``bc hydro`` and a ``matching_algorithm`` of
|
||||
``literal``, Paperless will automatically tag your newly-consumed document with
|
||||
your ``Home Utility`` tag so long as the text ``bc hydro`` appears in the body
|
||||
of the document somewhere.
|
||||
|
||||
The matching logic is quite powerful, and supports searching the text of your
|
||||
document with different algorithms, and as such, some experimentation may be
|
||||
|
112
docs/api.rst
112
docs/api.rst
@@ -147,93 +147,57 @@ The REST api provides three different forms of authentication.
|
||||
Searching for documents
|
||||
#######################
|
||||
|
||||
Paperless-ng offers API endpoints for full text search. These are as follows:
|
||||
Full text searching is available on the ``/api/documents/`` endpoint. Two specific
|
||||
query parameters cause the API to return full text search results:
|
||||
|
||||
``/api/search/``
|
||||
================
|
||||
* ``/api/documents/?query=your%20search%20query``: Search for a document using a full text query.
|
||||
For details on the syntax, see :ref:`basic-usage_searching`.
|
||||
|
||||
Get search results based on a query.
|
||||
* ``/api/documents/?more_like=1234``: Search for documents similar to the document with id 1234.
|
||||
|
||||
Query parameters:
|
||||
Pagination works exactly the same as it does for normal requests on this endpoint.
|
||||
|
||||
* ``query``: The query string. See
|
||||
`here <https://whoosh.readthedocs.io/en/latest/querylang.html>`_
|
||||
for details on the syntax.
|
||||
* ``page``: Specify the page you want to retrieve. Each page
|
||||
contains 10 search results and the first page is ``page=1``, which
|
||||
is the default if this is omitted.
|
||||
Certain limitations apply to full text queries:
|
||||
|
||||
Result list object returned by the endpoint:
|
||||
* Results are always sorted by search score. The results matching the query best will show up first.
|
||||
|
||||
.. code:: json
|
||||
* Only a small subset of filtering parameters are supported.
|
||||
|
||||
Furthermore, each returned document has an additional ``__search_hit__`` attribute with various information
|
||||
about the search results:
|
||||
|
||||
.. code::
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"page": 1,
|
||||
"page_count": 1,
|
||||
"corrected_query": "",
|
||||
"count": 31,
|
||||
"next": "http://localhost:8000/api/documents/?page=2&query=test",
|
||||
"previous": null,
|
||||
"results": [
|
||||
|
||||
...
|
||||
|
||||
{
|
||||
"id": 123,
|
||||
"title": "title",
|
||||
"content": "content",
|
||||
|
||||
...
|
||||
|
||||
"__search_hit__": {
|
||||
"score": 0.343,
|
||||
"highlights": "text <span class=\"match\">Test</span> text",
|
||||
"rank": 23
|
||||
}
|
||||
},
|
||||
|
||||
...
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
* ``count``: The approximate total number of results.
|
||||
* ``page``: The page returned to you. This might be different from
|
||||
the page you requested, if you requested a page that is behind
|
||||
the last page. In that case, the last page is returned.
|
||||
* ``page_count``: The total number of pages.
|
||||
* ``corrected_query``: Corrected version of the query string. Can be null.
|
||||
If not null, can be used verbatim to start a new query.
|
||||
* ``results``: A list of result objects on the current page.
|
||||
|
||||
Result object:
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"highlights": [
|
||||
|
||||
],
|
||||
"score": 6.34234,
|
||||
"rank": 23,
|
||||
"document": {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
* ``id``: the primary key of the found document
|
||||
* ``highlights``: an object containing parsable highlights for the result.
|
||||
See below.
|
||||
* ``score``: The score assigned to the document. A higher score indicates a
|
||||
better match with the query. Search results are sorted descending by score.
|
||||
* ``rank``: the position of the document within the entire search results list.
|
||||
* ``document``: The full json of the document, as returned by
|
||||
``/api/documents/<id>/``.
|
||||
|
||||
Highlights object:
|
||||
|
||||
Highlights are provided as a list of fragments. A fragment is a longer section of
|
||||
text from the original document.
|
||||
Each fragment contains a list of strings, and some of them are marked as a highlight.
|
||||
|
||||
.. code:: json
|
||||
|
||||
[
|
||||
[
|
||||
{"text": "This is a sample text with a ", "highlight": false},
|
||||
{"text": "highlighted", "highlight": true},
|
||||
{"text": " word.", "highlight": false}
|
||||
],
|
||||
[
|
||||
{"text": "Another", "highlight": true},
|
||||
{"text": " fragment with a highlight.", "highlight": false}
|
||||
]
|
||||
]
|
||||
|
||||
A client may use this example to produce the following output:
|
||||
|
||||
... This is a sample text with a **highlighted** word. ... **Another** fragment with a highlight. ...
|
||||
* ``score`` is an indication how well this document matches the query relative to the other search results.
|
||||
* ``highlights`` is an excerpt from the document content and highlights the search terms with ``<span>`` tags as shown above.
|
||||
* ``rank`` is the index of the search results. The first result will have rank 0.
|
||||
|
||||
``/api/search/autocomplete/``
|
||||
=============================
|
||||
|
@@ -5,6 +5,128 @@
|
||||
Changelog
|
||||
*********
|
||||
|
||||
paperless-ng 1.4.3
|
||||
##################
|
||||
|
||||
* Additions and changes
|
||||
|
||||
* Added Swedish locale.
|
||||
* `Stéphane Brunner`_ added an option to disable the progress bars of all management commands.
|
||||
* `Jo Vandeginste`_ added support for RTF documents to the Apache TIKA parser.
|
||||
* `Michael Shamoon`_ added dark mode for the login and logout pages.
|
||||
* `Alexander Menk`_ added additional stylesheets for printing. You can now print any page of paperless and the print result will hide the page header, sidebar, and action buttons.
|
||||
* Added support for sorting when using full text search.
|
||||
|
||||
* Fixes
|
||||
|
||||
* `puuu`_ fixed ``PAPERLESS_FORCE_SCRIPT_NAME``. You can now host paperless on sub paths such as ``https://localhost:8000/paperless/``.
|
||||
* Fixed an issue with the document consumer crashing on certain documents due to issues with pdfminer.six. This library is used for PDF text extraction.
|
||||
|
||||
paperless-ng 1.4.2
|
||||
##################
|
||||
|
||||
* Fixed an issue with ``sudo`` that caused paperless to not start on many Raspberry Pi devices. Thank you `WhiteHatTux`_!
|
||||
|
||||
paperless-ng 1.4.1
|
||||
##################
|
||||
|
||||
* Added Polish locale.
|
||||
|
||||
* Changed some parts of the Dockerfile to hopefully restore functionality on certain ARM devices.
|
||||
|
||||
* Updated python dependencies.
|
||||
|
||||
* `Michael Shamoon`_ added a sticky filter / bulk edit bar.
|
||||
|
||||
* `sbrl`_ changed the docker-entrypoint.sh script to increase compatibility with NFS shares.
|
||||
|
||||
* `Chris Nagy`_ added support for creating a super user by passing ``PAPERLESS_ADMIN_USER`` and
|
||||
``PAPERLESS_ADMIN_PASSWORD`` as environment variables to the docker container.
|
||||
|
||||
paperless-ng 1.4.0
|
||||
##################
|
||||
|
||||
* Docker images now use tesseract 4.1.1, which should fix a series of issues with OCR.
|
||||
|
||||
* The full text search now displays results using the default document list. This enables
|
||||
selection, filtering and bulk edit on search results.
|
||||
|
||||
* Changes
|
||||
|
||||
* Firefox only: Highlight search query in PDF previews.
|
||||
|
||||
* New URL pattern for accessing documents by ASN directly (http://<paperless>/asn/123)
|
||||
|
||||
* Added logging when executing pre- and post-consume scripts.
|
||||
|
||||
* Better error logging during document consumption.
|
||||
|
||||
* Updated python dependencies.
|
||||
|
||||
* Automatically inserts typed text when opening "Create new" dialogs on the document details page.
|
||||
|
||||
* Fixes
|
||||
|
||||
* Fixed an issue with null characters in the document content.
|
||||
|
||||
.. note::
|
||||
|
||||
The changed to the full text searching require you to reindex your documents.
|
||||
*The docker image does this automatically, you don't need to do anything.*
|
||||
To do this, execute the ``document_index reindex`` management command
|
||||
(see :ref:`administration-index`).
|
||||
|
||||
.. note::
|
||||
|
||||
Some packages that paperless depends on are slowly dropping Python 3.6
|
||||
support one after another, including the web server. Supporting Python
|
||||
3.6 means that I cannot update these packages anymore.
|
||||
|
||||
At some point, paperless will drop Python 3.6 support. If using a bare
|
||||
metal installation and you're still on Python 3.6, upgrade to 3.7 or newer.
|
||||
|
||||
If using docker, this does not affect you.
|
||||
|
||||
paperless-ng 1.3.2
|
||||
##################
|
||||
|
||||
* Added translation into Portuguese.
|
||||
|
||||
* Changes
|
||||
|
||||
* The exporter now exports user accounts, mail accounts, mail rules and saved views as well.
|
||||
|
||||
* Fixes
|
||||
|
||||
* Minor layout issues with document cards and the log viewer.
|
||||
|
||||
* Fixed an issue with any/all/exact matching when characters used in regular expressions were used for the match.
|
||||
|
||||
paperless-ng 1.3.1
|
||||
##################
|
||||
|
||||
* Added translation into Spanish and Russian.
|
||||
|
||||
* Other changes
|
||||
|
||||
* ISO-8601 date format will now always show years with 4 digits.
|
||||
|
||||
* Added the ability to search for a document with a specific ASN.
|
||||
|
||||
* The document cards now display ASN, types and dates in a more organized way.
|
||||
|
||||
* Added document previews when hovering over the preview button.
|
||||
|
||||
* Fixes
|
||||
|
||||
* The startup check for write permissions now works properly on NFS shares.
|
||||
|
||||
* Fixed an issue with the search results score indicator.
|
||||
|
||||
* Paperless was unable to generate thumbnails for encrypted PDF files and failed. Paperless will now generate a default thumbnail for these files.
|
||||
|
||||
* Fixed ``AUTO_LOGIN_USERNAME``: Unable to perform POST/PUT/DELETE requests and unable to receive WebSocket messages.
|
||||
|
||||
paperless-ng 1.3.0
|
||||
##################
|
||||
|
||||
@@ -68,17 +190,6 @@ paperless-ng 1.2.0
|
||||
|
||||
* Paperless no longer depends on ``libpoppler-cpp-dev``.
|
||||
|
||||
.. note::
|
||||
|
||||
Some packages that paperless depends on are slowly dropping Python 3.6
|
||||
support one after another, including the web server. Supporting Python
|
||||
3.6 means that I cannot update these packages anymore.
|
||||
|
||||
At some point, paperless will drop Python 3.6 support. If using a bare
|
||||
metal installation and you're still on Python 3.6, upgrade to 3.7 or newer.
|
||||
|
||||
If using docker, this does not affect you.
|
||||
|
||||
paperless-ng 1.1.4
|
||||
##################
|
||||
|
||||
@@ -1272,6 +1383,11 @@ bulk of the work on this big change.
|
||||
|
||||
* Initial release
|
||||
|
||||
.. _Alexander Menk: https://github.com/amenk
|
||||
.. _puuu: https://github.com/puuu
|
||||
.. _WhiteHatTux: https://github.com/WhiteHatTux
|
||||
.. _Chris Nagy: https://github.com/what-name
|
||||
.. _sbrl: https://github.com/sbrl
|
||||
.. _slorenz: https://github.com/sisao
|
||||
.. _Jo Vandeginste: https://github.com/jovandeginste
|
||||
.. _zjean: https://github.com/zjean
|
||||
|
@@ -154,10 +154,6 @@ PAPERLESS_FORCE_SCRIPT_NAME=<path>
|
||||
To host paperless under a subpath url like example.com/paperless you set
|
||||
this value to /paperless. No trailing slash!
|
||||
|
||||
.. note::
|
||||
|
||||
I don't know if this works in paperless-ng. Probably not.
|
||||
|
||||
Defaults to none, which hosts paperless at "/".
|
||||
|
||||
PAPERLESS_STATIC_URL=<path>
|
||||
@@ -177,6 +173,30 @@ PAPERLESS_AUTO_LOGIN_USERNAME=<username>
|
||||
|
||||
Defaults to none, which disables this feature.
|
||||
|
||||
PAPERLESS_ADMIN_USER=<username>
|
||||
If this environment variable is specified, Paperless automatically creates
|
||||
a superuser with the provided username at start. This is useful in cases
|
||||
where you can not run the `createsuperuser` command seperately, such as Kubernetes
|
||||
or AWS ECS.
|
||||
|
||||
Requires `PAPERLESS_ADMIN_PASSWORD` to be set.
|
||||
|
||||
.. note::
|
||||
|
||||
This will not change an existing [super]user's password, nor will
|
||||
it recreate a user that already exists. You can leave this throughout
|
||||
the lifecycle of the containers.
|
||||
|
||||
PAPERLESS_ADMIN_MAIL=<email>
|
||||
(Optional) Specify superuser email address. Only used when
|
||||
`PAPERLESS_ADMIN_USER` is set.
|
||||
|
||||
Defaults to ``root@localhost``.
|
||||
|
||||
PAPERLESS_ADMIN_PASSWORD=<password>
|
||||
Only used when `PAPERLESS_ADMIN_USER` is set.
|
||||
This will be the password of the automatically created superuser.
|
||||
|
||||
|
||||
PAPERLESS_COOKIE_PREFIX=<str>
|
||||
Specify a prefix that is added to the cookies used by paperless to identify
|
||||
|
@@ -13,26 +13,35 @@ that works right for you based on recommendations from other Paperless users.
|
||||
Physical scanners
|
||||
=================
|
||||
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| Brand | Model | Supports | Recommended By |
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| | | FTP | NFS | SMB | |
|
||||
+=========+================+=====+=====+=====+================+
|
||||
| Brother | `ADS-1500W`_ | yes | no | yes | `danielquinn`_ |
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| Brother | `MFC-J6930DW`_ | yes | | | `ayounggun`_ |
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| Brother | `MFC-J5910DW`_ | yes | | | `bmsleight`_ |
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| Brother | `MFC-9142CDN`_ | yes | | yes | `REOLDEV`_ |
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| Fujitsu | `ix500`_ | yes | | yes | `eonist`_ |
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| Epson | `WF-7710DWF`_ | yes | | yes | `Skylinar`_ |
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| Fujitsu | `S1300i`_ | yes | | yes | `jonaswinkler`_|
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brand | Model | Supports | Recommended By |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| | | FTP | NFS | SMB | SMTP | |
|
||||
+=========+================+=====+=====+=====+======+================+
|
||||
| Brother | `ADS-1700W`_ | yes | no | yes | yes |`holzhannes`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `ADS-1600W`_ | yes | no | yes | yes |`holzhannes`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `ADS-1500W`_ | yes | no | yes | yes |`danielquinn`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `MFC-J6930DW`_ | yes | | | |`ayounggun`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `MFC-L5850DW`_ | yes | | | yes |`holzhannes`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `MFC-J5910DW`_ | yes | | | |`bmsleight`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `MFC-9142CDN`_ | yes | | yes | |`REOLDEV`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Fujitsu | `ix500`_ | yes | | yes | |`eonist`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Epson | `WF-7710DWF`_ | yes | | yes | |`Skylinar`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Fujitsu | `S1300i`_ | yes | | yes | |`jonaswinkler`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
|
||||
.. _MFC-L5850DW: https://www.brother-usa.com/products/mfcl5850dw
|
||||
.. _ADS-1700W: https://www.brother-usa.com/products/ads1700w
|
||||
.. _ADS-1600W: https://www.brother-usa.com/products/ads1600w
|
||||
.. _ADS-1500W: https://www.brother.ca/en/p/ads1500w
|
||||
.. _MFC-J6930DW: https://www.brother.ca/en/p/MFCJ6930DW
|
||||
.. _MFC-J5910DW: https://www.brother.co.uk/printers/inkjet-printers/mfcj5910dw
|
||||
@@ -41,6 +50,7 @@ Physical scanners
|
||||
.. _WF-7710DWF: https://www.epson.de/en/products/printers/inkjet-printers/for-home/workforce-wf-7710dwf
|
||||
.. _S1300i: https://www.fujitsu.com/global/products/computing/peripheral/scanners/soho/s1300i/
|
||||
|
||||
|
||||
.. _danielquinn: https://github.com/danielquinn
|
||||
.. _ayounggun: https://github.com/ayounggun
|
||||
.. _bmsleight: https://github.com/bmsleight
|
||||
@@ -48,25 +58,33 @@ Physical scanners
|
||||
.. _REOLDEV: https://github.com/REOLDEV
|
||||
.. _Skylinar: https://github.com/Skylinar
|
||||
.. _jonaswinkler: https://github.com/jonaswinkler
|
||||
.. _holzhannes: https://github.com/holzhannes
|
||||
|
||||
Mobile phone software
|
||||
=====================
|
||||
|
||||
You can use your phone to "scan" documents. The regular camera app will work, but may have too low contrast for OCR to work well. Apps specifically for scanning are recommended.
|
||||
|
||||
+-------------------+----------------+-----+-----+-----+-------+--------+----------------+
|
||||
| Name | OS | Supports | Recommended By |
|
||||
+-------------------+----------------+-----+-----+-----+-------+--------+----------------+
|
||||
| | | FTP | NFS | SMB | Email | WebDav | |
|
||||
+===================+================+=====+=====+=====+=======+========+================+
|
||||
| `Office Lens`_ | Android | ? | ? | ? | ? | ? | `jonaswinkler`_|
|
||||
+-------------------+----------------+-----+-----+-----+-------+--------+----------------+
|
||||
| `Genius Scan`_ | Android | yes | no | yes | yes | yes | `hannahswain`_ |
|
||||
+-------------------+----------------+-----+-----+-----+-------+--------+----------------+
|
||||
+-------------------+----------------+-----+-----+-----+-------+--------+------------------+
|
||||
| Name | OS | Supports | Recommended By |
|
||||
+-------------------+----------------+-----+-----+-----+-------+--------+------------------+
|
||||
| | | FTP | NFS | SMB | Email | WebDav | |
|
||||
+===================+================+=====+=====+=====+=======+========+==================+
|
||||
| `Office Lens`_ | Android | ? | ? | ? | ? | ? | `jonaswinkler`_ |
|
||||
+-------------------+----------------+-----+-----+-----+-------+--------+------------------+
|
||||
| `Genius Scan`_ | Android | yes | no | yes | yes | yes | `hannahswain`_ |
|
||||
+-------------------+----------------+-----+-----+-----+-------+--------+------------------+
|
||||
| `OpenScan`_ | Android | no | no | no | no | no | `benjaminfrank`_ |
|
||||
+-------------------+----------------+-----+-----+-----+-------+--------+------------------+
|
||||
| `Quick Scan`_ | iOS | no | no | no | no | no | `holzhannes`_ |
|
||||
+-------------------+----------------+-----+-----+-----+-------+--------+------------------+
|
||||
|
||||
On Android, you can use these applications in combination with one of the :ref:`Paperless-ng compatible apps <usage-mobile_upload>` to "Share" the documents produced by these scanner apps with paperless.
|
||||
On Android, you can use these applications in combination with one of the :ref:`Paperless-ng compatible apps <usage-mobile_upload>` to "Share" the documents produced by these scanner apps with paperless. On iOS, you can share the scanned documents via iOS-Sharing to other mail, WebDav or FTP apps.
|
||||
|
||||
.. _Office Lens: https://play.google.com/store/apps/details?id=com.microsoft.office.officelens
|
||||
.. _Genius Scan: https://play.google.com/store/apps/details?id=com.thegrizzlylabs.geniusscan.free
|
||||
.. _Quick Scan: https://apps.apple.com/us/app/quickscan-scanner-text-ocr/id1513790291
|
||||
.. _OpenScan: https://github.com/Ethereal-Developers-Inc/OpenScan
|
||||
|
||||
.. _hannahswain: https://github.com/hannahswain
|
||||
.. _benjaminfrank: https://github.com/benjaminfrank
|
||||
|
@@ -460,6 +460,7 @@ Install Paperless using ansible
|
||||
.. note::
|
||||
|
||||
This role currently only supports Debian 10 Buster and Ubuntu 20.04 Focal or later as target hosts.
|
||||
Additionally, only i386 or amd64 based hosts are supported right now, i.e. installation on arm hosts will fail.
|
||||
|
||||
1. Install ansible 2.7+ on the management node.
|
||||
This may be the target host paperless-ng is being installed on or any remote host which can access the target host.
|
||||
@@ -485,29 +486,22 @@ Install Paperless using ansible
|
||||
|
||||
ansible -m ping YourAnsibleTargetHostGoesHere
|
||||
|
||||
2. Clone the repository of paperless-ng:
|
||||
2. Install the latest tag of the ansible role using ansible-galaxy
|
||||
|
||||
.. code:: sh
|
||||
|
||||
git clone https://github.com/jonaswinkler/paperless-ng
|
||||
ansible-galaxy install git+https://github.com/jonaswinkler/paperless-ng.git,ng-1.4.2
|
||||
|
||||
Checkout the latest release tag:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
cd paperless-ng
|
||||
git checkout ng-1.0.0
|
||||
|
||||
3. Create an ansible ``playbook.yml`` in the paperless-ng root directory:
|
||||
3. Create an ansible ``playbook.yml`` in a directory of your choice:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
- hosts: YourAnsibleTargetHostGoesHere
|
||||
become: yes
|
||||
vars_files:
|
||||
- ansible/vars.yml
|
||||
- vars/paperless-ng.yml
|
||||
roles:
|
||||
- ansible
|
||||
- paperless-ng
|
||||
|
||||
Optional: If you also want to use PostgreSQL on the target system, install and add (for example) the `geerlingguy.postgresql <https://github.com/geerlingguy/ansible-role-postgresql>`_ role:
|
||||
|
||||
@@ -520,10 +514,10 @@ Install Paperless using ansible
|
||||
- hosts: YourAnsibleTargetHostGoesHere
|
||||
become: yes
|
||||
vars_files:
|
||||
- ansible/vars.yml
|
||||
- vars/paperless-ng.yml
|
||||
roles:
|
||||
- geerlingguy.postgresql
|
||||
- ansible
|
||||
- paperless-ng
|
||||
|
||||
Optional: If you also want to use a reverse proxy on the target system, install and add (for example) the `geerlingguy.nginx <https://github.com/geerlingguy/ansible-role-nginx>`_ role:
|
||||
|
||||
@@ -536,13 +530,13 @@ Install Paperless using ansible
|
||||
- hosts: YourAnsibleTargetHostGoesHere
|
||||
become: yes
|
||||
vars_files:
|
||||
- ansible/vars.yml
|
||||
- vars/paperless-ng.yml
|
||||
roles:
|
||||
- geerlingguy.postgresql
|
||||
- ansible
|
||||
- paperless-ng
|
||||
- geerlingguy.nginx
|
||||
|
||||
4. Create ``ansible/vars.yml`` to configure your ansible deployment:
|
||||
4. Create ``vars/paperless-ng.yml`` to configure your ansible deployment:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
|
@@ -208,3 +208,16 @@ This might have multiple reasons.
|
||||
SENDFILE=0
|
||||
|
||||
to your `docker-compose.env` file.
|
||||
|
||||
Error while reading metadata
|
||||
############################
|
||||
|
||||
You might find messages like these in your log files:
|
||||
|
||||
.. code::
|
||||
|
||||
[WARNING] [paperless.parsing.tesseract] Error while reading metadata
|
||||
|
||||
This indicates that paperless failed to read PDF metadata from one of your documents. This happens when you
|
||||
open the affected documents in paperless for editing. Paperless will continue to work, and will simply not
|
||||
show the invalid metadata.
|
||||
|
@@ -255,6 +255,8 @@ Here are a couple examples of tags and types that you could use in your collecti
|
||||
* A tag ``missing_metadata`` when you still need to add some metadata to a document, but can't
|
||||
or don't want to do this right now.
|
||||
|
||||
.. _basic-usage_searching:
|
||||
|
||||
Searching
|
||||
#########
|
||||
|
||||
|
@@ -2,7 +2,7 @@ import os
|
||||
|
||||
bind = '0.0.0.0:8000'
|
||||
workers = int(os.getenv("PAPERLESS_WEBSERVER_WORKERS", 2))
|
||||
worker_class = 'uvicorn.workers.UvicornWorker'
|
||||
worker_class = 'paperless.workers.ConfigurableWorker'
|
||||
timeout = 120
|
||||
|
||||
def pre_fork(server, worker):
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
ask() {
|
||||
while true ; do
|
||||
if [[ -z $3 ]] ; then
|
||||
@@ -64,6 +62,21 @@ if [[ -z $(which docker-compose) ]] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if user has permissions to run Docker by trying to get the status of Docker (docker status).
|
||||
# If this fails, the user probably does not have permissions for Docker.
|
||||
docker stats --no-stream 2>/dev/null 1>&2
|
||||
if [ $? -ne 0 ] ; then
|
||||
echo ""
|
||||
echo "WARN: It look like the current user does not have Docker permissions."
|
||||
echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user."
|
||||
echo ""
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
default_time_zone=$(timedatectl show -p Timezone --value)
|
||||
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
echo "############################################"
|
||||
echo "### Paperless-ng docker installation ###"
|
||||
@@ -134,6 +147,15 @@ echo ""
|
||||
ask "Port" "8000"
|
||||
PORT=$ask_result
|
||||
|
||||
echo ""
|
||||
echo "Paperless requires you to configure the current time zone correctly."
|
||||
echo "Otherwise, the dates of your documents may appear off by one day,"
|
||||
echo "depending on where you are on earth."
|
||||
echo ""
|
||||
|
||||
ask "Current time zone" "$default_time_zone"
|
||||
TIME_ZONE=$ask_result
|
||||
|
||||
echo ""
|
||||
echo "Database backend: PostgreSQL and SQLite are available. Use PostgreSQL"
|
||||
echo "if unsure. If you're running on a low-power device such as Raspberry"
|
||||
@@ -269,6 +291,7 @@ DEFAULT_LANGUAGES="deu eng fra ita spa"
|
||||
if [[ ! $USERMAP_GID == "1000" ]] ; then
|
||||
echo "USERMAP_GID=$USERMAP_GID"
|
||||
fi
|
||||
echo "PAPERLESS_TIME_ZONE=$TIME_ZONE"
|
||||
echo "PAPERLESS_OCR_LANGUAGE=$OCR_LANGUAGE"
|
||||
echo "PAPERLESS_SECRET_KEY=$SECRET_KEY"
|
||||
if [[ ! " ${DEFAULT_LANGUAGES[@]} " =~ " ${OCR_LANGUAGE} " ]] ; then
|
||||
|
@@ -8,11 +8,11 @@
|
||||
-i https://pypi.python.org/simple
|
||||
--extra-index-url https://www.piwheels.org/simple
|
||||
aioredis==1.3.1
|
||||
arrow==1.0.1; python_version >= '3.6'
|
||||
asgiref==3.3.1; python_version >= '3.5'
|
||||
arrow==1.1.0; python_version >= '3.6'
|
||||
asgiref==3.3.4; python_version >= '3.6'
|
||||
async-timeout==3.0.1; python_full_version >= '3.5.3'
|
||||
attrs==20.3.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
autobahn==21.2.2; python_version >= '3.7'
|
||||
attrs==21.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
autobahn==21.3.1; python_version >= '3.7'
|
||||
automat==20.2.0
|
||||
blessed==1.18.0
|
||||
certifi==2020.12.5
|
||||
@@ -24,71 +24,71 @@ click==7.1.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2,
|
||||
coloredlogs==15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
concurrent-log-handler==0.9.19
|
||||
constantly==15.1.0
|
||||
cryptography==3.4.6
|
||||
daphne==3.0.1; python_version >= '3.6'
|
||||
cryptography==3.4.7
|
||||
daphne==3.0.2; python_version >= '3.6'
|
||||
dateparser==1.0.0
|
||||
django-cors-headers==3.7.0
|
||||
django-extensions==3.1.1
|
||||
django-extensions==3.1.3
|
||||
django-filter==2.4.0
|
||||
django-picklefield==3.0.1; python_version >= '3'
|
||||
django-q==1.3.4
|
||||
django==3.1.7
|
||||
djangorestframework==3.12.2
|
||||
django==3.2.3
|
||||
djangorestframework==3.12.4
|
||||
filelock==3.0.12
|
||||
fuzzywuzzy[speedup]==0.18.0
|
||||
gunicorn==20.0.4
|
||||
gunicorn==20.1.0
|
||||
h11==0.12.0; python_version >= '3.6'
|
||||
hiredis==1.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
httptools==0.1.1
|
||||
hiredis==2.0.0; python_version >= '3.6'
|
||||
httptools==0.1.2
|
||||
humanfriendly==9.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
hyperlink==21.0.0
|
||||
idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
imap-tools==0.38.1
|
||||
img2pdf==0.4.0
|
||||
incremental==17.5.0
|
||||
imap-tools==0.41.0
|
||||
img2pdf==0.4.1
|
||||
incremental==21.3.0
|
||||
inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
inotifyrecursive==0.3.5
|
||||
joblib==1.0.1; python_version >= '3.6'
|
||||
langdetect==1.0.8
|
||||
lxml==4.6.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
langdetect==1.0.9
|
||||
lxml==4.6.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
msgpack==1.0.2
|
||||
numpy==1.19.5
|
||||
ocrmypdf==11.7.0
|
||||
pathvalidate==2.3.2
|
||||
ocrmypdf==12.0.1
|
||||
pathvalidate==2.4.1
|
||||
pdfminer.six==20201018
|
||||
pikepdf==2.5.2
|
||||
pillow==8.1.0
|
||||
pikepdf==2.12.0
|
||||
pillow==8.2.0
|
||||
pluggy==0.13.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
portalocker==2.2.1; python_version >= '3'
|
||||
portalocker==2.3.0; python_version >= '3'
|
||||
psycopg2-binary==2.8.6
|
||||
pyasn1-modules==0.2.8
|
||||
pyasn1==0.4.8
|
||||
pycparser==2.20; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
pyopenssl==20.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
python-dateutil==2.8.1
|
||||
python-dotenv==0.15.0
|
||||
python-gnupg==0.4.6
|
||||
python-dotenv==0.17.1
|
||||
python-gnupg==0.4.7
|
||||
python-levenshtein==0.12.2
|
||||
python-magic==0.4.22
|
||||
pytz==2021.1
|
||||
pyyaml==5.4.1
|
||||
redis==3.5.3
|
||||
regex==2020.11.13
|
||||
reportlab==3.5.59
|
||||
regex==2021.4.4
|
||||
reportlab==3.5.67; python_version >= '2.7' and python_version < '4'
|
||||
requests==2.25.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
scikit-learn==0.24.0
|
||||
scipy==1.5.4
|
||||
service-identity==18.1.0
|
||||
six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
service-identity==21.1.0
|
||||
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
sortedcontainers==2.3.0
|
||||
sqlparse==0.4.1; python_version >= '3.5'
|
||||
threadpoolctl==2.1.0; python_version >= '3.5'
|
||||
tika==1.24
|
||||
tqdm==4.58.0
|
||||
tqdm==4.60.0
|
||||
twisted[tls]==21.2.0; python_full_version >= '3.5.4'
|
||||
txaio==21.2.1; python_version >= '3.6'
|
||||
tzlocal==2.1
|
||||
urllib3==1.26.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
|
||||
urllib3==1.26.4; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
|
||||
uvicorn[standard]==0.13.4
|
||||
uvloop==0.14.0
|
||||
watchdog==1.0.2
|
||||
@@ -97,4 +97,4 @@ wcwidth==0.2.5
|
||||
websockets==8.1
|
||||
whitenoise==5.2.0
|
||||
whoosh==2.7.4
|
||||
zope.interface==5.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
|
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=Paperless consumer
|
||||
Description=Paperless scheduler
|
||||
Requires=redis.service
|
||||
|
||||
[Service]
|
||||
|
@@ -16,13 +16,18 @@
|
||||
"i18n": {
|
||||
"sourceLocale": "en-US",
|
||||
"locales": {
|
||||
"de": "src/locale/messages.de.xlf",
|
||||
"de-DE": "src/locale/messages.de_DE.xlf",
|
||||
"nl-NL": "src/locale/messages.nl_NL.xlf",
|
||||
"fr": "src/locale/messages.fr.xlf",
|
||||
"fr-FR": "src/locale/messages.fr_FR.xlf",
|
||||
"en-GB": "src/locale/messages.en_GB.xlf",
|
||||
"pt-BR": "src/locale/messages.pt_BR.xlf",
|
||||
"it": "src/locale/messages.it.xlf",
|
||||
"ro": "src/locale/messages.ro.xlf"
|
||||
"pt-PT": "src/locale/messages.pt_PT.xlf",
|
||||
"it-IT": "src/locale/messages.it_IT.xlf",
|
||||
"ro-RO": "src/locale/messages.ro_RO.xlf",
|
||||
"ru-RU": "src/locale/messages.ru_RU.xlf",
|
||||
"es-ES": "src/locale/messages.es_ES.xlf",
|
||||
"pl-PL": "src/locale/messages.pl_PL.xlf",
|
||||
"sv-SE": "src/locale/messages.sv_SE.xlf"
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
@@ -104,7 +109,8 @@
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "paperless-ui:build"
|
||||
"browserTarget": "paperless-ui:build",
|
||||
"ivy": true
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
|
@@ -48,21 +48,21 @@
|
||||
<source>Documents</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||
<context context-type="linenumber">49</context>
|
||||
<context context-type="linenumber">51</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2155249406916744630" datatype="html">
|
||||
<source>View "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>" saved successfully.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||
<context context-type="linenumber">115</context>
|
||||
<context context-type="linenumber">116</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6837554170707123455" datatype="html">
|
||||
<source>View "<x id="PH" equiv-text="savedView.name"/>" created successfully.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||
<context context-type="linenumber">136</context>
|
||||
<context context-type="linenumber">138</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="9ca82952a6bc860b5391d5975322d8af8ceddfa4" datatype="html">
|
||||
@@ -146,77 +146,77 @@
|
||||
<source>ASN</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||
<context context-type="linenumber">105</context>
|
||||
<context context-type="linenumber">111</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7b5c6286aaded63fb279d6deb8aa8c704e085ced" datatype="html">
|
||||
<source>Correspondent</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||
<context context-type="linenumber">111</context>
|
||||
<context context-type="linenumber">117</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="fdf7cbdc140d0aab0f0b6c06065a0fd448ed6a2e" datatype="html">
|
||||
<source>Title</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||
<context context-type="linenumber">117</context>
|
||||
<context context-type="linenumber">123</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2bd5919e8098513664a89d5b7b52d61e3063950f" datatype="html">
|
||||
<source>Document type</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||
<context context-type="linenumber">123</context>
|
||||
<context context-type="linenumber">129</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
|
||||
<source>Created</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||
<context context-type="linenumber">129</context>
|
||||
<context context-type="linenumber">135</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="80e3b490720757978c99a7b5af3885faf202b955" datatype="html">
|
||||
<source>Added</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||
<context context-type="linenumber">135</context>
|
||||
<context context-type="linenumber">141</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="9021887951960049161" datatype="html">
|
||||
<source>Confirm delete</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||
<context context-type="linenumber">203</context>
|
||||
<context context-type="linenumber">206</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5382975254277698192" datatype="html">
|
||||
<source>Do you really want to delete document "<x id="PH" equiv-text="this.document.title"/>"?</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||
<context context-type="linenumber">204</context>
|
||||
<context context-type="linenumber">207</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6691075929777935948" datatype="html">
|
||||
<source>The files for this document will be deleted permanently. This operation cannot be undone.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||
<context context-type="linenumber">205</context>
|
||||
<context context-type="linenumber">208</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="719892092227206532" datatype="html">
|
||||
<source>Delete document</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||
<context context-type="linenumber">207</context>
|
||||
<context context-type="linenumber">210</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1844801255494293730" datatype="html">
|
||||
<source>Error deleting document: <x id="PH" equiv-text="JSON.stringify(error)"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||
<context context-type="linenumber">214</context>
|
||||
<context context-type="linenumber">217</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
|
||||
@@ -912,48 +912,6 @@
|
||||
<context context-type="linenumber">25</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="49c9ede51100b454f7841b24cd02355c6622bf44" datatype="html">
|
||||
<source>Search results</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/search/search.component.html</context>
|
||||
<context context-type="linenumber">1</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="31976d04f98e8a38098f66ac3a83ad33b576e5db" datatype="html">
|
||||
<source>Invalid search query: <x id="INTERPOLATION" equiv-text="{{errorMessage}}"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/search/search.component.html</context>
|
||||
<context context-type="linenumber">4</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2abff6a01d9b342a5a14b7fb90309a95ce934f8e" datatype="html">
|
||||
<source> Showing documents similar to <x id="START_LINK" equiv-text="<a routerLink="/documents/{{more_like}}">{{more_like_doc?.original_file_name}}"/><x id="INTERPOLATION" equiv-text="{{more_like_doc?.original_file_name}}</a>"/><x id="CLOSE_LINK" equiv-text="</a>"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/search/search.component.html</context>
|
||||
<context context-type="linenumber">7</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6e0b0a1ea16f18f2fb1586c53d99d2f22e1aee2e" datatype="html">
|
||||
<source>Search query: <x id="START_ITALIC_TEXT" equiv-text="<i>{{query}}"/><x id="INTERPOLATION" equiv-text="{{query}}</i>"/><x id="CLOSE_ITALIC_TEXT" equiv-text="</i>"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/search/search.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="afa760e48c97d64d19c1455d18b7834a2256e23f" datatype="html">
|
||||
<source>Did you mean "<x id="START_LINK" equiv-text="<a [routerLink]="" (click)="searchCorrectedQuery()">{{correctedQuery}}"/><x id="INTERPOLATION" equiv-text="{{correctedQuery}}</a>"/><x id="CLOSE_LINK" equiv-text="</a>"/>"?</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/search/search.component.html</context>
|
||||
<context context-type="linenumber">13</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="fe6ced3fcc803bba5a2e6c1a067b9ce62542500e" datatype="html">
|
||||
<source>{VAR_PLURAL, plural, =0 {No results} =1 {One result} other {<x id="INTERPOLATION"/> results}}</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/search/search.component.html</context>
|
||||
<context context-type="linenumber">18</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="41147374f427980a9f1a8cd5e3f4b1666e6f2418" datatype="html">
|
||||
<source>Paperless-ng</source>
|
||||
<context-group purpose="location">
|
||||
@@ -1039,95 +997,123 @@
|
||||
<context context-type="linenumber">106</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5701618810648052610" datatype="html">
|
||||
<source>Title</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">73</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3100631071441658964" datatype="html">
|
||||
<source>Title & content</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">74</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5195932016807797291" datatype="html">
|
||||
<source>Correspondent: <x id="PH" equiv-text="this.correspondents.find(c => c.id == +rule.value)?.name"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">32</context>
|
||||
<context context-type="linenumber">37</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8170755470576301659" datatype="html">
|
||||
<source>Without correspondent</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">34</context>
|
||||
<context context-type="linenumber">39</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8705701325879965907" datatype="html">
|
||||
<source>Type: <x id="PH" equiv-text="this.documentTypes.find(dt => dt.id == +rule.value)?.name"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">39</context>
|
||||
<context context-type="linenumber">44</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4362173610367509215" datatype="html">
|
||||
<source>Without document type</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">41</context>
|
||||
<context context-type="linenumber">46</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8180755793012580465" datatype="html">
|
||||
<source>Tag: <x id="PH" equiv-text="this.tags.find(t => t.id == +rule.value)?.name"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
<context context-type="linenumber">50</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6494566478302448576" datatype="html">
|
||||
<source>Without any tag</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">49</context>
|
||||
<context context-type="linenumber">54</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6523384805359286307" datatype="html">
|
||||
<source>Title: <x id="PH" equiv-text="rule.value"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">53</context>
|
||||
<context context-type="linenumber">58</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1872523635812236432" datatype="html">
|
||||
<source>ASN: <x id="PH" equiv-text="rule.value"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">61</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5701618810648052610" datatype="html">
|
||||
<source>Title</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">88</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3100631071441658964" datatype="html">
|
||||
<source>Title & content</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">89</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7517688192215738656" datatype="html">
|
||||
<source>ASN</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">90</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1010505078885609376" datatype="html">
|
||||
<source>Advanced search</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">91</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2649431021108393503" datatype="html">
|
||||
<source>More like</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||
<context context-type="linenumber">94</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="02d184c288f567825a1fcbf83bcd3099a10853d5" datatype="html">
|
||||
<source>Filter tags</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
<context context-type="linenumber">19</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4b089ca12c472cf0b46167bb5afe4b527b301bbc" datatype="html">
|
||||
<source>Filter correspondents</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||
<context context-type="linenumber">28</context>
|
||||
<context context-type="linenumber">27</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="0ad509732aaf702b7ea8c771c7809fa84bc85908" datatype="html">
|
||||
<source>Filter document types</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||
<context context-type="linenumber">35</context>
|
||||
<context context-type="linenumber">34</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2d9d55f1b70142ff4597ba32179d16888fd9c6b2" datatype="html">
|
||||
<source>Reset filters</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||
<context context-type="linenumber">58</context>
|
||||
<context context-type="linenumber">57</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7593728289020204896" datatype="html">
|
||||
@@ -1198,14 +1184,7 @@
|
||||
<source>View</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
|
||||
<context context-type="linenumber">50</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="849b42384616374df49bd8b3711ec159cb10b845" datatype="html">
|
||||
<source>Created: <x id="INTERPOLATION" equiv-text="{{document.created | customDate}}"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
|
||||
<context context-type="linenumber">67</context>
|
||||
<context context-type="linenumber">51</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="cd6f3fd48957e1fea6545c2b2defc7b2435ebfa8" datatype="html">
|
||||
@@ -1226,14 +1205,28 @@
|
||||
<source>Score:</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
|
||||
<context context-type="linenumber">62</context>
|
||||
<context context-type="linenumber">87</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2840db547019ce8c76b2cdbe3a1653c5b68b06af" datatype="html">
|
||||
<source>View in browser</source>
|
||||
<trans-unit id="727d980bba2b3e0b3d8705607f1208eef046479b" datatype="html">
|
||||
<source>Created: <x id="INTERPOLATION" equiv-text="{{ document.created | customDate}}"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
|
||||
<context context-type="linenumber">40</context>
|
||||
<context context-type="linenumber">43</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="0f5d856cb63c69fde44fbfc653ec0655f9040865" datatype="html">
|
||||
<source>Added: <x id="INTERPOLATION" equiv-text="{{ document.added | customDate}}"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
|
||||
<context context-type="linenumber">44</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="a205126adef6251fc63305f1a6228d07bc2795a8" datatype="html">
|
||||
<source>Modified: <x id="INTERPOLATION" equiv-text="{{ document.modified | customDate}}"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7985804062689412812" datatype="html">
|
||||
@@ -1414,11 +1407,19 @@
|
||||
<context context-type="linenumber">68</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="0f72a164f5a5d10d1fbdeeb84ac80a63c04b7e1b" datatype="html">
|
||||
<source>Add item</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Used for both types and correspondents</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="a1e6c11f20d4bf6e8e6b43e3c6d2561b2080645e" datatype="html">
|
||||
<source>Suggestions:</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
|
||||
<context context-type="linenumber">26</context>
|
||||
<context context-type="linenumber">31</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="27d158b47717ff9305d19866960418c603f19d55" datatype="html">
|
||||
@@ -1428,6 +1429,13 @@
|
||||
<context context-type="linenumber">3</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="21fd41eb147f55ca26362f9c398e47733639837a" datatype="html">
|
||||
<source>Add tag</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4eb84de23219c85432e38fb4fbdeb6c0f103ff8b" datatype="html">
|
||||
<source>Show all</source>
|
||||
<context-group purpose="location">
|
||||
@@ -1626,6 +1634,13 @@
|
||||
<context context-type="linenumber">14</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="bd5ca454e336126f3eeb67417d9264696b5c852c" datatype="html">
|
||||
<source>Searching document with asn <x id="INTERPOLATION" equiv-text="{{asn}}"/></source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-asn/document-asn.component.html</context>
|
||||
<context context-type="linenumber">1</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2807800733729323332" datatype="html">
|
||||
<source>Yes</source>
|
||||
<context-group purpose="location">
|
||||
@@ -1682,32 +1697,67 @@
|
||||
<context context-type="linenumber">94</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="153799456510623899" datatype="html">
|
||||
<source>Portuguese</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">95</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="9184513005098760425" datatype="html">
|
||||
<source>Portuguese (Brazil)</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">95</context>
|
||||
<context context-type="linenumber">96</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2935232983274991580" datatype="html">
|
||||
<source>Italian</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">96</context>
|
||||
<context context-type="linenumber">97</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8118856427047826368" datatype="html">
|
||||
<source>Romanian</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">97</context>
|
||||
<context context-type="linenumber">98</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7137419789978325708" datatype="html">
|
||||
<source>Russian</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">99</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5190825892106392539" datatype="html">
|
||||
<source>Spanish</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">100</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="792060551707690640" datatype="html">
|
||||
<source>Polish</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">101</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="499386805970351976" datatype="html">
|
||||
<source>Swedish</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">102</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4912706592792948707" datatype="html">
|
||||
<source>ISO 8601</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">102</context>
|
||||
<context context-type="linenumber">107</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2119857572761283468" datatype="html">
|
||||
@@ -1819,13 +1869,6 @@
|
||||
<context context-type="linenumber">39</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7517688192215738656" datatype="html">
|
||||
<source>ASN</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/rest/document.service.ts</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2691296884221415710" datatype="html">
|
||||
<source>Correspondent</source>
|
||||
<context-group purpose="location">
|
||||
@@ -1861,6 +1904,14 @@
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4460262093225954455" datatype="html">
|
||||
<source>Search score</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/rest/document.service.ts</context>
|
||||
<context context-type="linenumber">28</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Score is a value returned by the full text search engine and specifies how well a result matches the given query</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="4561076822163447092" datatype="html">
|
||||
<source>Create new item</source>
|
||||
<context-group purpose="location">
|
||||
|
@@ -10,7 +10,7 @@ import { LogsComponent } from './components/manage/logs/logs.component';
|
||||
import { SettingsComponent } from './components/manage/settings/settings.component';
|
||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component';
|
||||
import { NotFoundComponent } from './components/not-found/not-found.component';
|
||||
import { SearchComponent } from './components/search/search.component';
|
||||
import {DocumentAsnComponent} from "./components/document-asn/document-asn.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{path: '', redirectTo: 'dashboard', pathMatch: 'full'},
|
||||
@@ -18,15 +18,15 @@ const routes: Routes = [
|
||||
{path: 'dashboard', component: DashboardComponent },
|
||||
{path: 'documents', component: DocumentListComponent },
|
||||
{path: 'view/:id', component: DocumentListComponent },
|
||||
{path: 'search', component: SearchComponent },
|
||||
{path: 'documents/:id', component: DocumentDetailComponent },
|
||||
|
||||
{path: 'asn/:id', component: DocumentAsnComponent },
|
||||
|
||||
{path: 'tags', component: TagListComponent },
|
||||
{path: 'documenttypes', component: DocumentTypeListComponent },
|
||||
{path: 'correspondents', component: CorrespondentListComponent },
|
||||
{path: 'logs', component: LogsComponent },
|
||||
{path: 'settings', component: SettingsComponent },
|
||||
]},
|
||||
]},
|
||||
|
||||
{path: '404', component: NotFoundComponent},
|
||||
{path: '**', redirectTo: '/404', pathMatch: 'full'}
|
||||
|
@@ -18,7 +18,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor (private settings: SettingsService, private consumerStatusService: ConsumerStatusService, private toastService: ToastService, private router: Router) {
|
||||
let anyWindow = (window as any)
|
||||
anyWindow.pdfWorkerSrc = '/assets/js/pdf.worker.min.js';
|
||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js';
|
||||
this.settings.updateDarkModeSettings()
|
||||
}
|
||||
|
||||
|
@@ -21,8 +21,6 @@ import { CorrespondentEditDialogComponent } from './components/manage/correspond
|
||||
import { TagEditDialogComponent } from './components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component';
|
||||
import { DocumentTypeEditDialogComponent } from './components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
|
||||
import { TagComponent } from './components/common/tag/tag.component';
|
||||
import { SearchComponent } from './components/search/search.component';
|
||||
import { ResultHighlightComponent } from './components/search/result-highlight/result-highlight.component';
|
||||
import { PageHeaderComponent } from './components/common/page-header/page-header.component';
|
||||
import { AppFrameComponent } from './components/app-frame/app-frame.component';
|
||||
import { ToastsComponent } from './components/common/toasts/toasts.component';
|
||||
@@ -65,6 +63,7 @@ import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'
|
||||
import { ApiVersionInterceptor } from './interceptors/api-version.interceptor';
|
||||
import { ColorSliderModule } from 'ngx-color/slider';
|
||||
import { ColorComponent } from './components/common/input/color/color.component';
|
||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component';
|
||||
|
||||
import localeFr from '@angular/common/locales/fr';
|
||||
import localeNl from '@angular/common/locales/nl';
|
||||
@@ -73,15 +72,24 @@ import localePt from '@angular/common/locales/pt';
|
||||
import localeIt from '@angular/common/locales/it';
|
||||
import localeEnGb from '@angular/common/locales/en-GB';
|
||||
import localeRo from '@angular/common/locales/ro';
|
||||
import localeRu from '@angular/common/locales/ru';
|
||||
import localeEs from '@angular/common/locales/es';
|
||||
import localePl from '@angular/common/locales/pl';
|
||||
import localeSv from '@angular/common/locales/sv';
|
||||
|
||||
|
||||
registerLocaleData(localeFr)
|
||||
registerLocaleData(localeNl)
|
||||
registerLocaleData(localeDe)
|
||||
registerLocaleData(localePt, "pt-BR")
|
||||
registerLocaleData(localePt, "pt-PT")
|
||||
registerLocaleData(localeIt)
|
||||
registerLocaleData(localeEnGb)
|
||||
registerLocaleData(localeRo)
|
||||
registerLocaleData(localeRu)
|
||||
registerLocaleData(localeEs)
|
||||
registerLocaleData(localePl)
|
||||
registerLocaleData(localeSv)
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -100,8 +108,6 @@ registerLocaleData(localeRo)
|
||||
TagEditDialogComponent,
|
||||
DocumentTypeEditDialogComponent,
|
||||
TagComponent,
|
||||
SearchComponent,
|
||||
ResultHighlightComponent,
|
||||
PageHeaderComponent,
|
||||
AppFrameComponent,
|
||||
ToastsComponent,
|
||||
@@ -133,7 +139,8 @@ registerLocaleData(localeRo)
|
||||
SafePipe,
|
||||
CustomDatePipe,
|
||||
DateComponent,
|
||||
ColorComponent
|
||||
ColorComponent,
|
||||
DocumentAsnComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<a class="navbar-brand col-auto col-md-3 col-lg-2 mr-0 px-3 py-3 order-sm-0" routerLink="/dashboard">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1rem" class="mr-2" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="mr-2" fill="currentColor">
|
||||
<path d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z" transform="translate(0 0)"/>
|
||||
</svg>
|
||||
<ng-container i18n="app title">Paperless-ng</ng-container>
|
||||
@@ -31,7 +31,7 @@
|
||||
</button>
|
||||
<div ngbDropdownMenu class="dropdown-menu-right shadow mr-2" aria-labelledby="userDropdown">
|
||||
<div *ngIf="displayName" class="d-sm-none">
|
||||
<p class="small mb-0 px-3" i18n>Logged in as {{displayName}}</p>
|
||||
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{displayName}}</p>
|
||||
<div class="dropdown-divider"></div>
|
||||
</div>
|
||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()">
|
||||
@@ -175,7 +175,7 @@
|
||||
</svg> <ng-container i18n>GitHub</ng-container>
|
||||
</a>
|
||||
<a class="nav-link-additional small text-muted ml-3" target="_blank" rel="noopener noreferrer" href="https://github.com/jonaswinkler/paperless-ng/discussions/categories/feature-requests" title="Suggest an idea">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width=".9rem" height=".9rem" fill="currentColor" class="bi bi-lightbulb pr-1" viewBox="0 0 16 16">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" fill="currentColor" class="bi bi-lightbulb pr-1" viewBox="0 0 16 16">
|
||||
<path d="M2 6a6 6 0 1 1 10.174 4.31c-.203.196-.359.4-.453.619l-.762 1.769A.5.5 0 0 1 10.5 13a.5.5 0 0 1 0 1 .5.5 0 0 1 0 1l-.224.447a1 1 0 0 1-.894.553H6.618a1 1 0 0 1-.894-.553L5.5 15a.5.5 0 0 1 0-1 .5.5 0 0 1 0-1 .5.5 0 0 1-.46-.302l-.761-1.77a1.964 1.964 0 0 0-.453-.618A5.984 5.984 0 0 1 2 6zm6-5a5 5 0 0 0-3.479 8.592c.263.254.514.564.676.941L5.83 12h4.342l.632-1.467c.162-.377.413-.687.676-.941A5 5 0 0 0 8 1z"/>
|
||||
</svg>
|
||||
<ng-container i18n>Suggest an idea</ng-container>
|
||||
|
@@ -10,6 +10,8 @@ import { SearchService } from 'src/app/services/rest/search.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component';
|
||||
import { Meta } from '@angular/platform-browser';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type';
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-frame',
|
||||
@@ -24,6 +26,7 @@ export class AppFrameComponent implements OnInit {
|
||||
private openDocumentsService: OpenDocumentsService,
|
||||
private searchService: SearchService,
|
||||
public savedViewService: SavedViewService,
|
||||
private list: DocumentListViewService,
|
||||
private meta: Meta
|
||||
) {
|
||||
|
||||
@@ -74,7 +77,7 @@ export class AppFrameComponent implements OnInit {
|
||||
|
||||
search() {
|
||||
this.closeMenu()
|
||||
this.router.navigate(['search'], {queryParams: {query: this.searchField.value}})
|
||||
this.list.quickFilter([{rule_type: FILTER_FULLTEXT_QUERY, value: this.searchField.value}])
|
||||
}
|
||||
|
||||
closeDocument(d: PaperlessDocument) {
|
||||
|
@@ -1,33 +1,38 @@
|
||||
<div class="form-group paperless-input-select">
|
||||
<label [for]="inputId">{{title}}</label>
|
||||
<div [class.input-group]="showPlusButton()">
|
||||
<ng-select name="inputId" [(ngModel)]="value"
|
||||
[disabled]="disabled"
|
||||
[style.color]="textColor"
|
||||
[style.background]="backgroundColor"
|
||||
[clearable]="allowNull"
|
||||
[items]="items"
|
||||
bindLabel="name"
|
||||
bindValue="id"
|
||||
(change)="onChange(value)"
|
||||
(blur)="onTouched()">
|
||||
</ng-select>
|
||||
|
||||
<div *ngIf="showPlusButton()" class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="createNew.emit()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
</button>
|
||||
<div [class.input-group]="allowCreateNew">
|
||||
<ng-select name="inputId" [(ngModel)]="value"
|
||||
[disabled]="disabled"
|
||||
[style.color]="textColor"
|
||||
[style.background]="backgroundColor"
|
||||
[clearable]="allowNull"
|
||||
[items]="items"
|
||||
[addTag]="allowCreateNew && addItemRef"
|
||||
addTagText="Add item"
|
||||
i18n-addTagText="Used for both types and correspondents"
|
||||
bindLabel="name"
|
||||
bindValue="id"
|
||||
(change)="onChange(value)"
|
||||
(search)="onSearch($event)"
|
||||
(focus)="clearLastSearchTerm()"
|
||||
(clear)="clearLastSearchTerm()"
|
||||
(blur)="onBlur()">
|
||||
</ng-select>
|
||||
<div *ngIf="allowCreateNew" class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="addItem()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||
<small *ngIf="getSuggestions().length > 0">
|
||||
<span i18n>Suggestions:</span>
|
||||
<ng-container *ngFor="let s of getSuggestions()">
|
||||
<a (click)="value = s.id; onChange(value)" [routerLink]="">{{s.name}}</a>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
|
||||
</small>
|
||||
</div>
|
||||
|
@@ -16,6 +16,7 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.addItemRef = this.addItem.bind(this)
|
||||
}
|
||||
|
||||
@Input()
|
||||
@@ -34,9 +35,13 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
||||
suggestions: number[]
|
||||
|
||||
@Output()
|
||||
createNew = new EventEmitter()
|
||||
createNew = new EventEmitter<string>()
|
||||
|
||||
showPlusButton(): boolean {
|
||||
public addItemRef: (name) => void
|
||||
|
||||
private _lastSearchTerm: string
|
||||
|
||||
get allowCreateNew(): boolean {
|
||||
return this.createNew.observers.length > 0
|
||||
}
|
||||
|
||||
@@ -48,4 +53,29 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
||||
}
|
||||
}
|
||||
|
||||
addItem(name: string) {
|
||||
if (name) this.createNew.next(name)
|
||||
else this.createNew.next(this._lastSearchTerm)
|
||||
this.clearLastSearchTerm()
|
||||
}
|
||||
|
||||
clickNew() {
|
||||
this.createNew.next(this._lastSearchTerm)
|
||||
this.clearLastSearchTerm()
|
||||
}
|
||||
|
||||
clearLastSearchTerm() {
|
||||
this._lastSearchTerm = null
|
||||
}
|
||||
|
||||
onSearch($event) {
|
||||
this._lastSearchTerm = $event.term
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
setTimeout(() => {
|
||||
this.clearLastSearchTerm()
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -7,8 +7,14 @@
|
||||
[closeOnSelect]="false"
|
||||
[clearSearchOnAdd]="true"
|
||||
[hideSelected]="true"
|
||||
[addTag]="createTagRef"
|
||||
addTagText="Add tag"
|
||||
i18n-addTagText
|
||||
(change)="onChange(value)"
|
||||
(blur)="onTouched()">
|
||||
(search)="onSearch($event)"
|
||||
(focus)="clearLastSearchTerm()"
|
||||
(clear)="clearLastSearchTerm()"
|
||||
(blur)="onBlur()">
|
||||
|
||||
<ng-template ng-label-tmp let-item="item">
|
||||
<span class="tag-wrap tag-wrap-delete" (click)="removeTag(item.id)">
|
||||
@@ -39,8 +45,8 @@
|
||||
<ng-container *ngFor="let tag of getSuggestions()">
|
||||
<a (click)="addTag(tag.id)" [routerLink]="">{{tag.name}}</a>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
|
||||
</small>
|
||||
|
||||
</div>
|
||||
|
@@ -17,8 +17,9 @@ import { TagService } from 'src/app/services/rest/tag.service';
|
||||
})
|
||||
export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
|
||||
constructor(private tagService: TagService, private modalService: NgbModal) { }
|
||||
|
||||
constructor(private tagService: TagService, private modalService: NgbModal) {
|
||||
this.createTagRef = this.createTag.bind(this)
|
||||
}
|
||||
|
||||
onChange = (newValue: number[]) => {};
|
||||
|
||||
@@ -56,6 +57,10 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
|
||||
tags: PaperlessTag[]
|
||||
|
||||
public createTagRef: (name) => void
|
||||
|
||||
private _lastSearchTerm: string
|
||||
|
||||
getTag(id) {
|
||||
if (this.tags) {
|
||||
return this.tags.find(tag => tag.id == id)
|
||||
@@ -74,9 +79,11 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
createTag() {
|
||||
createTag(name: string = null) {
|
||||
var modal = this.modalService.open(TagEditDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.dialogMode = 'create'
|
||||
if (name) modal.componentInstance.object = { name: name }
|
||||
else if (this._lastSearchTerm) modal.componentInstance.object = { name: this._lastSearchTerm }
|
||||
modal.componentInstance.success.subscribe(newTag => {
|
||||
this.tagService.listAll().subscribe(tags => {
|
||||
this.tags = tags.results
|
||||
@@ -99,4 +106,18 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
this.onChange(this.value)
|
||||
}
|
||||
|
||||
clearLastSearchTerm() {
|
||||
this._lastSearchTerm = null
|
||||
}
|
||||
|
||||
onSearch($event) {
|
||||
this._lastSearchTerm = $event.term
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
setTimeout(() => {
|
||||
this.clearLastSearchTerm()
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -23,7 +23,7 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .progress {
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
@@ -0,0 +1 @@
|
||||
<p i18n>Searching document with asn {{asn}}</p>
|
@@ -1,20 +1,20 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SearchComponent } from './search.component';
|
||||
import { DocumentAsnComponent } from './document-asn.component';
|
||||
|
||||
describe('SearchComponent', () => {
|
||||
let component: SearchComponent;
|
||||
let fixture: ComponentFixture<SearchComponent>;
|
||||
describe('DocumentASNComponentComponent', () => {
|
||||
let component: DocumentAsnComponent;
|
||||
let fixture: ComponentFixture<DocumentAsnComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ SearchComponent ]
|
||||
declarations: [ DocumentAsnComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchComponent);
|
||||
fixture = TestBed.createComponent(DocumentAsnComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@@ -0,0 +1,34 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import {DocumentService} from "../../services/rest/document.service";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {FILTER_ASN} from "../../data/filter-rule-type";
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-asncomponent',
|
||||
templateUrl: './document-asn.component.html',
|
||||
styleUrls: ['./document-asn.component.scss']
|
||||
})
|
||||
export class DocumentAsnComponent implements OnInit {
|
||||
|
||||
asn: string
|
||||
constructor(
|
||||
private documentsService: DocumentService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router) { }
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.route.paramMap.subscribe(paramMap => {
|
||||
this.asn = paramMap.get('id');
|
||||
this.documentsService.listAllFilteredIds([{rule_type: FILTER_ASN, value: this.asn}]).subscribe(documentId => {
|
||||
if (documentId.length == 1) {
|
||||
this.router.navigate(['documents', documentId[0]])
|
||||
} else {
|
||||
this.router.navigate(['404'])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
}
|
@@ -60,9 +60,9 @@
|
||||
<app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number>
|
||||
<app-input-date i18n-title title="Date created" formControlName="created" [error]="error?.created"></app-input-date>
|
||||
<app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
|
||||
(createNew)="createCorrespondent()" [suggestions]="suggestions?.correspondents"></app-input-select>
|
||||
(createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents"></app-input-select>
|
||||
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
|
||||
(createNew)="createDocumentType()" [suggestions]="suggestions?.document_types"></app-input-select>
|
||||
(createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types"></app-input-select>
|
||||
<app-input-tags formControlName="tags" [suggestions]="suggestions?.tags"></app-input-tags>
|
||||
|
||||
</ng-template>
|
||||
@@ -139,7 +139,7 @@
|
||||
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
|
||||
</div>
|
||||
<ng-template #nativePdfViewer>
|
||||
<object [data]="previewUrl | safe" type="application/pdf" class="preview-sticky" width="100%"></object>
|
||||
<object [data]="previewUrl | safe" class="preview-sticky" width="100%"></object>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="getContentType() == 'text/plain'">
|
||||
|
@@ -20,6 +20,7 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
import { TextComponent } from '../common/input/text/text.component';
|
||||
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
|
||||
import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions';
|
||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-detail',
|
||||
@@ -127,9 +128,10 @@ export class DocumentDetailComponent implements OnInit {
|
||||
this.documentForm.patchValue(doc)
|
||||
}
|
||||
|
||||
createDocumentType() {
|
||||
createDocumentType(newName: string) {
|
||||
var modal = this.modalService.open(DocumentTypeEditDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.dialogMode = 'create'
|
||||
if (newName) modal.componentInstance.object = { name: newName }
|
||||
modal.componentInstance.success.subscribe(newDocumentType => {
|
||||
this.documentTypeService.listAll().subscribe(documentTypes => {
|
||||
this.documentTypes = documentTypes.results
|
||||
@@ -138,9 +140,10 @@ export class DocumentDetailComponent implements OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
createCorrespondent() {
|
||||
createCorrespondent(newName: string) {
|
||||
var modal = this.modalService.open(CorrespondentEditDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.dialogMode = 'create'
|
||||
if (newName) modal.componentInstance.object = { name: newName }
|
||||
modal.componentInstance.success.subscribe(newCorrespondent => {
|
||||
this.correspondentService.listAll().subscribe(correspondents => {
|
||||
this.correspondents = correspondents.results
|
||||
@@ -219,7 +222,7 @@ export class DocumentDetailComponent implements OnInit {
|
||||
}
|
||||
|
||||
moreLike() {
|
||||
this.router.navigate(["search"], {queryParams: {more_like:this.document.id}})
|
||||
this.documentListViewService.quickFilter([{rule_type: FILTER_FULLTEXT_MORELIKE, value: this.documentId.toString()}])
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
|
||||
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable" [class.popover-hidden]="popoverHidden" (mouseleave)="mouseLeaveCard()">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-2 d-none d-lg-block doc-img-background rounded-left" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)">
|
||||
<img [src]="getThumbUrl()" class="card-img doc-img border-right rounded-left" [class.inverted]="getIsThumbInverted()">
|
||||
@@ -23,17 +23,16 @@
|
||||
{{document.title | documentTitle}}
|
||||
<app-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle *ngFor="let t of document.tags$ | async" class="ml-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></app-tag>
|
||||
</h5>
|
||||
<h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5>
|
||||
</div>
|
||||
<p class="card-text">
|
||||
<app-result-highlight *ngIf="getDetailsAsHighlight()" class="result-content" [highlights]="getDetailsAsHighlight()"></app-result-highlight>
|
||||
<span *ngIf="getDetailsAsString()" class="result-content">{{getDetailsAsString()}}</span>
|
||||
<span *ngIf="document.__search_hit__" [innerHtml]="document.__search_hit__.highlights"></span>
|
||||
<span *ngIf="!document.__search_hit__" class="result-content">{{contentTrimmed}}</span>
|
||||
</p>
|
||||
|
||||
|
||||
<div class="d-flex flex-column flex-md-row align-items-md-center">
|
||||
<div class="btn-group">
|
||||
<a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary" *ngIf="moreLikeThis">
|
||||
<a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg> <span class="d-block d-md-inline" i18n>More like this</span>
|
||||
@@ -43,28 +42,52 @@
|
||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg> <span class="d-block d-md-inline" i18n>Edit</span>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" [href]="getPreviewUrl()">
|
||||
<a class="btn btn-sm btn-outline-secondary" [href]="previewUrl"
|
||||
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
|
||||
autoClose="true" popoverClass="shadow" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
|
||||
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
||||
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
||||
</svg> <span class="d-block d-md-inline" i18n>View</span>
|
||||
</a>
|
||||
<ng-template #previewContent>
|
||||
<object [data]="previewUrl | safe" class="preview" width="100%"></object>
|
||||
</ng-template>
|
||||
<a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg> <span class="d-block d-md-inline" i18n>Download</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="searchScore" class="d-flex align-items-center ml-md-auto mt-2 mt-md-0">
|
||||
<small class="text-muted" i18n>Score:</small>
|
||||
<div class="list-group list-group-horizontal border-0 card-info ml-md-auto mt-2 mt-md-0">
|
||||
<button *ngIf="document.document_type" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 mr-2" title="Filter by document type"
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
<svg class="metadata-icon mr-2 text-muted bi bi-file-earmark" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
|
||||
</svg>
|
||||
<small>{{(document.document_type$ | async)?.name}}</small>
|
||||
</button>
|
||||
<div *ngIf="document.archive_serial_number" class="list-group-item mr-2 bg-light text-dark p-1 border-0">
|
||||
<svg class="metadata-icon mr-2 text-muted bi bi-upc-scan" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5zM3 4.5a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-7zm3 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7z"/>
|
||||
</svg>
|
||||
<small>#{{document.archive_serial_number}}</small>
|
||||
</div>
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0" ngbTooltip="Added: {{document.added | customDate:'shortDate'}} Created: {{document.created | customDate:'shortDate'}}">
|
||||
<svg class="metadata-icon mr-2 text-muted bi bi-calendar-event" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/>
|
||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||
</svg>
|
||||
<small>{{document.created | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
|
||||
<ngb-progressbar [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
|
||||
<div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 pl-4 search-score">
|
||||
<small class="text-muted" i18n>Score:</small>
|
||||
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small class="text-muted" [class.ml-auto]="!searchScore" i18n>Created: {{document.created | customDate}}</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -37,3 +37,31 @@
|
||||
.doc-img-background-selected {
|
||||
background-color: $primaryFaded;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
line-height: 1;
|
||||
|
||||
button {
|
||||
line-height: 1;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.metadata-icon {
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
padding: 0.05rem;
|
||||
}
|
||||
|
||||
.search-score {
|
||||
padding-top: 0.35rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
span ::ng-deep .match {
|
||||
color: black;
|
||||
background-color: rgb(255, 211, 66);
|
||||
}
|
@@ -1,13 +1,16 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-card-large',
|
||||
templateUrl: './document-card-large.component.html',
|
||||
styleUrls: ['./document-card-large.component.scss']
|
||||
styleUrls: ['./document-card-large.component.scss', '../popover-preview/popover-preview.scss']
|
||||
})
|
||||
export class DocumentCardLargeComponent implements OnInit {
|
||||
|
||||
@@ -23,31 +26,35 @@ export class DocumentCardLargeComponent implements OnInit {
|
||||
return this.toggleSelected.observers.length > 0
|
||||
}
|
||||
|
||||
@Input()
|
||||
moreLikeThis: boolean = false
|
||||
|
||||
@Input()
|
||||
document: PaperlessDocument
|
||||
|
||||
@Input()
|
||||
details: any
|
||||
|
||||
@Output()
|
||||
clickTag = new EventEmitter<number>()
|
||||
|
||||
@Output()
|
||||
clickCorrespondent = new EventEmitter<number>()
|
||||
|
||||
@Input()
|
||||
searchScore: number
|
||||
@Output()
|
||||
clickDocumentType = new EventEmitter<number>()
|
||||
|
||||
@Output()
|
||||
clickMoreLike= new EventEmitter()
|
||||
|
||||
@ViewChild('popover') popover: NgbPopover
|
||||
|
||||
mouseOnPreview = false
|
||||
popoverHidden = true
|
||||
|
||||
get searchScoreClass() {
|
||||
if (this.searchScore > 0.7) {
|
||||
return "success"
|
||||
} else if (this.searchScore > 0.3) {
|
||||
return "warning"
|
||||
} else {
|
||||
return "danger"
|
||||
if (this.document.__search_hit__) {
|
||||
if (this.document.__search_hit__.score > 0.7) {
|
||||
return "success"
|
||||
} else if (this.document.__search_hit__.score > 0.3) {
|
||||
return "warning"
|
||||
} else {
|
||||
return "danger"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,19 +65,6 @@ export class DocumentCardLargeComponent implements OnInit {
|
||||
return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)
|
||||
}
|
||||
|
||||
getDetailsAsString() {
|
||||
if (typeof this.details === 'string') {
|
||||
return this.details.substring(0, 500)
|
||||
}
|
||||
}
|
||||
|
||||
getDetailsAsHighlight() {
|
||||
//TODO: this is not an exact typecheck, can we do better
|
||||
if (this.details instanceof Array) {
|
||||
return this.details
|
||||
}
|
||||
}
|
||||
|
||||
getThumbUrl() {
|
||||
return this.documentService.getThumbUrl(this.document.id)
|
||||
}
|
||||
@@ -79,7 +73,36 @@ export class DocumentCardLargeComponent implements OnInit {
|
||||
return this.documentService.getDownloadUrl(this.document.id)
|
||||
}
|
||||
|
||||
getPreviewUrl() {
|
||||
get previewUrl() {
|
||||
return this.documentService.getPreviewUrl(this.document.id)
|
||||
}
|
||||
|
||||
mouseEnterPreview() {
|
||||
this.mouseOnPreview = true
|
||||
if (!this.popover.isOpen()) {
|
||||
// we're going to open but hide to pre-load content during hover delay
|
||||
this.popover.open()
|
||||
this.popoverHidden = true
|
||||
setTimeout(() => {
|
||||
if (this.mouseOnPreview) {
|
||||
// show popover
|
||||
this.popoverHidden = false
|
||||
} else {
|
||||
this.popover.close()
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
mouseLeavePreview() {
|
||||
this.mouseOnPreview = false
|
||||
}
|
||||
|
||||
mouseLeaveCard() {
|
||||
this.popover.close()
|
||||
}
|
||||
|
||||
get contentTrimmed() {
|
||||
return this.document.content.substr(0, 500)
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="col p-2 h-100">
|
||||
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected">
|
||||
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected" [class.popover-hidden]="popoverHidden" (mouseleave)="mouseLeaveCard()">
|
||||
<div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)">
|
||||
<img class="card-img doc-img rounded-top" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
|
||||
|
||||
@@ -25,24 +25,60 @@
|
||||
<ng-container *ngIf="document.correspondent">
|
||||
<a [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>:
|
||||
</ng-container>
|
||||
{{document.title | documentTitle}} <span *ngIf="document.archive_serial_number">(#{{document.archive_serial_number}})</span>
|
||||
{{document.title | documentTitle}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="card-footer pt-0 pb-2 px-2">
|
||||
<div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
|
||||
<button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent pl-0 p-1 border-0" title="Filter by document type"
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
<svg class="metadata-icon mr-2 text-muted bi bi-file-earmark" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
|
||||
</svg>
|
||||
<small>{{(document.document_type$ | async)?.name}}</small>
|
||||
</button>
|
||||
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
||||
<ng-template #dateTooltip>
|
||||
<div class="d-flex flex-column">
|
||||
<span i18n>Created: {{ document.created | customDate}}</span>
|
||||
<span i18n>Added: {{ document.added | customDate}}</span>
|
||||
<span i18n>Modified: {{ document.modified | customDate}}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mx-n2">
|
||||
<div class="btn-group">
|
||||
<div class="pl-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
|
||||
<svg class="metadata-icon mr-2 text-muted bi bi-calendar-event" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/>
|
||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||
</svg>
|
||||
<small>{{document.created | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
<div *ngIf="document.archive_serial_number" class="pl-0 p-1">
|
||||
<svg class="metadata-icon mr-2 text-muted bi bi-upc-scan" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5zM3 4.5a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-7zm3 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7z"/>
|
||||
</svg>
|
||||
<small>#{{document.archive_serial_number}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group w-100">
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title>
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a [href]="getPreviewUrl()" class="btn btn-sm btn-outline-secondary" title="View in browser" i18n-title>
|
||||
<a [href]="previewUrl" target="_blank" class="btn btn-sm btn-outline-secondary"
|
||||
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
|
||||
autoClose="true" popoverClass="shadow" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
|
||||
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
||||
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<ng-template #previewContent>
|
||||
<object [data]="previewUrl | safe" class="preview" width="100%"></object>
|
||||
</ng-template>
|
||||
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" (click)="$event.stopPropagation()" i18n-title>
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||
@@ -50,9 +86,7 @@
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<small class="text-muted pl-1">{{document.created | customDate:'shortDate'}}</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,9 +1,13 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.card-text {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.doc-img {
|
||||
object-fit: cover;
|
||||
object-position: top left;
|
||||
height: 200px;
|
||||
height: 175px;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
@@ -34,3 +38,32 @@
|
||||
.doc-img-background-selected {
|
||||
background-color: $primaryFaded;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
line-height: 1;
|
||||
|
||||
button {
|
||||
line-height: 1;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: transparent !important;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.metadata-icon {
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
padding: 0.05rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer .btn {
|
||||
padding-top: .10rem;
|
||||
}
|
||||
|
||||
::ng-deep .tooltip-inner {
|
||||
text-align: left !important;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-card-small',
|
||||
templateUrl: './document-card-small.component.html',
|
||||
styleUrls: ['./document-card-small.component.scss']
|
||||
styleUrls: ['./document-card-small.component.scss', '../popover-preview/popover-preview.scss']
|
||||
})
|
||||
export class DocumentCardSmallComponent implements OnInit {
|
||||
|
||||
@@ -15,7 +16,7 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
|
||||
@Input()
|
||||
selected = false
|
||||
|
||||
|
||||
@Output()
|
||||
toggleSelected = new EventEmitter()
|
||||
|
||||
@@ -28,8 +29,16 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
@Output()
|
||||
clickCorrespondent = new EventEmitter<number>()
|
||||
|
||||
@Output()
|
||||
clickDocumentType = new EventEmitter<number>()
|
||||
|
||||
moreTags: number = null
|
||||
|
||||
@ViewChild('popover') popover: NgbPopover
|
||||
|
||||
mouseOnPreview = false
|
||||
popoverHidden = true
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
@@ -45,7 +54,7 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
return this.documentService.getDownloadUrl(this.document.id)
|
||||
}
|
||||
|
||||
getPreviewUrl() {
|
||||
get previewUrl() {
|
||||
return this.documentService.getPreviewUrl(this.document.id)
|
||||
}
|
||||
|
||||
@@ -62,4 +71,28 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
)
|
||||
}
|
||||
|
||||
mouseEnterPreview() {
|
||||
this.mouseOnPreview = true
|
||||
if (!this.popover.isOpen()) {
|
||||
// we're going to open but hide to pre-load content during hover delay
|
||||
this.popover.open()
|
||||
this.popoverHidden = true
|
||||
setTimeout(() => {
|
||||
if (this.mouseOnPreview) {
|
||||
// show popover
|
||||
this.popoverHidden = false
|
||||
} else {
|
||||
this.popover.close()
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
mouseLeavePreview() {
|
||||
this.mouseOnPreview = false
|
||||
}
|
||||
|
||||
mouseLeaveCard() {
|
||||
this.popover.close()
|
||||
}
|
||||
}
|
||||
|
@@ -75,8 +75,8 @@
|
||||
|
||||
</app-page-header>
|
||||
|
||||
<div class="w-100 mb-2 mb-sm-4">
|
||||
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [rulesModified]="filterRulesModified" (filterRulesChange)="rulesChanged()" (reset)="resetFilters()" #filterEditor></app-filter-editor>
|
||||
<div class="sticky-top py-2 mt-n2 mt-sm-n3 py-sm-4 bg-body mx-n3 px-3">
|
||||
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" #filterEditor></app-filter-editor>
|
||||
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
|
||||
</div>
|
||||
|
||||
@@ -89,86 +89,95 @@
|
||||
[rotate]="true" aria-label="Default pagination"></ngb-pagination>
|
||||
</div>
|
||||
|
||||
<div *ngIf="displayMode == 'largeCards'">
|
||||
<app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)">
|
||||
</app-document-card-large>
|
||||
</div>
|
||||
<ng-container *ngIf="list.error ; else documentListNoError">
|
||||
<div class="alert alert-danger" role="alert">Error while loading documents: {{list.error}}</div>
|
||||
</ng-container>
|
||||
|
||||
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th class="d-none d-lg-table-cell"
|
||||
sortable="archive_serial_number"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>ASN</th>
|
||||
<th class="d-none d-md-table-cell"
|
||||
sortable="correspondent__name"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Correspondent</th>
|
||||
<th
|
||||
sortable="title"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Title</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
sortable="document_type__name"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Document type</th>
|
||||
<th
|
||||
sortable="created"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Created</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
sortable="added"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Added</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" (click)="toggleSelected(d, $event)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
|
||||
<td>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event)">
|
||||
<label class="custom-control-label" for="docCheck{{d.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell">
|
||||
{{d.archive_serial_number}}
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<ng-container *ngIf="d.correspondent">
|
||||
<a [routerLink]="" (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
<a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
|
||||
</td>
|
||||
<td class="d-none d-xl-table-cell">
|
||||
<ng-container *ngIf="d.document_type">
|
||||
<a [routerLink]="" (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
{{d.created | customDate}}
|
||||
</td>
|
||||
<td class="d-none d-xl-table-cell">
|
||||
{{d.added | customDate}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ng-template #documentListNoError>
|
||||
|
||||
<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
||||
<app-document-card-small [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small>
|
||||
</div>
|
||||
<div *ngIf="displayMode == 'largeCards'">
|
||||
<app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickMoreLike)="clickMoreLike(d.id)">
|
||||
</app-document-card-large>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th class="d-none d-lg-table-cell"
|
||||
sortable="archive_serial_number"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>ASN</th>
|
||||
<th class="d-none d-md-table-cell"
|
||||
sortable="correspondent__name"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Correspondent</th>
|
||||
<th
|
||||
sortable="title"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Title</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
sortable="document_type__name"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Document type</th>
|
||||
<th
|
||||
sortable="created"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Created</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
sortable="added"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Added</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" (click)="toggleSelected(d, $event)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
|
||||
<td>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event)">
|
||||
<label class="custom-control-label" for="docCheck{{d.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell">
|
||||
{{d.archive_serial_number}}
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<ng-container *ngIf="d.correspondent">
|
||||
<a [routerLink]="" (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
<a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
|
||||
</td>
|
||||
<td class="d-none d-xl-table-cell">
|
||||
<ng-container *ngIf="d.document_type">
|
||||
<a [routerLink]="" (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
{{d.created | customDate}}
|
||||
</td>
|
||||
<td class="d-none d-xl-table-cell">
|
||||
{{d.added | customDate}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
||||
<app-document-card-small [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-template>
|
||||
|
@@ -34,3 +34,12 @@ $paperless-card-breakpoints: (
|
||||
right: 0 !important;
|
||||
left: auto !important;
|
||||
}
|
||||
|
||||
.sticky-top {
|
||||
z-index: 990; // below main navbar
|
||||
top: calc(7rem - 2px); // height of navbar (mobile)
|
||||
|
||||
@media (min-width: 580px) {
|
||||
top: 3.5rem; // height of navbar
|
||||
}
|
||||
}
|
||||
|
@@ -2,12 +2,14 @@ import { Component, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { FilterRule, isFullTextFilterRule } from 'src/app/data/filter-rule';
|
||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
||||
import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive';
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
|
||||
import { DOCUMENT_SORT_FIELDS, DOCUMENT_SORT_FIELDS_FULLTEXT } from 'src/app/services/rest/document.service';
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { FilterEditorComponent } from './filter-editor/filter-editor.component';
|
||||
@@ -37,7 +39,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
|
||||
displayMode = 'smallCards' // largeCards, smallCards, details
|
||||
|
||||
filterRulesModified: boolean = false
|
||||
unmodifiedFilterRules: FilterRule[] = []
|
||||
|
||||
private consumptionFinishedSubscription: Subscription
|
||||
|
||||
@@ -50,7 +52,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getSortFields() {
|
||||
return DOCUMENT_SORT_FIELDS
|
||||
return isFullTextFilterRule(this.list.filterRules) ? DOCUMENT_SORT_FIELDS_FULLTEXT : DOCUMENT_SORT_FIELDS
|
||||
}
|
||||
|
||||
onSort(event: SortEvent) {
|
||||
@@ -81,12 +83,12 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.list.activateSavedView(view)
|
||||
this.list.reload()
|
||||
this.rulesChanged()
|
||||
this.unmodifiedFilterRules = view.filter_rules
|
||||
})
|
||||
} else {
|
||||
this.list.activateSavedView(null)
|
||||
this.list.reload()
|
||||
this.rulesChanged()
|
||||
this.unmodifiedFilterRules = []
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -100,7 +102,6 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
loadViewConfig(view: PaperlessSavedView) {
|
||||
this.list.loadSavedView(view)
|
||||
this.list.reload()
|
||||
this.rulesChanged()
|
||||
}
|
||||
|
||||
saveViewConfig() {
|
||||
@@ -113,6 +114,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.savedViewService.patch(savedView).subscribe(result => {
|
||||
this.toastService.showInfo($localize`View "${this.list.activeSavedViewTitle}" saved successfully.`)
|
||||
this.unmodifiedFilterRules = this.list.filterRules
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -141,46 +143,6 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
}
|
||||
|
||||
resetFilters(): void {
|
||||
this.filterRulesModified = false
|
||||
if (this.list.activeSavedViewId) {
|
||||
this.savedViewService.getCached(this.list.activeSavedViewId).subscribe(viewUntouched => {
|
||||
this.list.filterRules = viewUntouched.filter_rules
|
||||
this.list.reload()
|
||||
})
|
||||
} else {
|
||||
this.list.filterRules = []
|
||||
this.list.reload()
|
||||
}
|
||||
}
|
||||
|
||||
rulesChanged() {
|
||||
let modified = false
|
||||
if (this.list.activeSavedViewId == null) {
|
||||
modified = this.list.filterRules.length > 0 // documents list is modified if it has any filters
|
||||
} else {
|
||||
// compare savedView current filters vs original
|
||||
this.savedViewService.getCached(this.list.activeSavedViewId).subscribe(view => {
|
||||
let filterRulesInitial = view.filter_rules
|
||||
|
||||
if (this.list.filterRules.length !== filterRulesInitial.length) modified = true
|
||||
else {
|
||||
modified = this.list.filterRules.some(rule => {
|
||||
return (filterRulesInitial.find(fri => fri.rule_type == rule.rule_type && fri.value == rule.value) == undefined)
|
||||
})
|
||||
|
||||
if (!modified) {
|
||||
// only check other direction if we havent already determined is modified
|
||||
modified = filterRulesInitial.some(rule => {
|
||||
this.list.filterRules.find(fr => fr.rule_type == rule.rule_type && fr.value == rule.value) == undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
this.filterRulesModified = modified
|
||||
}
|
||||
|
||||
toggleSelected(document: PaperlessDocument, event: MouseEvent): void {
|
||||
if (!event.shiftKey) this.list.toggleSelected(document)
|
||||
else this.list.selectRangeTo(document)
|
||||
@@ -207,6 +169,10 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
}
|
||||
|
||||
clickMoreLike(documentID: number) {
|
||||
this.list.quickFilter([{rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString()}])
|
||||
}
|
||||
|
||||
trackByDocumentId(index, item: PaperlessDocument) {
|
||||
return item.id
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<div class="row">
|
||||
<div class="col mb-2 mb-xl-0">
|
||||
<div class="form-inline d-flex align-items-center">
|
||||
<label class="text-muted mr-2 mb-0" i18n>Filter by:</label>
|
||||
<div class="input-group input-group-sm flex-fill w-auto">
|
||||
<div class="input-group-prepend" ngbDropdown>
|
||||
<button class="btn btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>
|
||||
@@ -9,7 +8,7 @@
|
||||
<button *ngFor="let t of textFilterTargets" ngbDropdownItem [class.active]="textFilterTarget == t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<input class="form-control form-control-sm" type="text" [(ngModel)]="textFilter">
|
||||
<input #textFilterInput class="form-control form-control-sm" type="text" [(ngModel)]="textFilter" [readonly]="textFilterTarget == 'fulltext-morelike'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag';
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||
@@ -8,12 +8,17 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service
|
||||
import { TagService } from 'src/app/services/rest/tag.service';
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
||||
import { FilterRule } from 'src/app/data/filter-rule';
|
||||
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_ANY_TAG, FILTER_HAS_TAG, FILTER_TITLE, FILTER_TITLE_CONTENT } from 'src/app/data/filter-rule-type';
|
||||
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_ASN, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_FULLTEXT_MORELIKE, FILTER_FULLTEXT_QUERY, FILTER_HAS_ANY_TAG, FILTER_HAS_TAG, FILTER_TITLE, FILTER_TITLE_CONTENT } from 'src/app/data/filter-rule-type';
|
||||
import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
|
||||
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
|
||||
const TEXT_FILTER_TARGET_TITLE = "title"
|
||||
const TEXT_FILTER_TARGET_TITLE_CONTENT = "title-content"
|
||||
const TEXT_FILTER_TARGET_ASN = "asn"
|
||||
const TEXT_FILTER_TARGET_FULLTEXT_QUERY = "fulltext-query"
|
||||
const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = "fulltext-morelike"
|
||||
|
||||
@Component({
|
||||
selector: 'app-filter-editor',
|
||||
@@ -51,6 +56,9 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
|
||||
case FILTER_TITLE:
|
||||
return $localize`Title: ${rule.value}`
|
||||
|
||||
case FILTER_ASN:
|
||||
return $localize`ASN: ${rule.value}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,19 +68,33 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private documentTypeService: DocumentTypeService,
|
||||
private tagService: TagService,
|
||||
private correspondentService: CorrespondentService
|
||||
private correspondentService: CorrespondentService,
|
||||
private documentService: DocumentService
|
||||
) { }
|
||||
|
||||
@ViewChild("textFilterInput")
|
||||
textFilterInput: ElementRef
|
||||
|
||||
tags: PaperlessTag[] = []
|
||||
correspondents: PaperlessCorrespondent[] = []
|
||||
documentTypes: PaperlessDocumentType[] = []
|
||||
|
||||
_textFilter = ""
|
||||
_moreLikeId: number
|
||||
_moreLikeDoc: PaperlessDocument
|
||||
|
||||
textFilterTargets = [
|
||||
{id: TEXT_FILTER_TARGET_TITLE, name: $localize`Title`},
|
||||
{id: TEXT_FILTER_TARGET_TITLE_CONTENT, name: $localize`Title & content`}
|
||||
]
|
||||
get textFilterTargets() {
|
||||
let targets = [
|
||||
{id: TEXT_FILTER_TARGET_TITLE, name: $localize`Title`},
|
||||
{id: TEXT_FILTER_TARGET_TITLE_CONTENT, name: $localize`Title & content`},
|
||||
{id: TEXT_FILTER_TARGET_ASN, name: $localize`ASN`},
|
||||
{id: TEXT_FILTER_TARGET_FULLTEXT_QUERY, name: $localize`Advanced search`}
|
||||
]
|
||||
if (this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) {
|
||||
targets.push({id: TEXT_FILTER_TARGET_FULLTEXT_MORELIKE, name: $localize`More like`})
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
|
||||
|
||||
@@ -90,12 +112,28 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
dateAddedBefore: string
|
||||
dateAddedAfter: string
|
||||
|
||||
_unmodifiedFilterRules: FilterRule[] = []
|
||||
_filterRules: FilterRule[] = []
|
||||
|
||||
@Input()
|
||||
set unmodifiedFilterRules(value: FilterRule[]) {
|
||||
this._unmodifiedFilterRules = value
|
||||
this.checkIfRulesHaveChanged()
|
||||
}
|
||||
|
||||
get unmodifiedFilterRules(): FilterRule[] {
|
||||
return this._unmodifiedFilterRules
|
||||
}
|
||||
|
||||
@Input()
|
||||
set filterRules (value: FilterRule[]) {
|
||||
this._filterRules = value
|
||||
|
||||
this.documentTypeSelectionModel.clear(false)
|
||||
this.tagSelectionModel.clear(false)
|
||||
this.correspondentSelectionModel.clear(false)
|
||||
this._textFilter = null
|
||||
this._moreLikeId = null
|
||||
this.dateAddedBefore = null
|
||||
this.dateAddedAfter = null
|
||||
this.dateCreatedBefore = null
|
||||
@@ -111,6 +149,22 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
this._textFilter = rule.value
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
|
||||
break
|
||||
case FILTER_ASN:
|
||||
this._textFilter = rule.value
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||
break
|
||||
case FILTER_FULLTEXT_QUERY:
|
||||
this._textFilter = rule.value
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_QUERY
|
||||
break
|
||||
case FILTER_FULLTEXT_MORELIKE:
|
||||
this._moreLikeId = +rule.value
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
|
||||
this.documentService.get(this._moreLikeId).subscribe(result => {
|
||||
this._moreLikeDoc = result
|
||||
this._textFilter = result.title
|
||||
})
|
||||
break
|
||||
case FILTER_CREATED_AFTER:
|
||||
this.dateCreatedAfter = rule.value
|
||||
break
|
||||
@@ -137,6 +191,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
break
|
||||
}
|
||||
})
|
||||
this.checkIfRulesHaveChanged()
|
||||
}
|
||||
|
||||
get filterRules(): FilterRule[] {
|
||||
@@ -147,6 +202,15 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
if (this._textFilter && this.textFilterTarget == TEXT_FILTER_TARGET_TITLE) {
|
||||
filterRules.push({rule_type: FILTER_TITLE, value: this._textFilter})
|
||||
}
|
||||
if (this._textFilter && this.textFilterTarget == TEXT_FILTER_TARGET_ASN) {
|
||||
filterRules.push({rule_type: FILTER_ASN, value: this._textFilter})
|
||||
}
|
||||
if (this._textFilter && this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_QUERY) {
|
||||
filterRules.push({rule_type: FILTER_FULLTEXT_QUERY, value: this._textFilter})
|
||||
}
|
||||
if (this._moreLikeId && this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) {
|
||||
filterRules.push({rule_type: FILTER_FULLTEXT_MORELIKE, value: this._moreLikeId?.toString()})
|
||||
}
|
||||
if (this.tagSelectionModel.isNoneSelected()) {
|
||||
filterRules.push({rule_type: FILTER_HAS_ANY_TAG, value: "false"})
|
||||
} else {
|
||||
@@ -178,12 +242,27 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
@Output()
|
||||
filterRulesChange = new EventEmitter<FilterRule[]>()
|
||||
|
||||
@Output()
|
||||
reset = new EventEmitter()
|
||||
|
||||
@Input()
|
||||
rulesModified: boolean = false
|
||||
|
||||
private checkIfRulesHaveChanged() {
|
||||
let modified = false
|
||||
if (this._unmodifiedFilterRules.length != this._filterRules.length) {
|
||||
modified = true
|
||||
} else {
|
||||
modified = this._unmodifiedFilterRules.some(rule => {
|
||||
return (this._filterRules.find(fri => fri.rule_type == rule.rule_type && fri.value == rule.value) == undefined)
|
||||
})
|
||||
|
||||
if (!modified) {
|
||||
// only check other direction if we havent already determined is modified
|
||||
modified = this._filterRules.some(rule => {
|
||||
this._unmodifiedFilterRules.find(fr => fr.rule_type == rule.rule_type && fr.value == rule.value) == undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
this.rulesModified = modified
|
||||
}
|
||||
|
||||
updateRules() {
|
||||
this.filterRulesChange.next(this.filterRules)
|
||||
}
|
||||
@@ -211,8 +290,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
distinctUntilChanged()
|
||||
).subscribe(text => {
|
||||
this._textFilter = text
|
||||
this.documentService.searchQuery = text
|
||||
this.updateRules()
|
||||
})
|
||||
|
||||
if (this._textFilter) this.documentService.searchQuery = this._textFilter
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -220,7 +303,9 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
resetSelected() {
|
||||
this.reset.next()
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
|
||||
this.filterRules = this._unmodifiedFilterRules
|
||||
this.updateRules()
|
||||
}
|
||||
|
||||
toggleTag(tagId: number) {
|
||||
@@ -248,7 +333,11 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
changeTextFilterTarget(target) {
|
||||
if (this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE && target != TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) {
|
||||
this._textFilter = ""
|
||||
}
|
||||
this.textFilterTarget = target
|
||||
this.textFilterInput.nativeElement.focus()
|
||||
this.updateRules()
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,22 @@
|
||||
::ng-deep .popover {
|
||||
max-width: 40rem;
|
||||
|
||||
.preview {
|
||||
min-width: 30rem;
|
||||
min-height: 18rem;
|
||||
max-height: 35rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
position: absolute;
|
||||
top: 4rem;
|
||||
left: calc(50% - 0.5rem);
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .popover-hidden .popover {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
@@ -11,8 +11,8 @@
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
|
||||
<div class="bg-dark p-3 mb-3 text-light text-monospace log-container">
|
||||
<div class="bg-dark p-3 text-light text-monospace log-container" #logContainer>
|
||||
<p
|
||||
class="m-0 p-0 log-entry-{{getLogLevel(log)}}"
|
||||
*ngFor="let log of logs" style="white-space: pre;">{{log}}</p>
|
||||
*ngFor="let log of logs">{{log}}</p>
|
||||
</div>
|
||||
|
@@ -16,9 +16,11 @@
|
||||
}
|
||||
|
||||
.log-container {
|
||||
|
||||
overflow: scroll;
|
||||
|
||||
height: calc(100vh - 190px);
|
||||
overflow-y: scroll;
|
||||
height: calc(100vh - 200px);
|
||||
top: 70px;
|
||||
}
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, ElementRef, OnInit, AfterViewChecked, ViewChild } from '@angular/core';
|
||||
import { LogService } from 'src/app/services/rest/log.service';
|
||||
|
||||
@Component({
|
||||
@@ -6,7 +6,7 @@ import { LogService } from 'src/app/services/rest/log.service';
|
||||
templateUrl: './logs.component.html',
|
||||
styleUrls: ['./logs.component.scss']
|
||||
})
|
||||
export class LogsComponent implements OnInit {
|
||||
export class LogsComponent implements OnInit, AfterViewChecked {
|
||||
|
||||
constructor(private logService: LogService) { }
|
||||
|
||||
@@ -16,6 +16,8 @@ export class LogsComponent implements OnInit {
|
||||
|
||||
activeLog: string
|
||||
|
||||
@ViewChild('logContainer') logContainer: ElementRef
|
||||
|
||||
ngOnInit(): void {
|
||||
this.logService.list().subscribe(result => {
|
||||
this.logFiles = result
|
||||
@@ -26,6 +28,10 @@ export class LogsComponent implements OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
reloadLogs() {
|
||||
this.logService.get(this.activeLog).subscribe(result => {
|
||||
this.logs = result
|
||||
@@ -48,4 +54,12 @@ export class LogsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom(): void {
|
||||
this.logContainer?.nativeElement.scroll({
|
||||
top: this.logContainer.nativeElement.scrollHeight,
|
||||
left: 0,
|
||||
behavior: 'auto'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -111,11 +111,11 @@
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
|
||||
<li [ngbNavItem]="2">
|
||||
<a ngbNavLink i18n>Notifications</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
|
||||
<h4 i18n>Document processing</h4>
|
||||
|
||||
<div class="form-row form-group">
|
||||
|
@@ -1,3 +0,0 @@
|
||||
... <span *ngFor="let fragment of highlights">
|
||||
<span *ngFor="let token of fragment" [class.match]="token.highlight">{{token.text}}</span> ...
|
||||
</span>
|
@@ -1,4 +0,0 @@
|
||||
.match {
|
||||
color: black;
|
||||
background-color: rgb(255, 211, 66);
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ResultHighlightComponent } from './result-highlight.component';
|
||||
|
||||
describe('ResultHighlightComponent', () => {
|
||||
let component: ResultHighlightComponent;
|
||||
let fixture: ComponentFixture<ResultHighlightComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ResultHighlightComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ResultHighlightComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -1,19 +0,0 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { SearchHitHighlight } from 'src/app/data/search-result';
|
||||
|
||||
@Component({
|
||||
selector: 'app-result-highlight',
|
||||
templateUrl: './result-highlight.component.html',
|
||||
styleUrls: ['./result-highlight.component.scss']
|
||||
})
|
||||
export class ResultHighlightComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
@Input()
|
||||
highlights: SearchHitHighlight[][]
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
<app-page-header i18n-title title="Search results">
|
||||
</app-page-header>
|
||||
|
||||
<div *ngIf="errorMessage" class="alert alert-danger" i18n>Invalid search query: {{errorMessage}}</div>
|
||||
|
||||
<p *ngIf="more_like" i18n>
|
||||
Showing documents similar to <a routerLink="/documents/{{more_like}}">{{more_like_doc?.original_file_name}}</a>
|
||||
</p>
|
||||
|
||||
<p *ngIf="query">
|
||||
<ng-container i18n>Search query: <i>{{query}}</i></ng-container>
|
||||
<ng-container *ngIf="correctedQuery">
|
||||
- <ng-container i18n>Did you mean "<a [routerLink]="" (click)="searchCorrectedQuery()">{{correctedQuery}}</a>"?</ng-container>
|
||||
</ng-container>
|
||||
</p>
|
||||
|
||||
<div *ngIf="!errorMessage" [class.result-content-searching]="searching" infiniteScroll (scrolled)="onScroll()">
|
||||
<p i18n>{resultCount, plural, =0 {No results} =1 {One result} other {{{resultCount}} results}}</p>
|
||||
<ng-container *ngFor="let result of results">
|
||||
<app-document-card-large *ngIf="result.document"
|
||||
[document]="result.document"
|
||||
[details]="result.highlights"
|
||||
[searchScore]="result.score / maxScore"
|
||||
[moreLikeThis]="true">
|
||||
</app-document-card-large>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
@@ -1,15 +0,0 @@
|
||||
.result-content {
|
||||
color: darkgray;
|
||||
}
|
||||
|
||||
.doc-img {
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
||||
}
|
||||
|
||||
.result-content-searching {
|
||||
opacity: 0.3;
|
||||
}
|
@@ -1,95 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||
import { SearchHit } from 'src/app/data/search-result';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { SearchService } from 'src/app/services/rest/search.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search',
|
||||
templateUrl: './search.component.html',
|
||||
styleUrls: ['./search.component.scss']
|
||||
})
|
||||
export class SearchComponent implements OnInit {
|
||||
|
||||
results: SearchHit[] = []
|
||||
|
||||
query: string = ""
|
||||
|
||||
more_like: number
|
||||
|
||||
more_like_doc: PaperlessDocument
|
||||
|
||||
searching = false
|
||||
|
||||
currentPage = 1
|
||||
|
||||
pageCount = 1
|
||||
|
||||
resultCount
|
||||
|
||||
correctedQuery: string = null
|
||||
|
||||
errorMessage: string
|
||||
|
||||
get maxScore() {
|
||||
return this.results?.length > 0 ? this.results[0].score : 100
|
||||
}
|
||||
|
||||
constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router, private documentService: DocumentService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParamMap.subscribe(paramMap => {
|
||||
window.scrollTo(0, 0)
|
||||
this.query = paramMap.get('query')
|
||||
this.more_like = paramMap.has('more_like') ? +paramMap.get('more_like') : null
|
||||
if (this.more_like) {
|
||||
this.documentService.get(this.more_like).subscribe(r => {
|
||||
this.more_like_doc = r
|
||||
})
|
||||
} else {
|
||||
this.more_like_doc = null
|
||||
}
|
||||
this.searching = true
|
||||
this.currentPage = 1
|
||||
this.loadPage()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
searchCorrectedQuery() {
|
||||
this.router.navigate(["search"], {queryParams: {query: this.correctedQuery, more_like: this.more_like}})
|
||||
}
|
||||
|
||||
loadPage(append: boolean = false) {
|
||||
this.errorMessage = null
|
||||
this.correctedQuery = null
|
||||
|
||||
this.searchService.search(this.query, this.currentPage, this.more_like).subscribe(result => {
|
||||
if (append) {
|
||||
this.results.push(...result.results)
|
||||
} else {
|
||||
this.results = result.results
|
||||
}
|
||||
this.pageCount = result.page_count
|
||||
this.searching = false
|
||||
this.resultCount = result.count
|
||||
this.correctedQuery = result.corrected_query
|
||||
}, error => {
|
||||
this.searching = false
|
||||
this.resultCount = 1
|
||||
this.pageCount = 1
|
||||
this.results = []
|
||||
this.errorMessage = error.error
|
||||
})
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage += 1
|
||||
this.loadPage(true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -22,6 +22,9 @@ export const FILTER_ASN_ISNULL = 18
|
||||
|
||||
export const FILTER_TITLE_CONTENT = 19
|
||||
|
||||
export const FILTER_FULLTEXT_QUERY = 20
|
||||
export const FILTER_FULLTEXT_MORELIKE = 21
|
||||
|
||||
export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
|
||||
{id: FILTER_TITLE, filtervar: "title__icontains", datatype: "string", multi: false, default: ""},
|
||||
@@ -51,7 +54,11 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
{id: FILTER_MODIFIED_AFTER, filtervar: "modified__date__gt", datatype: "date", multi: false},
|
||||
{id: FILTER_ASN_ISNULL, filtervar: "archive_serial_number__isnull", datatype: "boolean", multi: false},
|
||||
|
||||
{id: FILTER_TITLE_CONTENT, filtervar: "title_content", datatype: "string", multi: false}
|
||||
{id: FILTER_TITLE_CONTENT, filtervar: "title_content", datatype: "string", multi: false},
|
||||
|
||||
{id: FILTER_FULLTEXT_QUERY, filtervar: "query", datatype: "string", multi: false},
|
||||
|
||||
{id: FILTER_FULLTEXT_MORELIKE, filtervar: "more_like_id", datatype: "number", multi: false},
|
||||
]
|
||||
|
||||
export interface FilterRuleType {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { FILTER_FULLTEXT_MORELIKE, FILTER_FULLTEXT_QUERY } from "./filter-rule-type"
|
||||
|
||||
export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] {
|
||||
if (filterRules) {
|
||||
let newRules: FilterRule[] = []
|
||||
@@ -10,6 +12,10 @@ export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] {
|
||||
}
|
||||
}
|
||||
|
||||
export function isFullTextFilterRule(filterRules: FilterRule[]): boolean {
|
||||
return filterRules.find(r => r.rule_type == FILTER_FULLTEXT_QUERY || r.rule_type == FILTER_FULLTEXT_MORELIKE) != null
|
||||
}
|
||||
|
||||
export interface FilterRule {
|
||||
rule_type: number
|
||||
value: string
|
||||
|
@@ -4,6 +4,15 @@ import { PaperlessTag } from './paperless-tag'
|
||||
import { PaperlessDocumentType } from './paperless-document-type'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
export interface SearchHit {
|
||||
|
||||
score?: number
|
||||
rank?: number
|
||||
|
||||
highlights?: string
|
||||
|
||||
}
|
||||
|
||||
export interface PaperlessDocument extends ObjectWithId {
|
||||
|
||||
correspondent$?: Observable<PaperlessCorrespondent>
|
||||
@@ -40,4 +49,6 @@ export interface PaperlessDocument extends ObjectWithId {
|
||||
|
||||
archive_serial_number?: number
|
||||
|
||||
__search_hit__?: SearchHit
|
||||
|
||||
}
|
||||
|
@@ -1,29 +0,0 @@
|
||||
import { PaperlessDocument } from './paperless-document'
|
||||
|
||||
export class SearchHitHighlight {
|
||||
text?: string
|
||||
term?: number
|
||||
}
|
||||
|
||||
export interface SearchHit {
|
||||
id?: number
|
||||
title?: string
|
||||
score?: number
|
||||
rank?: number
|
||||
|
||||
highlights?: SearchHitHighlight[][]
|
||||
document?: PaperlessDocument
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
|
||||
count?: number
|
||||
page?: number
|
||||
page_count?: number
|
||||
|
||||
corrected_query?: string
|
||||
|
||||
results?: SearchHit[]
|
||||
|
||||
|
||||
}
|
@@ -4,8 +4,8 @@ import { SettingsService, SETTINGS_KEYS } from '../services/settings.service';
|
||||
|
||||
const FORMAT_TO_ISO_FORMAT = {
|
||||
"longDate": "y-MM-dd",
|
||||
"mediumDate": "yy-MM-dd",
|
||||
"shortDate": "yy-MM-dd"
|
||||
"mediumDate": "y-MM-dd",
|
||||
"shortDate": "y-MM-dd"
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
|
@@ -125,7 +125,7 @@ export class ConsumerStatusService {
|
||||
connect() {
|
||||
this.disconnect()
|
||||
|
||||
this.statusWebSocket = new WebSocket(`${environment.webSocketProtocol}//${environment.webSocketHost}/ws/status/`);
|
||||
this.statusWebSocket = new WebSocket(`${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`);
|
||||
this.statusWebSocket.onmessage = (ev) => {
|
||||
let statusMessage: WebsocketConsumerStatusMessage = JSON.parse(ev['data'])
|
||||
|
||||
|
@@ -1,27 +1,53 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { cloneFilterRules, FilterRule } from '../data/filter-rule';
|
||||
import { cloneFilterRules, FilterRule, isFullTextFilterRule } from '../data/filter-rule';
|
||||
import { PaperlessDocument } from '../data/paperless-document';
|
||||
import { PaperlessSavedView } from '../data/paperless-saved-view';
|
||||
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys';
|
||||
import { DocumentService } from './rest/document.service';
|
||||
import { SettingsService, SETTINGS_KEYS } from './settings.service';
|
||||
|
||||
/**
|
||||
* Captures the current state of the list view.
|
||||
*/
|
||||
interface ListViewState {
|
||||
|
||||
/**
|
||||
* Title of the document list view. Either "Documents" (localized) or the name of a saved view.
|
||||
*/
|
||||
title?: string
|
||||
|
||||
/**
|
||||
* Current paginated list of documents displayed.
|
||||
*/
|
||||
documents?: PaperlessDocument[]
|
||||
|
||||
currentPage: number
|
||||
|
||||
/**
|
||||
* Total amount of documents with the current filter rules. Used to calculate the number of pages.
|
||||
*/
|
||||
collectionSize: number
|
||||
|
||||
/**
|
||||
* Currently selected sort field.
|
||||
*/
|
||||
sortField: string
|
||||
|
||||
/**
|
||||
* True if the list is sorted in reverse.
|
||||
*/
|
||||
sortReverse: boolean
|
||||
|
||||
/**
|
||||
* Filter rules for the current list view.
|
||||
*/
|
||||
filterRules: FilterRule[]
|
||||
|
||||
/**
|
||||
* Contains the IDs of all selected documents.
|
||||
*/
|
||||
selected?: Set<number>
|
||||
|
||||
}
|
||||
@@ -38,6 +64,7 @@ interface ListViewState {
|
||||
export class DocumentListViewService {
|
||||
|
||||
isReloading: boolean = false
|
||||
error: string = null
|
||||
|
||||
rangeSelectionAnchorIndex: number
|
||||
lastRangeSelectionToIndex: number
|
||||
@@ -101,6 +128,7 @@ export class DocumentListViewService {
|
||||
|
||||
reload(onFinish?) {
|
||||
this.isReloading = true
|
||||
this.error = null
|
||||
let activeListViewState = this.activeListViewState
|
||||
|
||||
this.documentService.listFiltered(
|
||||
@@ -124,11 +152,16 @@ export class DocumentListViewService {
|
||||
// this happens when applying a filter: the current page might not be available anymore due to the reduced result set.
|
||||
activeListViewState.currentPage = 1
|
||||
this.reload()
|
||||
} else {
|
||||
this.error = error.error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
set filterRules(filterRules: FilterRule[]) {
|
||||
if (!isFullTextFilterRule(filterRules) && this.activeListViewState.sortField == "score") {
|
||||
this.activeListViewState.sortField = "created"
|
||||
}
|
||||
this.activeListViewState.filterRules = filterRules
|
||||
this.reload()
|
||||
this.reduceSelectionToFilter()
|
||||
@@ -197,7 +230,7 @@ export class DocumentListViewService {
|
||||
sortField: this.activeListViewState.sortField,
|
||||
sortReverse: this.activeListViewState.sortReverse
|
||||
}
|
||||
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(savedState))
|
||||
localStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(savedState))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,9 +238,17 @@ export class DocumentListViewService {
|
||||
this._activeSavedViewId = null
|
||||
this.activeListViewState.filterRules = filterRules
|
||||
this.activeListViewState.currentPage = 1
|
||||
if (isFullTextFilterRule(filterRules)) {
|
||||
this.activeListViewState.sortField = "score"
|
||||
this.activeListViewState.sortReverse = false
|
||||
}
|
||||
this.reduceSelectionToFilter()
|
||||
this.saveDocumentListView()
|
||||
this.router.navigate(["documents"])
|
||||
if (this.router.url == "/documents") {
|
||||
this.reload()
|
||||
} else {
|
||||
this.router.navigate(["documents"])
|
||||
}
|
||||
}
|
||||
|
||||
getLastPage(): number {
|
||||
@@ -317,8 +358,8 @@ export class DocumentListViewService {
|
||||
return this.documents.map(d => d.id).indexOf(documentID)
|
||||
}
|
||||
|
||||
constructor(private documentService: DocumentService, private settings: SettingsService, private router: Router) {
|
||||
let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||
constructor(private documentService: DocumentService, private settings: SettingsService, private router: Router, private route: ActivatedRoute) {
|
||||
let documentListViewConfigJson = localStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||
if (documentListViewConfigJson) {
|
||||
try {
|
||||
let savedState: ListViewState = JSON.parse(documentListViewConfigJson)
|
||||
@@ -332,7 +373,7 @@ export class DocumentListViewService {
|
||||
let newState = Object.assign(this.defaultListViewState(), savedState)
|
||||
this.listViewStates.set(null, newState)
|
||||
} catch (e) {
|
||||
sessionStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||
localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,11 @@ export const DOCUMENT_SORT_FIELDS = [
|
||||
{ field: 'modified', name: $localize`Modified` }
|
||||
]
|
||||
|
||||
export const DOCUMENT_SORT_FIELDS_FULLTEXT = [
|
||||
...DOCUMENT_SORT_FIELDS,
|
||||
{ field: 'score', name: $localize`:Score is a value returned by the full text search engine and specifies how well a result matches the given query:Search score` }
|
||||
]
|
||||
|
||||
export interface SelectionDataItem {
|
||||
id: number
|
||||
document_count: number
|
||||
@@ -39,6 +44,8 @@ export interface SelectionData {
|
||||
})
|
||||
export class DocumentService extends AbstractPaperlessService<PaperlessDocument> {
|
||||
|
||||
private _searchQuery: string
|
||||
|
||||
constructor(http: HttpClient, private correspondentService: CorrespondentService, private documentTypeService: DocumentTypeService, private tagService: TagService) {
|
||||
super(http, 'documents')
|
||||
}
|
||||
@@ -92,6 +99,7 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
||||
|
||||
getPreviewUrl(id: number, original: boolean = false): string {
|
||||
let url = this.getResourceUrl(id, 'preview')
|
||||
if (this._searchQuery) url += `#search="${this._searchQuery}"`
|
||||
if (original) {
|
||||
url += "?original=true"
|
||||
}
|
||||
@@ -138,4 +146,8 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
||||
return this.http.post(this.getResourceUrl(null, 'bulk_download'), {"documents": ids, "content": content}, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
public set searchQuery(query: string) {
|
||||
this._searchQuery = query
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -2,8 +2,6 @@ import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { SearchResult } from 'src/app/data/search-result';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { DocumentService } from './document.service';
|
||||
|
||||
@@ -13,30 +11,7 @@ import { DocumentService } from './document.service';
|
||||
})
|
||||
export class SearchService {
|
||||
|
||||
constructor(private http: HttpClient, private documentService: DocumentService) { }
|
||||
|
||||
search(query: string, page?: number, more_like?: number): Observable<SearchResult> {
|
||||
let httpParams = new HttpParams()
|
||||
if (query) {
|
||||
httpParams = httpParams.set('query', query)
|
||||
}
|
||||
if (page) {
|
||||
httpParams = httpParams.set('page', page.toString())
|
||||
}
|
||||
if (more_like) {
|
||||
httpParams = httpParams.set('more_like', more_like.toString())
|
||||
}
|
||||
return this.http.get<SearchResult>(`${environment.apiBaseUrl}search/`, {params: httpParams}).pipe(
|
||||
map(result => {
|
||||
result.results.forEach(hit => {
|
||||
if (hit.document) {
|
||||
this.documentService.addObservablesToDocument(hit.document)
|
||||
}
|
||||
})
|
||||
return result
|
||||
})
|
||||
)
|
||||
}
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
autocomplete(term: string): Observable<string[]> {
|
||||
return this.http.get<string[]>(`${environment.apiBaseUrl}search/autocomplete/`, {params: new HttpParams().set('term', term)})
|
||||
|
@@ -89,12 +89,17 @@ export class SettingsService {
|
||||
return [
|
||||
{code: "en-us", name: $localize`English (US)`, englishName: "English (US)", dateInputFormat: "mm/dd/yyyy"},
|
||||
{code: "en-gb", name: $localize`English (GB)`, englishName: "English (GB)", dateInputFormat: "dd/mm/yyyy"},
|
||||
{code: "de", name: $localize`German`, englishName: "German", dateInputFormat: "dd.mm.yyyy"},
|
||||
{code: "nl", name: $localize`Dutch`, englishName: "Dutch", dateInputFormat: "dd-mm-yyyy"},
|
||||
{code: "fr", name: $localize`French`, englishName: "French", dateInputFormat: "dd/mm/yyyy"},
|
||||
{code: "de-de", name: $localize`German`, englishName: "German", dateInputFormat: "dd.mm.yyyy"},
|
||||
{code: "nl-nl", name: $localize`Dutch`, englishName: "Dutch", dateInputFormat: "dd-mm-yyyy"},
|
||||
{code: "fr-fr", name: $localize`French`, englishName: "French", dateInputFormat: "dd/mm/yyyy"},
|
||||
{code: "pt-pt", name: $localize`Portuguese`, englishName: "Portuguese", dateInputFormat: "dd/mm/yyyy"},
|
||||
{code: "pt-br", name: $localize`Portuguese (Brazil)`, englishName: "Portuguese (Brazil)", dateInputFormat: "dd/mm/yyyy"},
|
||||
{code: "it", name: $localize`Italian`, englishName: "Italian", dateInputFormat: "dd/mm/yyyy"},
|
||||
{code: "ro", name: $localize`Romanian`, englishName: "Romanian", dateInputFormat: "dd.mm.yyyy"}
|
||||
{code: "it-it", name: $localize`Italian`, englishName: "Italian", dateInputFormat: "dd/mm/yyyy"},
|
||||
{code: "ro-ro", name: $localize`Romanian`, englishName: "Romanian", dateInputFormat: "dd.mm.yyyy"},
|
||||
{code: "ru-ru", name: $localize`Russian`, englishName: "Russian", dateInputFormat: "dd.mm.yyyy"},
|
||||
{code: "es-es", name: $localize`Spanish`, englishName: "Spanish", dateInputFormat: "dd/mm/yyyy"},
|
||||
{code: "pl-pl", name: $localize`Polish`, englishName: "Polish", dateInputFormat: "dd.mm.yyyy"},
|
||||
{code: "sv-se", name: $localize`Swedish`, englishName: "Swedish", dateInputFormat: "yyyy-mm-dd"}
|
||||
]
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,12 @@
|
||||
const base_url = new URL(document.baseURI)
|
||||
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiBaseUrl: "/api/",
|
||||
apiBaseUrl: document.baseURI + "api/",
|
||||
apiVersion: "2",
|
||||
appTitle: "Paperless-ng",
|
||||
version: "1.3.0",
|
||||
version: "1.4.3",
|
||||
webSocketHost: window.location.host,
|
||||
webSocketProtocol: (window.location.protocol == "https:" ? "wss:" : "ws:")
|
||||
webSocketProtocol: (window.location.protocol == "https:" ? "wss:" : "ws:"),
|
||||
webSocketBaseUrl: base_url.pathname + "ws/",
|
||||
};
|
||||
|
@@ -9,7 +9,8 @@ export const environment = {
|
||||
appTitle: "Paperless-ng",
|
||||
version: "DEVELOPMENT",
|
||||
webSocketHost: "localhost:8000",
|
||||
webSocketProtocol: "ws:"
|
||||
webSocketProtocol: "ws:",
|
||||
webSocketBaseUrl: "/ws/",
|
||||
};
|
||||
|
||||
/*
|
||||
|
2314
src-ui/src/locale/messages.cs_CZ.xlf
Normal file
2314
src-ui/src/locale/messages.cs_CZ.xlf
Normal file
File diff suppressed because it is too large
Load Diff
2314
src-ui/src/locale/messages.de_DE.xlf
Normal file
2314
src-ui/src/locale/messages.de_DE.xlf
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2314
src-ui/src/locale/messages.es_ES.xlf
Normal file
2314
src-ui/src/locale/messages.es_ES.xlf
Normal file
File diff suppressed because it is too large
Load Diff
2314
src-ui/src/locale/messages.fr_FR.xlf
Normal file
2314
src-ui/src/locale/messages.fr_FR.xlf
Normal file
File diff suppressed because it is too large
Load Diff
2314
src-ui/src/locale/messages.hu_HU.xlf
Normal file
2314
src-ui/src/locale/messages.hu_HU.xlf
Normal file
File diff suppressed because it is too large
Load Diff
2314
src-ui/src/locale/messages.it_IT.xlf
Normal file
2314
src-ui/src/locale/messages.it_IT.xlf
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2314
src-ui/src/locale/messages.pl_PL.xlf
Normal file
2314
src-ui/src/locale/messages.pl_PL.xlf
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2314
src-ui/src/locale/messages.pt_PT.xlf
Normal file
2314
src-ui/src/locale/messages.pt_PT.xlf
Normal file
File diff suppressed because it is too large
Load Diff
2314
src-ui/src/locale/messages.ro_RO.xlf
Normal file
2314
src-ui/src/locale/messages.ro_RO.xlf
Normal file
File diff suppressed because it is too large
Load Diff
2314
src-ui/src/locale/messages.ru_RU.xlf
Normal file
2314
src-ui/src/locale/messages.ru_RU.xlf
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2314
src-ui/src/locale/messages.zh_CN.xlf
Normal file
2314
src-ui/src/locale/messages.zh_CN.xlf
Normal file
File diff suppressed because it is too large
Load Diff
30
src-ui/src/print.scss
Normal file
30
src-ui/src/print.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
@media print {
|
||||
#sidebarMenu, .btn-toolbar {
|
||||
display: none !important
|
||||
}
|
||||
|
||||
.sticky-top {
|
||||
display: none;
|
||||
}
|
||||
|
||||
main, main.ml-sm-auto, main.mx-sm-auto {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
main.col-lg-10 {
|
||||
max-width: 100%;
|
||||
flex-basis: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.d-none.d-lg-table-cell { // always display ASN on print
|
||||
display: table-cell !important;
|
||||
}
|
||||
|
||||
app-document-list table {
|
||||
thead th:first-child, tbody td:first-child { // hide checkboxes
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
@import "theme";
|
||||
@import "theme_dark";
|
||||
@import "print";
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
@import "~@ng-select/ng-select/themes/default.theme.css";
|
||||
|
||||
|
@@ -4,4 +4,8 @@ $primaryFaded: #d1ddd2;
|
||||
|
||||
$theme-colors: (
|
||||
"primary": $primary
|
||||
);
|
||||
);
|
||||
|
||||
.bg-body {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
@@ -37,6 +37,10 @@ $border-color-dark-mode: #47494f;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-body {
|
||||
background-color: $bg-dark-mode !important;
|
||||
}
|
||||
|
||||
.text-light {
|
||||
color: $text-color-dark-mode !important;
|
||||
}
|
||||
@@ -76,6 +80,10 @@ $border-color-dark-mode: #47494f;
|
||||
}
|
||||
}
|
||||
|
||||
.page-item.active .page-link {
|
||||
background-color: darken($primary-dark-mode, 10%);
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
border-color: $border-color-dark-mode;
|
||||
|
||||
@@ -226,7 +234,7 @@ $border-color-dark-mode: #47494f;
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
border-color: $text-color-dark-mode;
|
||||
border-color: darken($text-color-dark-mode, 30%);
|
||||
color: $text-color-dark-mode;
|
||||
|
||||
&:not(:disabled):not(.disabled):hover {
|
||||
@@ -279,6 +287,10 @@ $border-color-dark-mode: #47494f;
|
||||
background-color: $bg-dark-mode !important;
|
||||
}
|
||||
|
||||
.card-footer button:hover {
|
||||
color: $primary-dark-mode !important;
|
||||
}
|
||||
|
||||
.form-control:not(.is-invalid):not(.btn),
|
||||
input:not(.is-invalid),
|
||||
textarea:not(.is-invalid) {
|
||||
@@ -392,7 +404,7 @@ $border-color-dark-mode: #47494f;
|
||||
border-color: $border-color-dark-mode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$placements: 'top', 'right', 'bottom', 'left';
|
||||
|
||||
@each $placement in $placements {
|
||||
|
@@ -64,9 +64,9 @@ class Consumer(LoggingMixin):
|
||||
{'type': 'status_update',
|
||||
'data': payload})
|
||||
|
||||
def _fail(self, message, log_message=None):
|
||||
def _fail(self, message, log_message=None, exc_info=None):
|
||||
self._send_progress(100, 100, 'FAILED', message)
|
||||
self.log("error", log_message or message)
|
||||
self.log("error", log_message or message, exc_info=exc_info)
|
||||
raise ConsumerError(f"{self.filename}: {log_message or message}")
|
||||
|
||||
def __init__(self):
|
||||
@@ -115,12 +115,16 @@ class Consumer(LoggingMixin):
|
||||
f"Configured pre-consume script "
|
||||
f"{settings.PRE_CONSUME_SCRIPT} does not exist.")
|
||||
|
||||
self.log("info",
|
||||
f"Executing pre-consume script {settings.PRE_CONSUME_SCRIPT}")
|
||||
|
||||
try:
|
||||
Popen((settings.PRE_CONSUME_SCRIPT, self.path)).wait()
|
||||
except Exception as e:
|
||||
self._fail(
|
||||
MESSAGE_PRE_CONSUME_SCRIPT_ERROR,
|
||||
f"Error while executing pre-consume script: {e}"
|
||||
f"Error while executing pre-consume script: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def run_post_consume_script(self, document):
|
||||
@@ -134,6 +138,11 @@ class Consumer(LoggingMixin):
|
||||
f"{settings.POST_CONSUME_SCRIPT} does not exist."
|
||||
)
|
||||
|
||||
self.log(
|
||||
"info",
|
||||
f"Executing post-consume script {settings.POST_CONSUME_SCRIPT}"
|
||||
)
|
||||
|
||||
try:
|
||||
Popen((
|
||||
settings.POST_CONSUME_SCRIPT,
|
||||
@@ -150,7 +159,8 @@ class Consumer(LoggingMixin):
|
||||
except Exception as e:
|
||||
self._fail(
|
||||
MESSAGE_POST_CONSUME_SCRIPT_ERROR,
|
||||
f"Error while executing post-consume script: {e}"
|
||||
f"Error while executing post-consume script: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def try_consume_file(self,
|
||||
@@ -255,7 +265,8 @@ class Consumer(LoggingMixin):
|
||||
document_parser.cleanup()
|
||||
self._fail(
|
||||
str(e),
|
||||
f"Error while consuming document {self.filename}: {e}"
|
||||
f"Error while consuming document {self.filename}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
# Prepare the document classifier.
|
||||
@@ -326,7 +337,8 @@ class Consumer(LoggingMixin):
|
||||
self._fail(
|
||||
str(e),
|
||||
f"The following error occured while consuming "
|
||||
f"{self.filename}: {e}"
|
||||
f"{self.filename}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
finally:
|
||||
document_parser.cleanup()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user